Files
krow-worker-app/lib/screens/settings_screens.dart
2026-09-17 22:26:11 +05:30

590 lines
19 KiB
Dart

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/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 ConsumerWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final session = ref.watch(sessionProvider);
final notifier = ref.read(sessionProvider.notifier);
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) => v ? notifier.grantLocation() : null,
),
const Divider(height: 1),
_SwitchRow(
icon: LucideIcons.bell,
title: 'Notifications',
subtitle: 'Shift offers, reminders and pay alerts',
value: session.notificationsGranted,
onChanged: (v) => v ? notifier.grantNotifications() : null,
),
const Divider(height: 1),
_SwitchRow(
icon: LucideIcons.camera,
title: 'Camera',
subtitle: 'Photograph documents and attire',
value: session.cameraGranted,
onChanged: (v) => v ? notifier.grantCamera() : null,
),
],
),
),
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<void> _signOut(BuildContext context, WidgetRef ref) async {
final ok = await showDialog<bool>(
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<bool> 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<SupportScreen> createState() => _SupportScreenState();
}
class _SupportScreenState extends ConsumerState<SupportScreen> {
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<String>(
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<FaqScreen> createState() => _FaqScreenState();
}
class _FaqScreenState extends State<FaqScreen> {
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<void> _confirmDelete(BuildContext context) async {
await showDialog<void>(
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'),
),
],
),
);
}
}