338 lines
11 KiB
Dart
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,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|