import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../core/permissions.dart'; import '../core/theme.dart'; import '../state/profile_controller.dart'; import '../state/seed.dart'; import '../state/session_controller.dart'; import '../state/shifts_controller.dart'; import '../state/timecards_controller.dart'; import '../widgets/common.dart'; class SettingsScreen extends ConsumerStatefulWidget { const SettingsScreen({super.key}); @override ConsumerState createState() => _SettingsScreenState(); } class _SettingsScreenState extends ConsumerState { @override void initState() { super.initState(); // A permission can be revoked in system settings while the app is closed, // so the switches are re-read from the OS rather than trusted from cache. WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(sessionProvider.notifier).refresh(); }); } /// The OS has no API to revoke a permission, and re-requesting a granted one /// does nothing — so the switch hands off to the system settings page for /// anything it cannot do itself. Future _toggle(AppPermission permission, bool value) async { final notifier = ref.read(sessionProvider.notifier); if (!value) { await ref.read(permissionServiceProvider).openSettings(); return; } final granted = await notifier.request(permission); if (granted || !mounted) return; await ref.read(permissionServiceProvider).openSettings(); } @override Widget build(BuildContext context) { final session = ref.watch(sessionProvider); final profile = ref.watch(profileProvider); final reviewCount = ref.watch(timecardsNeedingReviewProvider).length; return Scaffold( appBar: AppBar( title: const Text('Settings'), actions: const [ Padding( padding: EdgeInsets.only(right: 16), child: Center(child: StatusChip('v1.1.1', tone: ChipTone.yellow)), ), ], ), body: ListView( padding: const EdgeInsets.fromLTRB(20, 8, 20, 28), children: [ const SectionLabel('Permissions'), KrowCard( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ _SwitchRow( icon: LucideIcons.mapPin, title: 'Location', subtitle: 'Needed to clock in at the venue', value: session.locationGranted, onChanged: (v) => _toggle(AppPermission.location, v), ), const Divider(height: 1), _SwitchRow( icon: LucideIcons.bell, title: 'Notifications', subtitle: 'Shift offers, reminders and pay alerts', value: session.notificationsGranted, onChanged: (v) => _toggle(AppPermission.notifications, v), ), const Divider(height: 1), _SwitchRow( icon: LucideIcons.camera, title: 'Camera', subtitle: 'Photograph documents and attire', value: session.cameraGranted, onChanged: (v) => _toggle(AppPermission.camera, v), ), ], ), ), const SectionLabel('Money'), TileGroup( children: [ NavTile( icon: LucideIcons.landmark, title: 'Direct deposit', subtitle: profile.bank == null ? 'Where your pay lands' : profile.bank!.bankName, badge: profile.bank == null ? const StatusChip('Add', tone: ChipTone.yellow) : null, onTap: () => context.push('/direct-deposit'), ), NavTile( icon: LucideIcons.clock, title: 'Timecards', subtitle: 'Hours worked and disputes', badge: reviewCount > 0 ? StatusChip('$reviewCount', tone: ChipTone.yellow) : null, onTap: () => context.push('/timecards'), ), NavTile( icon: LucideIcons.calendarDays, title: 'Payroll schedule', subtitle: 'When each period closes and pays', onTap: () => context.push('/payroll'), ), ], ), const SectionLabel('Support'), TileGroup( children: [ NavTile( icon: LucideIcons.lifeBuoy, title: 'Contact support', subtitle: 'Usually answered within 4 hours', onTap: () => context.push('/support'), ), NavTile( icon: LucideIcons.circleHelp, title: 'FAQs', subtitle: 'Common questions answered', onTap: () => context.push('/faqs'), ), NavTile( icon: LucideIcons.shield, title: 'Privacy & security', subtitle: 'Account and data settings', onTap: () => context.push('/privacy'), ), ], ), const SizedBox(height: 22), OutlinedButton.icon( onPressed: () => _signOut(context, ref), icon: const Icon(LucideIcons.logOut, size: 18), label: const Text('Sign out'), ), const SizedBox(height: 22), const Center( child: Text( 'VERSION 1.1.1 · BUILD 26', style: TextStyle( fontSize: 11, letterSpacing: 1.2, color: Color(0xFF949FA5), ), ), ), ], ), ); } Future _signOut(BuildContext context, WidgetRef ref) async { final ok = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Sign out?'), content: const Text('You will need to verify your number again.'), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Stay'), ), FilledButton( onPressed: () => Navigator.pop(context, true), style: FilledButton.styleFrom(minimumSize: const Size(0, 44)), child: const Text('Sign out'), ), ], ), ); if (ok != true || !context.mounted) return; await ref.read(sessionProvider.notifier).signOut(); if (!context.mounted) return; context.go('/welcome'); } } class _SwitchRow extends StatelessWidget { const _SwitchRow({ required this.icon, required this.title, required this.subtitle, required this.value, required this.onChanged, }); final IconData icon; final String title; final String subtitle; final bool value; final ValueChanged onChanged; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: KrowPalette.blueTint, borderRadius: BorderRadius.circular(11), ), child: Icon(icon, size: 18, color: KrowPalette.blue), ), const SizedBox(width: 13), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontSize: 14.5, fontWeight: FontWeight.w600, ), ), Text( subtitle, style: const TextStyle(fontSize: 12, color: KrowPalette.mute), ), ], ), ), Switch(value: value, onChanged: onChanged), ], ), ); } } /// Pick what it is about so it reaches the right person first time. class SupportScreen extends ConsumerStatefulWidget { const SupportScreen({super.key}); @override ConsumerState createState() => _SupportScreenState(); } class _SupportScreenState extends ConsumerState { String _topic = Seed.supportTopics.first; String? _shiftId; final _message = TextEditingController(); @override void dispose() { _message.dispose(); super.dispose(); } void _send() { if (_message.text.trim().length < 10) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Tell us a little more than that')), ); return; } ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Sent to support · $_topic'))); Navigator.of(context).pop(); } @override Widget build(BuildContext context) { final shifts = ref.watch(shiftsProvider); return Scaffold( appBar: AppBar( title: const Text('Get help'), actions: const [ Padding( padding: EdgeInsets.only(right: 16), child: Center(child: StatusChip('~4 hr reply')), ), ], ), body: ListView( padding: const EdgeInsets.fromLTRB(20, 8, 20, 28), children: [ const Text( 'Pick what it is about so it reaches the right person first time.', style: TextStyle( fontSize: 13.5, height: 1.55, color: KrowPalette.mute, ), ), const SectionLabel("What's it about?"), Wrap( spacing: 7, runSpacing: 7, children: [ for (final t in Seed.supportTopics) ChoiceChip( label: Text(t), selected: t == _topic, showCheckmark: false, selectedColor: KrowPalette.blue, labelStyle: TextStyle( fontSize: 12.5, fontWeight: FontWeight.w500, color: t == _topic ? Colors.white : KrowPalette.mute, ), onSelected: (_) => setState(() => _topic = t), ), ], ), const SectionLabel('Which shift?'), DropdownButtonFormField( initialValue: _shiftId, isExpanded: true, decoration: const InputDecoration( hintText: 'Not about a specific shift', ), items: [ for (final s in shifts) DropdownMenuItem( value: s.id, child: Text( '${s.role} · ${s.business}', overflow: TextOverflow.ellipsis, ), ), ], onChanged: (v) => setState(() => _shiftId = v), ), const SectionLabel('Tell us what happened'), TextField( controller: _message, maxLines: 5, maxLength: 500, decoration: const InputDecoration( hintText: 'The venue door was locked when I arrived...', ), ), TextButton.icon( onPressed: () => ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Attachments open the camera roll')), ), icon: const Icon(LucideIcons.paperclip, size: 16), label: const Text('Attach a photo'), ), const SizedBox(height: 16), FilledButton(onPressed: _send, child: const Text('Send to support')), ], ), ); } } class FaqScreen extends StatefulWidget { const FaqScreen({super.key}); @override State createState() => _FaqScreenState(); } class _FaqScreenState extends State { String _query = ''; @override Widget build(BuildContext context) { final q = _query.toLowerCase(); return Scaffold( appBar: AppBar(title: const Text('Questions')), body: ListView( padding: const EdgeInsets.fromLTRB(20, 8, 20, 28), children: [ TextField( onChanged: (v) => setState(() => _query = v), decoration: const InputDecoration( hintText: 'Search help', prefixIcon: Icon(LucideIcons.search, size: 18), ), ), for (final entry in Seed.faqs.entries) () { final matches = entry.value .where( (f) => q.isEmpty || f.$1.toLowerCase().contains(q) || f.$2.toLowerCase().contains(q), ) .toList(); if (matches.isEmpty) return const SizedBox.shrink(); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SectionLabel(entry.key), KrowCard( padding: const EdgeInsets.symmetric(horizontal: 4), child: Column( children: [ for (var i = 0; i < matches.length; i++) ...[ if (i > 0) const Divider(height: 1), ExpansionTile( shape: const Border(), collapsedShape: const Border(), tilePadding: const EdgeInsets.symmetric( horizontal: 12, ), childrenPadding: const EdgeInsets.fromLTRB( 12, 0, 12, 14, ), title: Text( matches[i].$1, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), ), children: [ Align( alignment: Alignment.centerLeft, child: Text( matches[i].$2, style: const TextStyle( fontSize: 13, height: 1.55, color: KrowPalette.mute, ), ), ), ], ), ], ], ), ), ], ); }(), ], ), ); } } class PrivacyScreen extends ConsumerWidget { const PrivacyScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final profile = ref.watch(profileProvider); return Scaffold( appBar: AppBar(title: const Text('Privacy & security')), body: ListView( padding: const EdgeInsets.fromLTRB(20, 8, 20, 28), children: [ const SectionLabel('Legal'), TileGroup( children: [ NavTile( icon: LucideIcons.fileText, title: 'Terms of service', subtitle: 'Updated 14 Jun 2026', onTap: () => _open(context, 'Terms of service'), ), NavTile( icon: LucideIcons.shield, title: 'Privacy policy', subtitle: 'What we collect and why', onTap: () => _open(context, 'Privacy policy'), ), ], ), const SectionLabel('Who sees your data'), TileGroup( children: [ NavTile( icon: LucideIcons.briefcase, title: "Businesses you've worked for", subtitle: '2 have access to your documents', onTap: () => _open(context, 'Data sharing'), ), ], ), const SectionLabel('Email'), KrowCard( padding: const EdgeInsets.fromLTRB(16, 8, 8, 8), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: KrowPalette.blueTint, borderRadius: BorderRadius.circular(11), ), child: const Icon( LucideIcons.mail, size: 18, color: KrowPalette.blue, ), ), const SizedBox(width: 13), const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'KROW newsletter', style: TextStyle( fontSize: 14.5, fontWeight: FontWeight.w600, ), ), Text( 'Product news and operational tips', style: TextStyle(fontSize: 12, color: KrowPalette.mute), ), ], ), ), Switch( value: profile.newsletter, onChanged: (v) => ref.read(profileProvider.notifier).setNewsletter(v), ), ], ), ), const SectionLabel('Account'), KrowCard( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( 'Delete your account', style: TextStyle(fontSize: 14.5, fontWeight: FontWeight.w600), ), const SizedBox(height: 3), const Text( 'Permanent. Timecards and tax records are kept for 4 years ' 'by law, then destroyed.', style: TextStyle( fontSize: 12, height: 1.45, color: KrowPalette.mute, ), ), const SizedBox(height: 14), OutlinedButton.icon( onPressed: () => _confirmDelete(context), icon: const Icon(LucideIcons.trash2, size: 18), label: const Text('Delete account'), ), ], ), ), ], ), ); } void _open(BuildContext context, String title) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('$title opens in the browser'))); } Future _confirmDelete(BuildContext context) async { await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Delete your account?'), content: const Text( 'This cannot be undone. To go ahead, contact support so we can ' 'confirm it is really you.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Keep my account'), ), FilledButton( onPressed: () { Navigator.pop(context); context.push('/support'); }, style: FilledButton.styleFrom(minimumSize: const Size(0, 44)), child: const Text('Contact support'), ), ], ), ); } }