Show cached schedule coverage status in admin

This commit is contained in:
dwindown
2026-03-30 22:22:25 +07:00
parent fe3e2fb3fa
commit 18958be720
4 changed files with 153 additions and 3 deletions

View File

@@ -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<DailyPrayerSchedule> 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<DailyPrayerSchedule>(HiveBoxes.prayerSchedule);
return ScheduleCacheStatus.fromSchedules(
scheduleBox.values,
referenceDate ?? DateTime.now(),
);
}
}

View File

@@ -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<AdminScreen> {
if (mounted) {
ref.invalidate(todayScheduleProvider);
ref.invalidate(scheduleCacheStatusProvider);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
@@ -1142,7 +1144,11 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
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<AdminScreen> {
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<AdminScreen> {
);
}
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,

View File

@@ -77,6 +77,11 @@ final todayScheduleProvider = Provider<DailyPrayerSchedule?>((ref) {
return SyncService.instance.getTodaySchedule(clock);
});
final scheduleCacheStatusProvider = Provider<ScheduleCacheStatus>((ref) {
final clock = ref.watch(clockProvider).valueOrNull ?? DateTime.now();
return SyncService.instance.getCacheStatus(clock);
});
final hijriDateProvider = FutureProvider<String>((ref) async {
final clock = ref.watch(clockProvider).valueOrNull ?? DateTime.now();
final hijriOffsetDays =