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