Initial commit: Krow Worker App
This commit is contained in:
362
lib/screens/profile_screen.dart
Normal file
362
lib/screens/profile_screen.dart
Normal file
@@ -0,0 +1,362 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user