Show cached schedule coverage status in admin
This commit is contained in:
@@ -4,6 +4,75 @@ import 'package:intl/intl.dart';
|
|||||||
import '../local/models.dart';
|
import '../local/models.dart';
|
||||||
import 'myquran_service.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.
|
/// Service to sync monthly prayer data from MyQuran API → Hive.
|
||||||
class SyncService {
|
class SyncService {
|
||||||
SyncService._();
|
SyncService._();
|
||||||
@@ -90,4 +159,13 @@ class SyncService {
|
|||||||
final dateStr = DateFormat('yyyy-MM-dd').format(dateToFetch);
|
final dateStr = DateFormat('yyyy-MM-dd').format(dateToFetch);
|
||||||
return scheduleBox.get(dateStr);
|
return scheduleBox.get(dateStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScheduleCacheStatus getCacheStatus([DateTime? referenceDate]) {
|
||||||
|
final scheduleBox =
|
||||||
|
Hive.box<DailyPrayerSchedule>(HiveBoxes.prayerSchedule);
|
||||||
|
return ScheduleCacheStatus.fromSchedules(
|
||||||
|
scheduleBox.values,
|
||||||
|
referenceDate ?? DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:hugeicons/hugeicons.dart';
|
import 'package:hugeicons/hugeicons.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import '../../core/sacred_tokens.dart';
|
import '../../core/sacred_tokens.dart';
|
||||||
import '../../providers.dart';
|
import '../../providers.dart';
|
||||||
@@ -240,6 +241,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
|||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ref.invalidate(todayScheduleProvider);
|
ref.invalidate(todayScheduleProvider);
|
||||||
|
ref.invalidate(scheduleCacheStatusProvider);
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
@@ -1142,7 +1144,11 @@ class _AdminScreenState extends ConsumerState<AdminScreen> {
|
|||||||
Widget _buildJadwalTab(double s) {
|
Widget _buildJadwalTab(double s) {
|
||||||
final settings = ref.watch(settingsProvider);
|
final settings = ref.watch(settingsProvider);
|
||||||
final todayScheduleOption = ref.watch(todayScheduleProvider);
|
final todayScheduleOption = ref.watch(todayScheduleProvider);
|
||||||
|
final cacheStatus = ref.watch(scheduleCacheStatusProvider);
|
||||||
final displayedHijri = ref.watch(hijriDateProvider).valueOrNull;
|
final displayedHijri = ref.watch(hijriDateProvider).valueOrNull;
|
||||||
|
final cacheRangeLabel = cacheStatus.hasData
|
||||||
|
? '${_formatCacheDate(cacheStatus.startDate)} - ${_formatCacheDate(cacheStatus.endDate)}'
|
||||||
|
: 'Belum ada data';
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
controller: _jadwalScrollController,
|
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),
|
style: GoogleFonts.manrope(fontSize: 20 * s, fontWeight: FontWeight.w700, color: SacredColors.onSurfaceVariant, letterSpacing: 1 * s),
|
||||||
),
|
),
|
||||||
SizedBox(height: 24 * s),
|
SizedBox(height: 24 * s),
|
||||||
Row(
|
Wrap(
|
||||||
|
spacing: 48 * s,
|
||||||
|
runSpacing: 20 * s,
|
||||||
children: [
|
children: [
|
||||||
_buildStatusRow('Terakhir Sync', settings.lastSyncDate ?? 'Belum pernah', HugeIcons.strokeRoundedClock01, s),
|
_buildStatusRow('Terakhir Sync', settings.lastSyncDate ?? 'Belum pernah', HugeIcons.strokeRoundedClock01, s),
|
||||||
SizedBox(width: 48 * s),
|
|
||||||
_buildStatusRow('Sumber Data', 'api.myquran.com', HugeIcons.strokeRoundedDatabase01, s),
|
_buildStatusRow('Sumber Data', 'api.myquran.com', HugeIcons.strokeRoundedDatabase01, s),
|
||||||
SizedBox(width: 48 * s),
|
|
||||||
_buildStatusRow('Lokasi Data', settings.cityDisplayName, HugeIcons.strokeRoundedLocation01, 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({
|
Widget _scaleSlider({
|
||||||
required double s,
|
required double s,
|
||||||
required String label,
|
required String label,
|
||||||
|
|||||||
@@ -77,6 +77,11 @@ final todayScheduleProvider = Provider<DailyPrayerSchedule?>((ref) {
|
|||||||
return SyncService.instance.getTodaySchedule(clock);
|
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 hijriDateProvider = FutureProvider<String>((ref) async {
|
||||||
final clock = ref.watch(clockProvider).valueOrNull ?? DateTime.now();
|
final clock = ref.watch(clockProvider).valueOrNull ?? DateTime.now();
|
||||||
final hijriOffsetDays =
|
final hijriOffsetDays =
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:jamshalat_masjid_screen/core/enums.dart';
|
import 'package:jamshalat_masjid_screen/core/enums.dart';
|
||||||
import 'package:jamshalat_masjid_screen/data/local/models.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/hijri_service.dart';
|
||||||
|
import 'package:jamshalat_masjid_screen/data/services/sync_service.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('PrayerName display labels', () {
|
group('PrayerName display labels', () {
|
||||||
@@ -56,4 +57,42 @@ void main() {
|
|||||||
expect(label, '11 Syawal 1447 H');
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user