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

363 lines
14 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/compliance_controller.dart';
import '../state/profile_controller.dart';
import '../widgets/collapsing_header.dart';
import '../widgets/common.dart';
class ProfileScreen extends ConsumerWidget {
const ProfileScreen({super.key});
/// Avatar (84) + gap + name + role, plus 4/16 padding.
static const _expandedHeader = 192.0;
@override
Widget build(BuildContext context, WidgetRef ref) {
final worker = ref.watch(workerStatsProvider);
final profile = ref.watch(profileProvider);
final (done, total) = ref.watch(complianceProgressProvider);
return Scaffold(
body: CustomScrollView(
slivers: [
KrowSliverHeader(
// Must clear the open content: avatar, name and role, plus the
// padding around them. The settings button overlays rather than
// stacking above, which is what keeps this under 200.
expandedHeight: _expandedHeader,
collapsedHeight: 60,
builder: (context, t) => HeaderCrossFade(
t: t,
expandedHeight: _expandedHeader,
collapsed: Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: Row(
children: [
Expanded(
child: Text(
worker.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
letterSpacing: -0.5,
color: Colors.white,
),
),
),
HeaderIconButton(
icon: LucideIcons.settings,
tooltip: 'Settings',
onPressed: () => context.push('/settings'),
),
],
),
),
expanded: Padding(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 16),
child: Stack(
// Without this the unpositioned column would shrink-wrap to
// the Stack's top-left corner instead of centring.
alignment: Alignment.topCenter,
children: [
// Overlaid rather than stacked above the avatar: as its own
// row it added 46px the header could not afford.
Positioned(
top: 0,
right: 0,
child: HeaderIconButton(
icon: LucideIcons.settings,
tooltip: 'Settings',
onPressed: () => context.push('/settings'),
),
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap: () => _editName(context, ref, worker.name),
child: Stack(
children: [
Container(
width: 84,
height: 84,
alignment: Alignment.center,
decoration: const BoxDecoration(
color: KrowPalette.yellow,
shape: BoxShape.circle,
),
child: Text(
worker.initials,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.w700,
color: KrowPalette.ink,
),
),
),
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
border: Border.all(
color: KrowPalette.blue,
width: 2.5,
),
),
child: const Icon(
LucideIcons.pencil,
size: 12,
color: KrowPalette.blue,
),
),
),
],
),
),
const SizedBox(height: 10),
Text(
worker.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 23,
fontWeight: FontWeight.w700,
letterSpacing: -0.8,
color: Colors.white,
),
),
const SizedBox(height: 2),
Text(
'${worker.role} · ${worker.city}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12.5,
letterSpacing: 0.4,
color: Colors.white.withValues(alpha: 0.6),
),
),
],
),
],
),
),
),
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(20, 14, 20, 0),
sliver: SliverList.list(
children: [
StatRail(
stats: [
('Shifts', '${worker.shiftsWorked}'),
('Rating', worker.rating.toStringAsFixed(1)),
('On time', '${worker.onTimePercent}%'),
('No show', '${worker.noShows}'),
('Cancel', '${worker.cancellations}'),
],
),
const SizedBox(height: 11),
KrowCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Trust index',
style: TextStyle(
fontSize: 14.5,
fontWeight: FontWeight.w600,
),
),
Text(
'${worker.trustIndex}%',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
letterSpacing: -0.6,
),
),
],
),
const SizedBox(height: 11),
KrowProgressBar(
worker.trustIndex / 100,
color: worker.trustIndex < 45
? const Color(0xFFA6403F)
: KrowPalette.blue,
),
const SizedBox(height: 9),
const Text(
'Stay above 45% to keep picking up shifts. On-time '
'arrivals raise it fastest.',
style: TextStyle(
fontSize: 12,
height: 1.45,
color: KrowPalette.mute,
),
),
],
),
),
const SectionLabel('Compliance'),
TileGroup(
children: [
NavTile(
icon: LucideIcons.shieldCheck,
title: 'Compliance hub',
subtitle: 'Everything a business must see',
badge: StatusChip(
'$done of $total',
tone: done == total
? ChipTone.neutral
: ChipTone.yellow,
),
onTap: () => context.push('/compliance'),
),
],
),
const SectionLabel('Your details'),
TileGroup(
children: [
NavTile(
icon: LucideIcons.user,
title: 'Personal info',
subtitle: worker.phone,
onTap: () => _editName(context, ref, worker.name),
),
NavTile(
icon: LucideIcons.phone,
title: 'Emergency contact',
subtitle: 'Who we call if something happens',
badge: StatusChip(
'${profile.contacts.length}',
tone: ChipTone.neutral,
),
onTap: () => context.push('/emergency-contacts'),
),
NavTile(
icon: LucideIcons.briefcase,
title: 'Experience & skills',
subtitle: 'Work history and trades',
badge: StatusChip(
'${profile.skills.length}',
tone: ChipTone.neutral,
),
onTap: () => context.push('/skills'),
),
NavTile(
icon: LucideIcons.calendar,
title: 'Availability',
subtitle: 'The hours you can work',
onTap: () => context.push('/availability'),
),
NavTile(
icon: LucideIcons.gift,
title: 'Benefits',
subtitle: 'Policies and balance history',
onTap: () => context.push('/benefits'),
),
],
),
const SectionLabel('Money'),
TileGroup(
children: [
NavTile(
icon: LucideIcons.landmark,
title: 'Direct deposit',
subtitle: profile.bank == null
? 'Not set up yet'
: '${profile.bank!.bankName} '
'····${profile.bank!.accountLast4}',
badge: profile.bank == null
? const StatusChip('Add', tone: ChipTone.yellow)
: null,
onTap: () => context.push('/direct-deposit'),
),
NavTile(
icon: LucideIcons.fileText,
title: 'Timecards',
subtitle: 'Hours worked and disputes',
onTap: () => context.push('/timecards'),
),
],
),
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: 28),
],
),
),
],
),
);
}
Future<void> _editName(
BuildContext context,
WidgetRef ref,
String current,
) async {
final controller = TextEditingController(text: current);
final name = await showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Your name'),
content: TextField(
controller: controller,
autofocus: true,
textCapitalization: TextCapitalization.words,
decoration: const InputDecoration(hintText: 'Full name'),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(context, controller.text.trim()),
style: FilledButton.styleFrom(minimumSize: const Size(0, 44)),
child: const Text('Save'),
),
],
),
);
if (name == null || name.isEmpty) return;
ref.read(profileProvider.notifier).updateName(name);
}
}