From 18958be72066a683b26d91846bbd98de869b7386 Mon Sep 17 00:00:00 2001 From: dwindown Date: Mon, 30 Mar 2026 22:22:25 +0700 Subject: [PATCH] Show cached schedule coverage status in admin --- lib/data/services/sync_service.dart | 78 ++++++++++++++++++++++++++++ lib/features/admin/admin_screen.dart | 34 ++++++++++-- lib/providers.dart | 5 ++ test/widget_test.dart | 39 ++++++++++++++ 4 files changed, 153 insertions(+), 3 deletions(-) diff --git a/lib/data/services/sync_service.dart b/lib/data/services/sync_service.dart index 1c4931e..5e2a878 100644 --- a/lib/data/services/sync_service.dart +++ b/lib/data/services/sync_service.dart @@ -4,6 +4,75 @@ import 'package:intl/intl.dart'; import '../local/models.dart'; import 'myquran_service.dart'; +class ScheduleCacheStatus { + final DateTime? startDate; + final DateTime? endDate; + final int cachedDays; + final int daysUntilRefresh; + + const ScheduleCacheStatus({ + required this.startDate, + required this.endDate, + required this.cachedDays, + required this.daysUntilRefresh, + }); + + const ScheduleCacheStatus.empty() + : startDate = null, + endDate = null, + cachedDays = 0, + daysUntilRefresh = -1; + + bool get hasData => startDate != null && endDate != null && cachedDays > 0; + bool get isExpired => hasData && daysUntilRefresh < 0; + bool get needsRefreshSoon => hasData && daysUntilRefresh <= 3; + + static ScheduleCacheStatus fromSchedules( + Iterable schedules, + DateTime referenceDate, + ) { + DateTime? startDate; + DateTime? endDate; + var cachedDays = 0; + + for (final schedule in schedules) { + final parsedDate = DateTime.tryParse(schedule.date); + if (parsedDate == null) continue; + + final normalized = DateTime( + parsedDate.year, + parsedDate.month, + parsedDate.day, + ); + + cachedDays++; + startDate = startDate == null || normalized.isBefore(startDate) + ? normalized + : startDate; + endDate = endDate == null || normalized.isAfter(endDate) + ? normalized + : endDate; + } + + if (startDate == null || endDate == null || cachedDays == 0) { + return const ScheduleCacheStatus.empty(); + } + + final today = DateTime( + referenceDate.year, + referenceDate.month, + referenceDate.day, + ); + + return ScheduleCacheStatus( + startDate: startDate, + endDate: endDate, + cachedDays: cachedDays, + daysUntilRefresh: endDate.difference(today).inDays, + ); + } +} + /// Service to sync monthly prayer data from MyQuran API → Hive. class SyncService { SyncService._(); @@ -90,4 +159,13 @@ class SyncService { final dateStr = DateFormat('yyyy-MM-dd').format(dateToFetch); return scheduleBox.get(dateStr); } + + ScheduleCacheStatus getCacheStatus([DateTime? referenceDate]) { + final scheduleBox = + Hive.box(HiveBoxes.prayerSchedule); + return ScheduleCacheStatus.fromSchedules( + scheduleBox.values, + referenceDate ?? DateTime.now(), + ); + } } diff --git a/lib/features/admin/admin_screen.dart b/lib/features/admin/admin_screen.dart index 71315eb..3da56b8 100644 --- a/lib/features/admin/admin_screen.dart +++ b/lib/features/admin/admin_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:hugeicons/hugeicons.dart'; +import 'package:intl/intl.dart'; import '../../core/sacred_tokens.dart'; import '../../providers.dart'; @@ -240,6 +241,7 @@ class _AdminScreenState extends ConsumerState { if (mounted) { ref.invalidate(todayScheduleProvider); + ref.invalidate(scheduleCacheStatusProvider); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -1142,7 +1144,11 @@ class _AdminScreenState extends ConsumerState { Widget _buildJadwalTab(double s) { final settings = ref.watch(settingsProvider); final todayScheduleOption = ref.watch(todayScheduleProvider); + final cacheStatus = ref.watch(scheduleCacheStatusProvider); final displayedHijri = ref.watch(hijriDateProvider).valueOrNull; + final cacheRangeLabel = cacheStatus.hasData + ? '${_formatCacheDate(cacheStatus.startDate)} - ${_formatCacheDate(cacheStatus.endDate)}' + : 'Belum ada data'; return SingleChildScrollView( controller: _jadwalScrollController, @@ -1180,13 +1186,16 @@ class _AdminScreenState extends ConsumerState { style: GoogleFonts.manrope(fontSize: 20 * s, fontWeight: FontWeight.w700, color: SacredColors.onSurfaceVariant, letterSpacing: 1 * s), ), SizedBox(height: 24 * s), - Row( + Wrap( + spacing: 48 * s, + runSpacing: 20 * s, children: [ _buildStatusRow('Terakhir Sync', settings.lastSyncDate ?? 'Belum pernah', HugeIcons.strokeRoundedClock01, s), - SizedBox(width: 48 * s), _buildStatusRow('Sumber Data', 'api.myquran.com', HugeIcons.strokeRoundedDatabase01, s), - SizedBox(width: 48 * s), _buildStatusRow('Lokasi Data', settings.cityDisplayName, HugeIcons.strokeRoundedLocation01, s), + _buildStatusRow('Cache Tersimpan', cacheRangeLabel, HugeIcons.strokeRoundedCalendar03, s), + _buildStatusRow('Jumlah Hari', cacheStatus.hasData ? '${cacheStatus.cachedDays} hari' : '0 hari', HugeIcons.strokeRoundedTaskDaily01, s), + _buildStatusRow('Status Update', _buildCacheUpdateLabel(cacheStatus, todayScheduleOption != null), HugeIcons.strokeRoundedAlert02, s), ], ), ], @@ -2000,6 +2009,25 @@ class _AdminScreenState extends ConsumerState { ); } + String _formatCacheDate(DateTime? date) { + if (date == null) return 'Belum ada'; + return DateFormat('dd MMM yyyy').format(date); + } + + String _buildCacheUpdateLabel( + ScheduleCacheStatus status, + bool hasTodayData, + ) { + if (!status.hasData) return 'Belum ada cache'; + if (!hasTodayData) return 'Hari ini belum tersimpan'; + if (status.daysUntilRefresh < 0) { + return 'Lewat ${-status.daysUntilRefresh} hari'; + } + if (status.daysUntilRefresh == 0) return 'Update hari ini'; + if (status.daysUntilRefresh == 1) return '1 hari lagi'; + return '${status.daysUntilRefresh} hari lagi'; + } + Widget _scaleSlider({ required double s, required String label, diff --git a/lib/providers.dart b/lib/providers.dart index 5917432..4628ce2 100644 --- a/lib/providers.dart +++ b/lib/providers.dart @@ -77,6 +77,11 @@ final todayScheduleProvider = Provider((ref) { return SyncService.instance.getTodaySchedule(clock); }); +final scheduleCacheStatusProvider = Provider((ref) { + final clock = ref.watch(clockProvider).valueOrNull ?? DateTime.now(); + return SyncService.instance.getCacheStatus(clock); +}); + final hijriDateProvider = FutureProvider((ref) async { final clock = ref.watch(clockProvider).valueOrNull ?? DateTime.now(); final hijriOffsetDays = diff --git a/test/widget_test.dart b/test/widget_test.dart index c5a4e00..acd15c4 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:jamshalat_masjid_screen/core/enums.dart'; import 'package:jamshalat_masjid_screen/data/local/models.dart'; import 'package:jamshalat_masjid_screen/data/services/hijri_service.dart'; +import 'package:jamshalat_masjid_screen/data/services/sync_service.dart'; void main() { group('PrayerName display labels', () { @@ -56,4 +57,42 @@ void main() { expect(label, '11 Syawal 1447 H'); }); }); + + group('ScheduleCacheStatus', () { + test('derives cached date range and days left from stored schedules', () { + final status = ScheduleCacheStatus.fromSchedules( + [ + DailyPrayerSchedule( + date: '2026-03-01', + imsak: '04:20', + subuh: '04:30', + terbit: '05:45', + dhuha: '06:10', + dzuhur: '11:55', + ashar: '15:10', + maghrib: '17:58', + isya: '19:05', + ), + DailyPrayerSchedule( + date: '2026-04-30', + imsak: '04:19', + subuh: '04:29', + terbit: '05:44', + dhuha: '06:09', + dzuhur: '11:54', + ashar: '15:09', + maghrib: '17:57', + isya: '19:04', + ), + ], + DateTime(2026, 3, 30), + ); + + expect(status.hasData, isTrue); + expect(status.startDate, DateTime(2026, 3, 1)); + expect(status.endDate, DateTime(2026, 4, 30)); + expect(status.cachedDays, 2); + expect(status.daysUntilRefresh, 31); + }); + }); }