amoriai/lib/screens/onboarding/early_setup_screen.dart

642 lines
19 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 'package:flutter/services.dart';
import '../../services/firebase_service.dart';
import '../../services/dialog_service.dart';
import '../home/home_screen.dart';
import '../auth/login_screen.dart';
import '../../models/user_model.dart';
// Colors matching the design
const Color primaryBrown = Color(0xFF4A3C3C);
const Color labelGray = Color(0xFF9E9E9E);
const Color borderGray = Color(0xFFE8E8E8);
class EarlySetupScreen extends StatefulWidget {
const EarlySetupScreen({super.key});
@override
State<EarlySetupScreen> createState() => _EarlySetupScreenState();
}
class _EarlySetupScreenState extends State<EarlySetupScreen> {
final PageController _pageController = PageController();
int _currentPage = 0;
bool _isLoading = false;
// Form controllers
final _nicknameController = TextEditingController();
final _addressController = TextEditingController();
// Demographics selections
String? _selectedAgeRange;
String? _selectedGender;
// Preferences selections
List<String> _selectedAllergies = [];
String _selectedDietaryRestriction = 'None';
// Questionnaire state removed — daily check-in questions
// no longer shown during onboarding.
// Options
final List<String> _ageRanges = ['< 18', '1824', '2534', '35+'];
final List<String> _genders = ['Laki-laki', 'Perempuan', 'Rahasiakan'];
final List<String> _availableAllergies = [
'Gluten',
'Kacang',
'Susu',
'Telur',
'Seafood',
'Kedelai',
'Wijen',
'Sulfit',
];
final List<String> _availableDietaryRestrictions = [
'None',
'Vegetarian',
'Vegan',
'Halal',
'Keto',
'Low Carb',
];
@override
void initState() {
super.initState();
_loadUserData();
}
@override
void dispose() {
_pageController.dispose();
_nicknameController.dispose();
_addressController.dispose();
super.dispose();
}
Future<void> _loadUserData() async {
try {
final user = FirebaseService().currentUser;
if (user != null) {
final userData = await FirebaseService().getUserData(user.uid);
if (userData != null && mounted) {
setState(() {
if (userData.nickname != null)
_nicknameController.text = userData.nickname!;
if (userData.address != null)
_addressController.text = userData.address!;
if (userData.gender != null) _selectedGender = userData.gender;
_selectedAllergies = userData.preferences.allergies;
_selectedDietaryRestriction =
userData.preferences.dietaryRestriction;
if (userData.preferences.ageRange != null) {
_selectedAgeRange = userData.preferences.ageRange;
}
});
}
}
} catch (e) {
debugPrint('Error loading user data: $e');
}
}
void _nextPage() {
// Page 0: Demographics Validation
if (_currentPage == 0) {
if (_nicknameController.text.trim().isEmpty ||
_addressController.text.trim().isEmpty ||
_selectedAgeRange == null ||
_selectedGender == null) {
DialogService().showError('Silakan lengkapi semua data profil');
return;
}
}
// Page 1 (Preferensi) — lanjut selesai setup
if (_currentPage == 1) {
_handleComplete();
return;
}
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
void _previousPage() async {
if (_currentPage > 0) {
_pageController.previousPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
} else {
await FirebaseService().signOut();
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const LoginScreen()),
);
}
}
}
Future<void> _handleComplete() async {
setState(() => _isLoading = true);
try {
final firebaseService = FirebaseService();
final currentUser = firebaseService.currentUser;
if (currentUser == null) throw Exception('User not logged in');
final existingUser = await firebaseService.getUserData(currentUser.uid);
final nickname = _nicknameController.text.trim();
final address = _addressController.text.trim();
// (questionnaire answers no longer saved during onboarding)
if (existingUser != null) {
final updatedUser = existingUser.copyWith(
nickname: nickname.isNotEmpty ? nickname : null,
address: address.isNotEmpty ? address : null,
gender: _selectedGender,
isSetupComplete: true,
);
await firebaseService.updateUserData(updatedUser);
} else {
final newUser = await firebaseService.createUserDocument(currentUser);
final updatedNewUser = newUser.copyWith(
nickname: nickname.isNotEmpty ? nickname : null,
address: address.isNotEmpty ? address : null,
gender: _selectedGender,
isSetupComplete: true,
);
await firebaseService.updateUserData(updatedNewUser);
}
final currentPrefs =
existingUser?.preferences ??
UserPreferences(
allergies: [],
dietaryRestriction: 'None',
ageRange: null,
);
final updatedPreferences = currentPrefs.copyWith(
ageRange: _selectedAgeRange,
allergies: _selectedAllergies,
dietaryRestriction: _selectedDietaryRestriction,
);
await firebaseService.updateUserPreferences(
currentUser.uid,
updatedPreferences,
);
await firebaseService.setSetupComplete(currentUser.uid);
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const HomeScreen()),
);
}
} catch (e) {
if (mounted) DialogService().showError('Gagal menyimpan: $e');
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, _) {
if (!didPop) _previousPage();
},
child: Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
children: [
// Header
Align(
alignment: Alignment.topLeft,
child: Padding(
padding: const EdgeInsets.only(left: 8, top: 8),
child: IconButton(
icon: const Icon(
Icons.chevron_left,
color: Colors.black54,
size: 28,
),
onPressed: _previousPage,
),
),
),
// Title Section
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
children: [
const SizedBox(height: 8),
const Text(
'Kenalan Dulu Yuk!',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Poppins',
fontSize: 26,
fontWeight: FontWeight.bold,
color: primaryBrown,
height: 1.25,
),
),
const SizedBox(height: 10),
Text(
_getSubtitle(),
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'Poppins',
fontSize: 14,
fontWeight: FontWeight.w400,
color: labelGray,
),
),
const SizedBox(height: 8),
_buildProgressIndicator(),
],
),
),
const SizedBox(height: 24),
// PageView
Expanded(
child: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
onPageChanged: (index) {
setState(() => _currentPage = index);
},
children: [_buildDemographicsPage(), _buildPreferencesPage()],
),
),
],
),
),
),
);
}
String _getSubtitle() {
switch (_currentPage) {
case 0:
return 'Data diri dasar kamu';
case 1:
return 'Info alergi & pantangan';
default:
return '';
}
}
Widget _buildProgressIndicator() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(2, (index) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
width: index == _currentPage ? 24 : 8,
height: 8,
decoration: BoxDecoration(
color: index == _currentPage ? primaryBrown : borderGray,
borderRadius: BorderRadius.circular(4),
),
);
}),
);
}
// ============ PAGE 1: Profile & Demographics ============
Widget _buildDemographicsPage() {
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTextField(
controller: _nicknameController,
label: 'Nama Panggilan',
hint: 'Bagaimana kami harus memanggil Anda?',
),
const SizedBox(height: 24),
_buildTextField(
controller: _addressController,
label: 'Alamat Lengkap',
hint: 'Domisili Anda saat ini',
maxLines: 2,
minHeight: 80,
keyboardType: TextInputType.streetAddress,
),
const SizedBox(height: 24),
_buildSectionTitle('Rentang Usia', 'Pilih rentang usia Anda'),
const SizedBox(height: 12),
_buildChoiceChips(
options: _ageRanges,
selectedValue: _selectedAgeRange,
onSelected: (value) => setState(() => _selectedAgeRange = value),
),
const SizedBox(height: 24),
_buildSectionTitle('Jenis Kelamin', 'Pilih jenis kelamin Anda'),
const SizedBox(height: 12),
_buildChoiceChips(
options: _genders,
selectedValue: _selectedGender,
onSelected: (value) => setState(() => _selectedGender = value),
),
const SizedBox(height: 48),
_buildNavigationButton(),
const SizedBox(height: 32),
],
),
);
}
// ============ PAGE 3: Preferences ============
Widget _buildPreferencesPage() {
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle(
'Alergi & Pantangan',
'Pilih bahan yang harus dihindari',
),
const SizedBox(height: 12),
_buildFilterChips(),
const SizedBox(height: 28),
_buildSectionTitle(
'Pembatasan Diet',
'Pilih pola makan yang Anda ikuti',
),
const SizedBox(height: 12),
_buildChoiceChips(
options: _availableDietaryRestrictions,
selectedValue: _selectedDietaryRestriction,
onSelected: (value) =>
setState(() => _selectedDietaryRestriction = value),
),
const SizedBox(height: 48),
_buildNavigationButton(),
const SizedBox(height: 32),
],
),
);
}
// ============ REUSABLE WIDGETS ============
Widget _buildTextField({
required TextEditingController controller,
required String label,
String? hint,
double minHeight = 56,
int maxLines = 1,
TextInputType keyboardType = TextInputType.text,
List<TextInputFormatter>? inputFormatters,
}) {
return Builder(
builder: (context) {
return Container(
constraints: BoxConstraints(minHeight: minHeight),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: borderGray, width: 1),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: const TextStyle(
fontFamily: 'Poppins',
fontSize: 11,
fontWeight: FontWeight.w400,
color: labelGray,
),
),
const SizedBox(height: 4),
TextFormField(
controller: controller,
maxLines: maxLines,
keyboardType: keyboardType,
inputFormatters: inputFormatters,
onTap: () {
Future.delayed(const Duration(milliseconds: 300), () {
if (context.mounted) {
Scrollable.ensureVisible(
context,
alignment: 0.5,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
});
},
style: const TextStyle(
fontFamily: 'Poppins',
fontSize: 14,
fontWeight: FontWeight.w600,
color: primaryBrown,
),
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
hintText: hint,
hintStyle: TextStyle(
fontFamily: 'Poppins',
fontSize: 14,
fontWeight: FontWeight.w400,
color: labelGray.withOpacity(0.6),
),
),
validator: (v) => v!.isEmpty ? '$label is required' : null,
),
],
),
);
},
);
}
Widget _buildSectionTitle(String title, String subtitle) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontFamily: 'Poppins',
fontSize: 16,
fontWeight: FontWeight.w600,
color: primaryBrown,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: const TextStyle(
fontFamily: 'Poppins',
fontSize: 12,
fontWeight: FontWeight.w400,
color: labelGray,
),
),
],
);
}
Widget _buildChoiceChips({
required List<String> options,
required String? selectedValue,
required Function(String) onSelected,
}) {
return Wrap(
spacing: 10,
runSpacing: 10,
children: options.map((option) {
final isSelected = selectedValue == option;
return ChoiceChip(
label: Text(option),
selected: isSelected,
onSelected: (selected) {
if (selected) onSelected(option);
},
selectedColor: primaryBrown,
backgroundColor: Colors.grey[100],
labelStyle: TextStyle(
fontFamily: 'Poppins',
fontSize: 14,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
color: isSelected ? Colors.white : labelGray,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: isSelected ? primaryBrown : borderGray,
width: 1,
),
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
);
}).toList(),
);
}
Widget _buildFilterChips() {
return Wrap(
spacing: 8,
runSpacing: 8,
children: _availableAllergies.map((allergy) {
final isSelected = _selectedAllergies.contains(allergy);
return FilterChip(
label: Text(allergy),
selected: isSelected,
onSelected: (selected) {
setState(() {
if (selected) {
_selectedAllergies.add(allergy);
} else {
_selectedAllergies.remove(allergy);
}
});
},
selectedColor: const Color(0xFFFFEBEE),
checkmarkColor: const Color(0xFFD32F2F),
backgroundColor: Colors.grey[100],
labelStyle: TextStyle(
fontFamily: 'Poppins',
fontSize: 13,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
color: isSelected ? const Color(0xFFD32F2F) : primaryBrown,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: isSelected ? const Color(0xFFD32F2F) : borderGray,
width: 1,
),
),
);
}).toList(),
);
}
Widget _buildNavigationButton() {
return Align(
alignment: Alignment.centerRight,
child: _buildCircularButton(
onPressed: _isLoading ? null : _nextPage,
isLoading: _isLoading,
isComplete: _currentPage == 1, // Page 1 = preferensi = halaman terakhir
),
);
}
Widget _buildCircularButton({
VoidCallback? onPressed,
bool isLoading = false,
bool isComplete = false,
}) {
return Stack(
clipBehavior: Clip.none,
children: [
Positioned(
top: -4,
left: 4,
right: -4,
bottom: 4,
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: const Color(0xFF5A4A3A).withOpacity(0.3),
shape: BoxShape.circle,
),
),
),
Material(
color: const Color(0xFF5A4A3A),
shape: const CircleBorder(),
child: InkWell(
onTap: onPressed,
customBorder: const CircleBorder(),
child: Container(
width: 64,
height: 64,
decoration: const BoxDecoration(shape: BoxShape.circle),
child: isLoading
? const Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3,
),
)
: Icon(
isComplete ? Icons.check : Icons.arrow_forward,
color: Colors.white,
size: 32,
),
),
),
),
],
);
}
}