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

338 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../core/formatters.dart';
import '../core/theme.dart';
import '../models/availability.dart';
import '../state/availability_controller.dart';
import '../widgets/common.dart';
import '../widgets/week_strip.dart';
/// Auto-Match only offers shifts inside the blocks switched on here.
class AvailabilityScreen extends ConsumerStatefulWidget {
const AvailabilityScreen({super.key});
@override
ConsumerState<AvailabilityScreen> createState() => _AvailabilityScreenState();
}
class _AvailabilityScreenState extends ConsumerState<AvailabilityScreen> {
DateTime _selected = DateTime.now();
List<DateTime> get _week {
final now = DateTime.now();
return [for (var i = 0; i < 7; i++) now.add(Duration(days: i))];
}
@override
Widget build(BuildContext context) {
final controller = ref.watch(availabilityProvider.notifier);
ref.watch(availabilityProvider);
final blocks = controller.blocksFor(_selected);
final hours = blocks.fold<int>(0, (sum, b) => sum + b.hours);
final marked = <int>{
for (var i = 0; i < _week.length; i++)
if (controller.blocksFor(_week[i]).isNotEmpty) i,
};
return Scaffold(
body: ListView(
padding: EdgeInsets.zero,
children: [
BrandHeader(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
HeaderIconButton(
icon: LucideIcons.arrowLeft,
onPressed: () => Navigator.of(context).pop(),
),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Availability',
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.w700,
letterSpacing: -0.9,
color: Colors.white,
),
),
),
],
),
const SizedBox(height: 18),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_Preset(
label: 'All week',
onTap: () =>
controller.applyPreset(_week, test: (_) => true),
),
_Preset(
label: 'Weekdays',
onTap: () => controller.applyPreset(
_week,
test: (d) => d.weekday <= DateTime.friday,
),
),
_Preset(
label: 'Weekends',
onTap: () => controller.applyPreset(
_week,
test: (d) => d.weekday >= DateTime.saturday,
),
),
_Preset(
label: 'Clear',
accent: true,
onTap: () => controller.clearWeek(_week),
),
],
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(20, 18, 20, 28),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
WeekStrip(
days: _week,
selected: _selected,
onSelect: (d) => setState(() => _selected = d),
marked: marked,
),
const SizedBox(height: 16),
KrowCard(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 16),
child: Column(
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
Fmt.dayShort(_selected),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
letterSpacing: -0.5,
),
),
const SizedBox(height: 2),
Text(
blocks.isEmpty
? 'Marked as a day off'
: 'Available · $hours hrs',
style: TextStyle(
fontSize: 12.5,
fontWeight: FontWeight.w500,
color: blocks.isEmpty
? KrowPalette.mute
: KrowPalette.blue,
),
),
],
),
),
Switch(
value: blocks.isNotEmpty,
onChanged: (v) =>
controller.setDayAvailable(_selected, v),
),
],
),
const SizedBox(height: 14),
for (final block in DayBlock.values)
Padding(
padding: const EdgeInsets.only(bottom: 9),
child: _BlockRow(
block: block,
on: blocks.contains(block),
onTap: () =>
controller.toggleBlock(_selected, block),
),
),
],
),
),
const SizedBox(height: 14),
InfoBanner(
blocks.isEmpty
? 'With every block off, Auto-Match will not send you '
'anything on this day.'
: 'Saved. Auto-Match will only offer you shifts inside '
'these blocks.',
icon: blocks.isEmpty ? null : LucideIcons.check,
),
],
),
),
],
),
);
}
}
class _Preset extends StatelessWidget {
const _Preset({
required this.label,
required this.onTap,
this.accent = false,
});
final String label;
final VoidCallback onTap;
final bool accent;
@override
Widget build(BuildContext context) {
// Built by hand rather than as a Chip: the app's ChipTheme paints a white
// fill, which would hide white-on-blue header text.
return Material(
color: Colors.white.withValues(alpha: accent ? 0.06 : 0.16),
borderRadius: BorderRadius.circular(9),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(9),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 9),
child: Text(
label,
style: TextStyle(
fontSize: 12.5,
fontWeight: FontWeight.w600,
color: accent ? KrowPalette.yellow : Colors.white,
),
),
),
),
);
}
}
class _BlockRow extends StatelessWidget {
const _BlockRow({required this.block, required this.on, required this.onTap});
final DayBlock block;
final bool on;
final VoidCallback onTap;
IconData get _icon => switch (block) {
DayBlock.morning => LucideIcons.sunrise,
DayBlock.afternoon => LucideIcons.sun,
DayBlock.evening => LucideIcons.moon,
};
@override
Widget build(BuildContext context) {
const duration = Duration(milliseconds: 260);
const curve = Curves.easeOutCubic;
return GestureDetector(
onTap: onTap,
// Every part of the row animates together, so switching a block on is a
// single motion rather than four things changing at once.
child: AnimatedContainer(
duration: duration,
curve: curve,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: on ? KrowPalette.blueTint : const Color(0xFFE5F0E8),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: on
? KrowPalette.blue.withValues(alpha: 0.3)
: Colors.transparent,
),
),
child: Row(
children: [
AnimatedContainer(
duration: duration,
curve: curve,
width: 38,
height: 38,
decoration: BoxDecoration(
color: on ? KrowPalette.blue : Colors.white,
borderRadius: BorderRadius.circular(11),
border: Border.all(
color: on ? KrowPalette.blue : KrowPalette.line,
),
),
child: Icon(
_icon,
size: 18,
color: on ? Colors.white : const Color(0xFF949FA5),
),
),
const SizedBox(width: 13),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnimatedDefaultTextStyle(
duration: duration,
curve: curve,
style: TextStyle(
fontSize: 14.5,
fontWeight: FontWeight.w600,
color: on ? KrowPalette.ink : KrowPalette.mute,
),
child: Text(block.label),
),
AnimatedDefaultTextStyle(
duration: duration,
curve: curve,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: on ? KrowPalette.blue : const Color(0xFF949FA5),
),
child: Text(block.window),
),
],
),
),
AnimatedContainer(
duration: duration,
curve: curve,
width: 24,
height: 24,
decoration: BoxDecoration(
color: on ? KrowPalette.blue : Colors.transparent,
borderRadius: BorderRadius.circular(7),
border: Border.all(
color: on ? KrowPalette.blue : const Color(0xFFC4D4C9),
width: 1.5,
),
),
child: AnimatedScale(
scale: on ? 1 : 0,
duration: duration,
curve: Curves.easeOutBack,
child: const Icon(
LucideIcons.check,
size: 14,
color: Colors.white,
),
),
),
],
),
),
);
}
}