Files
krow-worker-app/lib/screens/today_screen.dart

432 lines
15 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/formatters.dart';
import '../core/theme.dart';
import '../models/shift.dart';
import '../state/availability_controller.dart';
import '../state/compliance_controller.dart';
import '../state/profile_controller.dart';
import '../state/shifts_controller.dart';
import '../widgets/collapsing_header.dart';
import '../widgets/common.dart';
import '../widgets/shift_card.dart';
import '../widgets/krow_wordmark.dart';
class TodayScreen extends ConsumerWidget {
const TodayScreen({super.key});
/// Brand line (40) + gap + identity row (44) + gap + stats, plus 6/18
/// padding.
static const _expandedHeader = 188.0;
String _greeting() {
final h = DateTime.now().hour;
if (h < 12) return 'Good morning';
if (h < 18) return 'Good afternoon';
return 'Good evening';
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final worker = ref.watch(workerStatsProvider);
final next = ref.watch(nextShiftProvider);
final open = ref.watch(openShiftsProvider);
final scheduled = ref.watch(scheduledEarningsProvider);
final hours = ref.watch(hoursLoggedProvider);
final daysSet = ref.watch(daysSetProvider);
final compliant = ref.watch(isCompliantProvider);
final (done, total) = ref.watch(complianceProgressProvider);
return Scaffold(
body: CustomScrollView(
slivers: [
KrowSliverHeader(
expandedHeight: _expandedHeader,
collapsedHeight: 60,
builder: (context, t) => HeaderCrossFade(
t: t,
expandedHeight: _expandedHeader,
expanded: Padding(
padding: const EdgeInsets.fromLTRB(20, 6, 20, 18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
HeaderBrandLine(
trailing: HeaderIconButton(
icon: LucideIcons.messageCircle,
tooltip: 'Support',
onPressed: () => context.push('/support'),
),
),
const SizedBox(height: 14),
Row(
children: [
_Avatar(worker.initials),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_greeting().toUpperCase(),
style: TextStyle(
fontSize: 9.5,
letterSpacing: 1.6,
fontWeight: FontWeight.w600,
color: Colors.white.withValues(alpha: 0.5),
),
),
const SizedBox(height: 2),
Text(
worker.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
letterSpacing: -0.7,
color: Colors.white,
),
),
],
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
// Flexible, not free-sized: on a 320px screen the two
// captions plus the rule are wider than the header.
Flexible(
child: _HeaderStat(
Fmt.money(scheduled),
'This period',
),
),
Container(
width: 1,
height: 34,
margin: const EdgeInsets.symmetric(horizontal: 18),
color: Colors.white24,
),
Flexible(
child: _HeaderStat(
Fmt.duration(hours),
'Hours logged',
),
),
],
),
],
),
),
// Once collapsed, keep who you are and the figure that matters,
// plus the one control that was in the open state.
collapsed: Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: Row(
children: [
_Avatar(worker.initials, size: 32),
const SizedBox(width: 10),
Expanded(
child: Text(
worker.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
letterSpacing: -0.4,
color: Colors.white,
),
),
),
Text(
Fmt.money(scheduled),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
letterSpacing: -0.5,
color: KrowPalette.yellow,
),
),
const SizedBox(width: 10),
HeaderIconButton(
icon: LucideIcons.messageCircle,
tooltip: 'Support',
onPressed: () => context.push('/support'),
),
],
),
),
),
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 24),
sliver: SliverList.list(
children: [
if (!compliant) ...[
const SizedBox(height: 16),
KrowCard(
onTap: () => context.push('/compliance'),
padding: const EdgeInsets.all(14),
child: Row(
children: [
const Icon(
LucideIcons.triangleAlert,
size: 20,
color: KrowPalette.ink,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Finish your compliance',
style: TextStyle(
fontSize: 14.5,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
'$done of $total items cleared',
style: const TextStyle(
fontSize: 12,
color: KrowPalette.mute,
),
),
],
),
),
const StatusChip('Action', tone: ChipTone.yellow),
],
),
),
],
const SectionLabel('Up next'),
if (next == null)
EmptyState(
icon: LucideIcons.ticket,
title: 'Nothing booked',
message:
'Open shifts near you start at '
'${open.isEmpty ? '-' : Fmt.money(open.last.rate)}/hr.',
action: FilledButton(
onPressed: () => context.go('/shifts'),
child: const Text('Browse open shifts'),
),
)
else
ShiftCard(
shift: next,
showDate: !Fmt.sameDay(next.start, DateTime.now()),
onTap: () => context.push('/shift/${next.id}'),
),
if (next != null && next.status == ShiftStatus.active) ...[
const SizedBox(height: 10),
FilledButton.icon(
onPressed: () => context.go('/clock'),
icon: const Icon(LucideIcons.squareCheck, size: 18),
label: const Text('You are on shift - go to clock'),
),
],
const SizedBox(height: 14),
Row(
children: [
Expanded(
child: _QuickTile(
icon: LucideIcons.search,
label: 'Find shifts',
caption: '${open.length} OPEN',
onTap: () => context.go('/shifts'),
),
),
const SizedBox(width: 10),
Expanded(
child: _QuickTile(
icon: LucideIcons.calendar,
label: 'Availability',
caption: '$daysSet DAYS SET',
onTap: () => context.push('/availability'),
),
),
const SizedBox(width: 10),
Expanded(
child: _QuickTile(
icon: LucideIcons.banknote,
label: 'Earnings',
caption: 'PENDING',
onTap: () => context.go('/pay'),
),
),
],
),
SectionLabel(
'Matched to you',
trailing: TextButton(
onPressed: () => context.go('/shifts'),
child: const Text('See all'),
),
),
if (open.isEmpty)
const InfoBanner(
'No open shifts right now. Turn on more availability blocks '
'and we will match you to more work.',
)
else
for (final shift in open.take(3))
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: ShiftCard(
shift: shift,
showDate: true,
onTap: () => context.push('/shift/${shift.id}'),
),
),
],
),
),
],
),
);
}
}
/// The worker's initials, used at two sizes as the header collapses.
class _Avatar extends StatelessWidget {
const _Avatar(this.initials, {this.size = 44});
final String initials;
final double size;
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
alignment: Alignment.center,
decoration: BoxDecoration(
color: KrowPalette.yellow,
borderRadius: BorderRadius.circular(size / 3.4),
),
child: Text(
initials,
style: TextStyle(
fontSize: size * 0.39,
fontWeight: FontWeight.w700,
color: KrowPalette.ink,
),
),
);
}
}
class _HeaderStat extends StatelessWidget {
const _HeaderStat(this.value, this.caption);
final String value;
final String caption;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Both lines hold to one line and shrink to fit: a wrapped caption
// would push the header past its pinned height.
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
value,
maxLines: 1,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
letterSpacing: -0.8,
color: Colors.white,
),
),
),
const SizedBox(height: 3),
FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
caption.toUpperCase(),
maxLines: 1,
style: TextStyle(
fontSize: 9.5,
letterSpacing: 1.4,
fontWeight: FontWeight.w600,
color: Colors.white.withValues(alpha: 0.5),
),
),
),
],
);
}
}
class _QuickTile extends StatelessWidget {
const _QuickTile({
required this.icon,
required this.label,
required this.caption,
required this.onTap,
});
final IconData icon;
final String label;
final String caption;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return KrowCard(
onTap: onTap,
padding: const EdgeInsets.fromLTRB(12, 14, 12, 14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: KrowPalette.blueTint,
borderRadius: BorderRadius.circular(11),
),
child: Icon(icon, size: 17, color: KrowPalette.blue),
),
const SizedBox(height: 22),
Text(
label,
style: const TextStyle(
fontSize: 13.5,
fontWeight: FontWeight.w600,
letterSpacing: -0.2,
),
),
const SizedBox(height: 2),
Text(
caption,
style: const TextStyle(
fontSize: 9.5,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
color: Color(0xFF949FA5),
),
),
],
),
);
}
}