Files
jamshalat-diary/lib/features/checklist/presentation/checklist_screen.dart

652 lines
22 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:intl/intl.dart';
import '../../../app/theme/app_colors.dart';
import '../../../core/widgets/progress_bar.dart';
import '../../../data/local/hive_boxes.dart';
import '../../../data/local/models/app_settings.dart';
import '../../../data/local/models/daily_worship_log.dart';
import '../../../data/local/models/shalat_log.dart';
import '../../../data/local/models/tilawah_log.dart';
import '../../../data/local/models/dzikir_log.dart';
import '../../../data/local/models/puasa_log.dart';
class ChecklistScreen extends ConsumerStatefulWidget {
const ChecklistScreen({super.key});
@override
ConsumerState<ChecklistScreen> createState() => _ChecklistScreenState();
}
class _ChecklistScreenState extends ConsumerState<ChecklistScreen> {
late String _todayKey;
late Box<DailyWorshipLog> _logBox;
late Box<AppSettings> _settingsBox;
late AppSettings _settings;
final List<String> _fardhuPrayers = ['Subuh', 'Dzuhur', 'Ashar', 'Maghrib', 'Isya'];
@override
void initState() {
super.initState();
_todayKey = DateFormat('yyyy-MM-dd').format(DateTime.now());
_logBox = Hive.box<DailyWorshipLog>(HiveBoxes.worshipLogs);
_settingsBox = Hive.box<AppSettings>(HiveBoxes.settings);
_settings = _settingsBox.get('default') ?? AppSettings();
_ensureLogExists();
}
void _ensureLogExists() {
if (!_logBox.containsKey(_todayKey)) {
final shalatLogs = <String, ShalatLog>{};
for (final p in _fardhuPrayers) {
shalatLogs[p.toLowerCase()] = ShalatLog();
}
_logBox.put(
_todayKey,
DailyWorshipLog(
date: _todayKey,
shalatLogs: shalatLogs,
tilawahLog: TilawahLog(
targetValue: _settings.tilawahTargetValue,
targetUnit: _settings.tilawahTargetUnit,
autoSync: _settings.tilawahAutoSync,
),
dzikirLog: _settings.trackDzikir ? DzikirLog() : null,
puasaLog: _settings.trackPuasa ? PuasaLog() : null,
),
);
}
}
DailyWorshipLog get _todayLog => _logBox.get(_todayKey)!;
void _recalculateProgress() {
final log = _todayLog;
// Lazily attach Dzikir and Puasa if user toggles them mid-day
if (_settings.trackDzikir && log.dzikirLog == null) log.dzikirLog = DzikirLog();
if (_settings.trackPuasa && log.puasaLog == null) log.puasaLog = PuasaLog();
int total = 0;
int completed = 0;
// Shalat
for (final p in _fardhuPrayers) {
final pKey = p.toLowerCase();
final sLog = log.shalatLogs[pKey];
if (sLog != null) {
total++;
if (sLog.completed) completed++;
if (hasQabliyah(pKey, _settings.rawatibLevel)) {
total++;
if (sLog.qabliyah == true) completed++;
}
if (hasBadiyah(pKey, _settings.rawatibLevel)) {
total++;
if (sLog.badiyah == true) completed++;
}
}
}
// Tilawah
if (log.tilawahLog != null) {
total++;
if (log.tilawahLog!.isCompleted) completed++;
}
// Dzikir
if (_settings.trackDzikir && log.dzikirLog != null) {
total += 2;
if (log.dzikirLog!.pagi) completed++;
if (log.dzikirLog!.petang) completed++;
}
// Puasa
if (_settings.trackPuasa && log.puasaLog != null) {
total++;
if (log.puasaLog!.completed) completed++;
}
log.totalItems = total;
log.completedCount = completed;
log.completionPercent = total > 0 ? completed / total : 0.0;
log.save();
setState(() {});
}
bool hasQabliyah(String prayer, int level) {
if (level == 0) return false;
if (prayer == 'subuh') return true;
if (prayer == 'dzuhur') return true;
if (prayer == 'ashar') return level == 2; // Ghairu Muakkad
if (prayer == 'maghrib') return level == 2; // Ghairu Muakkad
if (prayer == 'isya') return level == 2; // Ghairu Muakkad
return false;
}
bool hasBadiyah(String prayer, int level) {
if (level == 0) return false;
if (prayer == 'subuh') return false;
if (prayer == 'dzuhur') return true;
if (prayer == 'ashar') return false;
if (prayer == 'maghrib') return true;
if (prayer == 'isya') return true;
return false;
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final log = _todayLog;
return Scaffold(
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Ibadah Harian'),
Text(
DateFormat('EEEE, d MMM yyyy').format(DateTime.now()),
style: theme.textTheme.bodySmall?.copyWith(
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
),
),
],
),
centerTitle: false,
actions: [
IconButton(
onPressed: () {},
icon: const Icon(LucideIcons.bell),
),
IconButton(
onPressed: () => context.push('/settings'),
icon: const Icon(LucideIcons.settings),
),
const SizedBox(width: 8),
],
),
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16),
children: [
const SizedBox(height: 12),
if (!_settings.simpleMode) ...[
_buildProgressCard(log, isDark),
const SizedBox(height: 24),
],
_sectionLabel('SHOLAT FARDHU & RAWATIB'),
const SizedBox(height: 12),
..._fardhuPrayers.map((p) => _buildShalatCard(p, isDark)).toList(),
const SizedBox(height: 24),
_sectionLabel('TILAWAH AL-QURAN'),
const SizedBox(height: 12),
_buildTilawahCard(isDark),
if (_settings.trackDzikir || _settings.trackPuasa) ...[
const SizedBox(height: 24),
_sectionLabel('AMALAN TAMBAHAN'),
const SizedBox(height: 12),
],
if (_settings.trackDzikir) _buildDzikirCard(isDark),
if (_settings.trackPuasa) _buildPuasaCard(isDark),
const SizedBox(height: 32),
],
),
);
}
Widget _sectionLabel(String text) {
return Text(
text,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
letterSpacing: 1.5,
color: AppColors.sage,
),
);
}
Widget _buildProgressCard(DailyWorshipLog log, bool isDark) {
final percent = log.completionPercent;
final remaining = log.totalItems - log.completedCount;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isDark ? AppColors.surfaceDark : const Color(0xFF2B3441),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 16,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"POIN HARI INI",
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 1.5,
color: AppColors.primary.withValues(alpha: 0.8),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: AppColors.primary.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
const Icon(LucideIcons.star, color: AppColors.primary, size: 14),
const SizedBox(width: 4),
Text(
'${log.totalPoints} pts',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: AppColors.primary,
),
),
],
),
),
],
),
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${(percent * 100).round()}%',
style: const TextStyle(
fontSize: 42,
fontWeight: FontWeight.w800,
color: Colors.white,
height: 1.1,
),
),
const SizedBox(width: 8),
const Padding(
padding: EdgeInsets.only(bottom: 8),
child: Text(
'Selesai',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.white70,
),
),
),
],
),
const SizedBox(height: 16),
AppProgressBar(
value: percent,
height: 8,
backgroundColor: Colors.white.withValues(alpha: 0.15),
fillColor: AppColors.primary,
),
const SizedBox(height: 12),
Text(
remaining == 0 && log.totalItems > 0
? 'MasyaAllah! Poin maksimal tercapai hari ini! 🎉'
: 'Kumpulkan poin lebih banyak dengan Sholat di Masjid dan amalan sunnah lainnya!',
style: TextStyle(
fontSize: 13,
color: Colors.white.withValues(alpha: 0.7),
),
),
],
),
);
}
Widget _buildShalatCard(String prayerName, bool isDark) {
final pKey = prayerName.toLowerCase();
final log = _todayLog.shalatLogs[pKey];
if (log == null) return const SizedBox.shrink();
final hasQab = hasQabliyah(pKey, _settings.rawatibLevel);
final hasBad = hasBadiyah(pKey, _settings.rawatibLevel);
final isCompleted = log.completed;
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isCompleted
? AppColors.primary.withValues(alpha: 0.3)
: (isDark ? AppColors.primary.withValues(alpha: 0.08) : AppColors.cream),
),
),
child: Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
leading: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: isCompleted
? AppColors.primary.withValues(alpha: 0.15)
: (isDark ? AppColors.primary.withValues(alpha: 0.08) : AppColors.cream.withValues(alpha: 0.5)),
borderRadius: BorderRadius.circular(12),
),
child: Icon(LucideIcons.building, size: 22, color: isCompleted ? AppColors.primary : AppColors.sage),
),
title: Text(
'Sholat $prayerName',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
color: isCompleted && isDark ? AppColors.textSecondaryDark : null,
decoration: isCompleted ? TextDecoration.lineThrough : null,
),
),
subtitle: log.location != null
? Text('Di ${log.location}', style: const TextStyle(fontSize: 12, color: AppColors.primary))
: null,
trailing: _CustomCheckbox(
value: isCompleted,
onChanged: (v) {
log.completed = v ?? false;
_recalculateProgress();
},
),
childrenPadding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
children: [
const Divider(),
const SizedBox(height: 8),
// Location Radio
Row(
children: [
const Text('Pelaksanaan:', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
const SizedBox(width: 16),
_radioOption('Masjid', log, () {
log.location = 'Masjid';
log.completed = true; // Auto-check parent
_recalculateProgress();
}),
const SizedBox(width: 16),
_radioOption('Rumah', log, () {
log.location = 'Rumah';
log.completed = true; // Auto-check parent
_recalculateProgress();
}),
],
),
if (hasQab || hasBad) const SizedBox(height: 12),
if (hasQab)
_sunnahRow('Qabliyah $prayerName', log.qabliyah ?? false, (v) {
log.qabliyah = v;
_recalculateProgress();
}),
if (hasBad)
_sunnahRow('Ba\'diyah $prayerName', log.badiyah ?? false, (v) {
log.badiyah = v;
_recalculateProgress();
}),
],
),
),
);
}
Widget _radioOption(String title, ShalatLog log, VoidCallback onTap) {
final selected = log.location == title;
return GestureDetector(
onTap: onTap,
child: Row(
children: [
Icon(
selected ? LucideIcons.checkCircle2 : LucideIcons.circle,
size: 18,
color: selected ? AppColors.primary : Colors.grey,
),
const SizedBox(width: 4),
Text(title, style: TextStyle(fontSize: 13, color: selected ? AppColors.primary : null)),
],
),
);
}
Widget _sunnahRow(String title, bool value, ValueChanged<bool?> onChanged) {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title, style: const TextStyle(fontSize: 14)),
_CustomCheckbox(value: value, onChanged: onChanged),
],
),
);
}
Widget _buildTilawahCard(bool isDark) {
final log = _todayLog.tilawahLog;
if (log == null) return const SizedBox.shrink();
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: log.isCompleted
? AppColors.primary.withValues(alpha: 0.3)
: (isDark ? AppColors.primary.withValues(alpha: 0.08) : AppColors.cream),
),
),
child: Column(
children: [
// ── Row 1: Target + Checkbox ──
Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: log.isCompleted
? AppColors.primary.withValues(alpha: 0.15)
: (isDark ? AppColors.primary.withValues(alpha: 0.08) : AppColors.cream.withValues(alpha: 0.5)),
borderRadius: BorderRadius.circular(12),
),
child: Icon(LucideIcons.bookOpen, size: 22, color: log.isCompleted ? AppColors.primary : AppColors.sage),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Tilawah',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: log.isCompleted && isDark ? AppColors.textSecondaryDark : null,
decoration: log.isCompleted ? TextDecoration.lineThrough : null,
),
),
Text(
'Target: ${log.targetValue} ${log.targetUnit}',
style: const TextStyle(fontSize: 12, color: AppColors.primary),
),
],
),
),
_CustomCheckbox(
value: log.targetCompleted,
onChanged: (v) {
log.targetCompleted = v ?? false;
_recalculateProgress();
},
),
],
),
const SizedBox(height: 12),
const Divider(height: 1),
const SizedBox(height: 12),
// ── Row 2: Ayat Tracker ──
Row(
children: [
Icon(LucideIcons.bookOpen, size: 18, color: AppColors.sage),
const SizedBox(width: 8),
Expanded(
child: Text(
'Sudah Baca: ${log.rawAyatRead} Ayat',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight,
),
),
),
if (log.autoSync)
Tooltip(
message: 'Sinkron dari Al-Quran',
child: Icon(LucideIcons.refreshCw, size: 16, color: AppColors.primary),
),
IconButton(
icon: const Icon(LucideIcons.minusCircle, size: 20),
visualDensity: VisualDensity.compact,
onPressed: log.rawAyatRead > 0
? () {
log.rawAyatRead--;
_recalculateProgress();
}
: null,
),
IconButton(
icon: const Icon(LucideIcons.plusCircle, size: 20, color: AppColors.primary),
visualDensity: VisualDensity.compact,
onPressed: () {
log.rawAyatRead++;
_recalculateProgress();
},
),
],
),
],
),
);
}
Widget _buildDzikirCard(bool isDark) {
final log = _todayLog.dzikirLog;
if (log == null) return const SizedBox.shrink();
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(LucideIcons.sparkles, size: 20, color: AppColors.sage),
const SizedBox(width: 8),
const Text('Dzikir Harian', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15)),
],
),
const SizedBox(height: 12),
_sunnahRow('Dzikir Pagi', log.pagi, (v) {
log.pagi = v ?? false;
_recalculateProgress();
}),
_sunnahRow('Dzikir Petang', log.petang, (v) {
log.petang = v ?? false;
_recalculateProgress();
}),
],
),
);
}
Widget _buildPuasaCard(bool isDark) {
final log = _todayLog.puasaLog;
if (log == null) return const SizedBox.shrink();
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isDark ? AppColors.surfaceDark : AppColors.surfaceLight,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
const Icon(LucideIcons.moonStar, size: 20, color: AppColors.sage),
const SizedBox(width: 8),
const Expanded(child: Text('Puasa Sunnah', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15))),
DropdownButton<String>(
value: log.jenisPuasa,
hint: const Text('Jenis', style: TextStyle(fontSize: 12)),
underline: const SizedBox(),
items: ['Senin', 'Kamis', 'Ayyamul Bidh', 'Daud', 'Lainnya']
.map((e) => DropdownMenuItem(value: e, child: Text(e, style: const TextStyle(fontSize: 13))))
.toList(),
onChanged: (v) {
log.jenisPuasa = v;
_recalculateProgress();
},
),
const SizedBox(width: 8),
_CustomCheckbox(
value: log.completed,
onChanged: (v) {
log.completed = v ?? false;
_recalculateProgress();
},
),
],
),
);
}
}
class _CustomCheckbox extends StatelessWidget {
final bool value;
final ValueChanged<bool?> onChanged;
const _CustomCheckbox({required this.value, required this.onChanged});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => onChanged(!value),
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: value ? AppColors.primary : Colors.transparent,
borderRadius: BorderRadius.circular(6),
border: value ? null : Border.all(color: Colors.grey, width: 2),
),
child: value ? const Icon(LucideIcons.check, size: 16, color: Colors.white) : null,
),
);
}
}