amoriai/lib/screens/onboarding/daily_checkin_screen.dart

851 lines
29 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import '../../services/checkin_dataset.dart';
import '../../services/firebase_service.dart';
import '../../services/daily_checkin_service.dart';
// ─── Design tokens ───────────────────────────────────────────────────────────
const Color _brown = Color(0xFF4A3C3C);
const Color _lightBg = Color(0xFFFAF6F1);
const Color _accent = Color(0xFF8B5E3C);
const Color _grey = Color(0xFF9E9E9E);
const Color _border = Color(0xFFE8E8E8);
// ─────────────────────────────────────────────────────────────────────────────
class DailyCheckinScreen extends StatefulWidget {
final bool isMandatory;
const DailyCheckinScreen({super.key, this.isMandatory = false});
@override
State<DailyCheckinScreen> createState() => _DailyCheckinScreenState();
}
class _DailyCheckinScreenState extends State<DailyCheckinScreen> {
final PageController _pageController = PageController();
int _currentPage = 0;
bool _isSaving = false;
// Page 1 — dynamic
late final CheckinEntry _moodEntry;
late final CheckinEntry _cuacaEntry;
late final CheckinEntry _suasanaEntry;
// All answers
final Map<String, dynamic> _answers = {};
// Budget slider state
double _budget = 30000;
bool _isLoadingData = false;
@override
void initState() {
super.initState();
final daily = CheckinDataset.getDailyEntries();
_moodEntry = daily.mood;
_cuacaEntry = daily.cuaca;
_suasanaEntry = daily.suasana;
_loadPreviousData();
}
Future<void> _loadPreviousData() async {
setState(() => _isLoadingData = true);
try {
final user = FirebaseService().currentUser;
if (user != null) {
final userData = await FirebaseService().getUserData(user.uid);
if (userData != null) {
setState(() {
// Sinkronkan default budget dengan Preferensi Diri
_budget = userData.preferences.budgetRange.toDouble();
_answers['budget'] = _budget.toInt();
if (userData.dailyPreferences != null) {
userData.dailyPreferences!.forEach((key, value) {
if (key == 'budget') {
_budget = (value as num).toDouble();
_answers['budget'] = value;
} else {
_answers[key] = value.toString();
}
});
}
});
}
}
} catch (e) {
debugPrint('Error loading previous preferences: $e');
} finally {
if (mounted) setState(() => _isLoadingData = false);
}
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
// ── Helpers ────────────────────────────────────────────────────────────────
bool get _page1Complete =>
_answers.containsKey('mood') &&
_answers.containsKey('cuaca') &&
_answers.containsKey('suasana');
bool get _page2Complete =>
_answers.containsKey('keinginan') && _answers.containsKey('budget');
bool get _page3Complete => _answers.containsKey('waktu');
void _goNext() {
if (_currentPage == 0 && !_page1Complete) {
_showValidation('Mohon jawab semua pertanyaan di halaman ini.');
return;
}
if (_currentPage == 1 && !_page2Complete) {
_showValidation('Mohon pilih keinginan dan budget-mu.');
return;
}
if (_currentPage < 2) {
_pageController.nextPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
}
}
void _goPrev() {
if (_currentPage > 0) {
_pageController.previousPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
}
}
void _showValidation(String msg) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(msg, style: const TextStyle(fontFamily: 'Poppins')),
backgroundColor: _accent,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
}
Future<void> _submit() async {
if (!_page3Complete) {
_showValidation('Pilih waktu kunjunganmu dulu ya.');
return;
}
setState(() => _isSaving = true);
try {
final user = FirebaseService().currentUser;
if (user == null) throw Exception('User not logged in');
// Simpan budget sebagai int
_answers['budget'] = _budget.toInt();
// Simpan ke Daily Preferences
await DailyCheckinService().saveDailyPreferences(user.uid, _answers);
// Sinkronkan juga ke Preferensi Diri (Master Profile) agar tetap sama
final userData = await FirebaseService().getUserData(user.uid);
if (userData != null) {
final updatedPrefs = userData.preferences.copyWith(
budgetRange: _budget.toInt(),
);
await FirebaseService().updateUserPreferences(user.uid, updatedPrefs);
}
if (mounted) Navigator.of(context).pop(true);
} catch (e) {
debugPrint('Error saving check-in: $e');
if (mounted) _showValidation('Gagal menyimpan: $e');
} finally {
if (mounted) setState(() => _isSaving = false);
}
}
// ── Build ──────────────────────────────────────────────────────────────────
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _lightBg,
body: SafeArea(
child: Column(
children: [
_buildHeader(),
_buildProgressDots(),
Expanded(
child: _isLoadingData
? const Center(child: CircularProgressIndicator(color: _accent))
: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
onPageChanged: (i) => setState(() => _currentPage = i),
children: [_buildPage1(), _buildPage2(), _buildPage3()],
),
),
_buildNavBar(),
],
),
),
);
}
// ── Header ─────────────────────────────────────────────────────────────────
Widget _buildHeader() {
final titles = [
'Gimana Hari Ini? 🌤️',
'Kamu Lagi Mau Apa? 🍽️',
'Kapan Datangnya? 🕐',
];
final subs = [
'Ceritakan mood, cuaca & suasanamu',
'Pilih keinginan & budget-mu',
'Waktu kunjungan membantu rekomendasi',
];
return Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
child: Row(
children: [
if (widget.isMandatory || _currentPage > 0)
GestureDetector(
onTap: _currentPage == 0
? () => Navigator.of(context).pop()
: _goPrev,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: _border),
),
child: const Icon(
Icons.arrow_back_ios_new,
size: 16,
color: _brown,
),
),
)
else
const SizedBox(width: 36),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Text(
titles[_currentPage],
key: ValueKey(_currentPage),
style: const TextStyle(
fontFamily: 'Poppins',
fontSize: 18,
fontWeight: FontWeight.w700,
color: _brown,
),
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Text(
subs[_currentPage],
key: ValueKey('s$_currentPage'),
style: const TextStyle(
fontFamily: 'Poppins',
fontSize: 12,
color: _grey,
),
),
),
],
),
),
if (!widget.isMandatory && _currentPage == 0)
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text(
'Lewati',
style: TextStyle(
fontFamily: 'Poppins',
fontSize: 13,
color: _grey,
),
),
),
],
),
);
}
// ── Progress dots ──────────────────────────────────────────────────────────
Widget _buildProgressDots() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 14),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(3, (i) {
final active = i == _currentPage;
final done = i < _currentPage;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: const EdgeInsets.symmetric(horizontal: 4),
width: active ? 24 : 8,
height: 8,
decoration: BoxDecoration(
color: done
? _accent.withOpacity(0.4)
: active
? _accent
: _border,
borderRadius: BorderRadius.circular(4),
),
);
}),
),
);
}
// ── Page 1: Mood + Cuaca + Suasana ────────────────────────────────────────
Widget _buildPage1() {
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Column(
children: [
_buildDynamicCard(
icon: '😊',
label: 'Mood',
entry: _moodEntry,
answerKey: 'mood',
valueList: CheckinDataset.moodValues,
),
const SizedBox(height: 16),
_buildDynamicCard(
icon: '🌤️',
label: 'Cuaca',
entry: _cuacaEntry,
answerKey: 'cuaca',
valueList: CheckinDataset.cuacaValues,
),
const SizedBox(height: 16),
_buildDynamicCard(
icon: '👥',
label: 'Suasana',
entry: _suasanaEntry,
answerKey: 'suasana',
valueList: CheckinDataset.suasanaValues,
),
const SizedBox(height: 20),
],
),
);
}
Widget _buildDynamicCard({
required String icon,
required String label,
required CheckinEntry entry,
required String answerKey,
required List<String> valueList,
}) {
final selectedValue = _answers[answerKey];
return _card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(icon, style: const TextStyle(fontSize: 20)),
const SizedBox(width: 8),
Text(
label,
style: const TextStyle(
fontFamily: 'Poppins',
fontSize: 11,
fontWeight: FontWeight.w600,
color: _accent,
letterSpacing: 0.5,
),
),
],
),
const SizedBox(height: 8),
Text(
entry.question,
style: const TextStyle(
fontFamily: 'Poppins',
fontSize: 14,
fontWeight: FontWeight.w600,
color: _brown,
height: 1.4,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: List.generate(entry.options.length, (i) {
final val = valueList[i];
final selected = selectedValue == val;
return GestureDetector(
onTap: () => setState(() => _answers[answerKey] = val),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: selected ? _accent.withOpacity(0.12) : Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: selected ? _accent : _border,
width: selected ? 1.5 : 1,
),
),
child: Text(
entry.options[i],
style: TextStyle(
fontFamily: 'Poppins',
fontSize: 12,
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
color: selected ? _accent : _grey,
),
),
),
);
}),
),
],
),
);
}
// ── Page 2: Keinginan + Budget ─────────────────────────────────────────────
Widget _buildPage2() {
final keinginanOptions = [
('🥤', 'Minuman', 'minuman'),
('🍽️', 'Makanan', 'makanan'),
('☕🍽️', 'Keduanya', 'keduanya'),
];
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Column(
children: [
// Keinginan card
_card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Text('🛒', style: TextStyle(fontSize: 20)),
SizedBox(width: 8),
Text(
'KEINGINAN',
style: TextStyle(
fontFamily: 'Poppins',
fontSize: 11,
fontWeight: FontWeight.w600,
color: _accent,
letterSpacing: 0.5,
),
),
],
),
const SizedBox(height: 8),
const Text(
'Hari ini lagi mau apa?',
style: TextStyle(
fontFamily: 'Poppins',
fontSize: 14,
fontWeight: FontWeight.w600,
color: _brown,
),
),
const SizedBox(height: 14),
Row(
children: keinginanOptions.map((opt) {
final (emoji, label, val) = opt;
final selected = _answers['keinginan'] == val;
return Expanded(
child: GestureDetector(
onTap: () =>
setState(() => _answers['keinginan'] = val),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: selected
? _accent.withOpacity(0.12)
: Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: selected ? _accent : _border,
width: selected ? 1.5 : 1,
),
),
child: Column(
children: [
Text(emoji, style: const TextStyle(fontSize: 24)),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontFamily: 'Poppins',
fontSize: 12,
fontWeight: selected
? FontWeight.w700
: FontWeight.w400,
color: selected ? _accent : _grey,
),
),
],
),
),
),
);
}).toList(),
),
],
),
),
const SizedBox(height: 16),
// Budget card
_card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Text('💰', style: TextStyle(fontSize: 20)),
SizedBox(width: 8),
Text(
'BUDGET',
style: TextStyle(
fontFamily: 'Poppins',
fontSize: 11,
fontWeight: FontWeight.w600,
color: _accent,
letterSpacing: 0.5,
),
),
],
),
const SizedBox(height: 8),
const Text(
'Sekitar berapa budget-mu?',
style: TextStyle(
fontFamily: 'Poppins',
fontSize: 14,
fontWeight: FontWeight.w600,
color: _brown,
),
),
const SizedBox(height: 16),
Center(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 8,
),
decoration: BoxDecoration(
color: _accent.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
_formatRp(_budget.toInt()),
style: const TextStyle(
fontFamily: 'Poppins',
fontSize: 20,
fontWeight: FontWeight.w700,
color: _accent,
),
),
),
),
const SizedBox(height: 8),
SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: _accent,
inactiveTrackColor: _border,
thumbColor: _accent,
overlayColor: _accent.withOpacity(0.1),
trackHeight: 4,
),
child: Slider(
value: _budget,
min: 10000,
max: 200000,
divisions: 38,
onChanged: (v) => setState(() {
_budget = v;
_answers['budget'] = v.toInt();
}),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Rp 10K',
style: const TextStyle(
fontFamily: 'Poppins',
fontSize: 11,
color: _grey,
),
),
Text(
'Rp 200K',
style: const TextStyle(
fontFamily: 'Poppins',
fontSize: 11,
color: _grey,
),
),
],
),
),
],
),
),
const SizedBox(height: 20),
],
),
);
}
// ── Page 3: Waktu ──────────────────────────────────────────────────────────
Widget _buildPage3() {
final options = [
('🌅', 'Pagi', 'pagi', '06.00 11.00'),
('☀️', 'Siang', 'siang', '11.00 15.00'),
('🌤️', 'Sore', 'sore', '15.00 18.00'),
('🌙', 'Malam', 'malam', '18.00 22.00'),
];
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: _card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Text('🕐', style: TextStyle(fontSize: 20)),
SizedBox(width: 8),
Text(
'WAKTU KUNJUNGAN',
style: TextStyle(
fontFamily: 'Poppins',
fontSize: 11,
fontWeight: FontWeight.w600,
color: _accent,
letterSpacing: 0.5,
),
),
],
),
const SizedBox(height: 8),
const Text(
'Kapan kamu ke sini?',
style: TextStyle(
fontFamily: 'Poppins',
fontSize: 14,
fontWeight: FontWeight.w600,
color: _brown,
),
),
const SizedBox(height: 16),
GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.1,
children: options.map((opt) {
final (emoji, label, val, time) = opt;
final selected = _answers['waktu'] == val;
return GestureDetector(
onTap: () => setState(() => _answers['waktu'] = val),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: selected
? _accent.withOpacity(0.12)
: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: selected ? _accent : _border,
width: selected ? 1.5 : 1,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(emoji, style: const TextStyle(fontSize: 26)),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontFamily: 'Poppins',
fontSize: 13,
fontWeight: selected
? FontWeight.w700
: FontWeight.w500,
color: selected ? _accent : _brown,
),
),
Text(
time,
style: const TextStyle(
fontFamily: 'Poppins',
fontSize: 10,
color: _grey,
),
),
],
),
),
);
}).toList(),
),
const SizedBox(height: 8),
],
),
),
);
}
// ── Bottom navigation bar ──────────────────────────────────────────────────
Widget _buildNavBar() {
final isLast = _currentPage == 2;
return Container(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: Row(
children: [
// Step indicator
Text(
'${_currentPage + 1} / 3',
style: const TextStyle(
fontFamily: 'Poppins',
fontSize: 13,
color: _grey,
fontWeight: FontWeight.w500,
),
),
const Spacer(),
// Next / Submit button
GestureDetector(
onTap: _isSaving ? null : (isLast ? _submit : _goNext),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 14),
decoration: BoxDecoration(
color: _accent,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: _accent.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: _isSaving
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
isLast ? 'Selesai' : 'Lanjut',
style: const TextStyle(
fontFamily: 'Poppins',
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
const SizedBox(width: 6),
Icon(
isLast
? Icons.check_rounded
: Icons.arrow_forward_rounded,
color: Colors.white,
size: 18,
),
],
),
),
),
],
),
);
}
// ── Helpers ────────────────────────────────────────────────────────────────
Widget _card({required Widget child}) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: child,
);
}
String _formatRp(int value) {
final s = value.toString();
final buf = StringBuffer('Rp ');
for (var i = 0; i < s.length; i++) {
if (i > 0 && (s.length - i) % 3 == 0) buf.write('.');
buf.write(s[i]);
}
return buf.toString();
}
}