refactor: remove old availability page and update module imports

- Deleted the old availability_page_new.dart file.
- Updated the staff_availability_module.dart to import the new availability_page.dart.
- Added firebase_auth dependency in pubspec.yaml for authentication features.
This commit is contained in:
Achintha Isuru
2026-01-30 16:08:05 -05:00
parent 0b763bae44
commit aa39b0fd06
6 changed files with 269 additions and 1091 deletions

View File

@@ -1,8 +1,15 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart' hide ModularWatchExtension;
import 'package:intl/intl.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../blocs/availability_bloc.dart';
import '../blocs/availability_event.dart';
import '../blocs/availability_state.dart';
import 'package:krow_domain/krow_domain.dart';
class AvailabilityPage extends StatefulWidget {
const AvailabilityPage({super.key});
@@ -11,216 +18,73 @@ class AvailabilityPage extends StatefulWidget {
}
class _AvailabilityPageState extends State<AvailabilityPage> {
late DateTime _currentWeekStart;
late DateTime _selectedDate;
// Mock Availability State
// Map of day name (lowercase) to availability status
Map<String, bool> _availability = {
'monday': true,
'tuesday': true,
'wednesday': true,
'thursday': true,
'friday': true,
'saturday': false,
'sunday': false,
};
// Map of day name to time slot map
Map<String, Map<String, bool>> _timeSlotAvailability = {
'monday': {'morning': true, 'afternoon': true, 'evening': true},
'tuesday': {'morning': true, 'afternoon': true, 'evening': true},
'wednesday': {'morning': true, 'afternoon': true, 'evening': true},
'thursday': {'morning': true, 'afternoon': true, 'evening': true},
'friday': {'morning': true, 'afternoon': true, 'evening': true},
'saturday': {'morning': false, 'afternoon': false, 'evening': false},
'sunday': {'morning': false, 'afternoon': false, 'evening': false},
};
final List<String> _dayNames = [
'sunday',
'monday',
'tuesday',
'wednesday',
'thursday',
'friday',
'saturday',
];
final List<Map<String, dynamic>> _timeSlots = [
{
'slotId': 'morning',
'label': 'Morning',
'timeRange': '4:00 AM - 12:00 PM',
'icon': LucideIcons.sunrise,
'bg': const Color(0xFFE6EBF9), // bg-[#0032A0]/10
'iconColor': const Color(0xFF0032A0),
},
{
'slotId': 'afternoon',
'label': 'Afternoon',
'timeRange': '12:00 PM - 6:00 PM',
'icon': LucideIcons.sun,
'bg': const Color(0xFFCCD6EC), // bg-[#0032A0]/20
'iconColor': const Color(0xFF0032A0),
},
{
'slotId': 'evening',
'label': 'Evening',
'timeRange': '6:00 PM - 12:00 AM',
'icon': LucideIcons.moon,
'bg': const Color(0xFFEBEDEE), // bg-[#333F48]/10
'iconColor': const Color(0xFF333F48),
},
];
final AvailabilityBloc _bloc = Modular.get<AvailabilityBloc>();
@override
void initState() {
super.initState();
_calculateInitialWeek();
}
void _calculateInitialWeek() {
final today = DateTime.now();
// Dart equivalent for Monday start:
final day = today.weekday; // Mon=1, Sun=7
final diff = day - 1;
_currentWeekStart = today.subtract(Duration(days: diff));
// Reset time to midnight
_currentWeekStart = DateTime(
_currentWeekStart.year,
_currentWeekStart.month,
_currentWeekStart.day,
final diff = day - 1; // Assuming Monday start
DateTime currentWeekStart = today.subtract(Duration(days: diff));
currentWeekStart = DateTime(
currentWeekStart.year,
currentWeekStart.month,
currentWeekStart.day,
);
_selectedDate = today;
}
List<DateTime> _getWeekDates() {
return List.generate(
7,
(index) => _currentWeekStart.add(Duration(days: index)),
);
}
String _formatDay(DateTime date) {
return DateFormat('EEE').format(date);
}
bool _isToday(DateTime date) {
final now = DateTime.now();
return date.year == now.year &&
date.month == now.month &&
date.day == now.day;
}
bool _isSelected(DateTime date) {
return date.year == _selectedDate.year &&
date.month == _selectedDate.month &&
date.day == _selectedDate.day;
}
void _navigateWeek(int direction) {
setState(() {
_currentWeekStart = _currentWeekStart.add(Duration(days: direction * 7));
});
}
void _toggleDayAvailability(String dayName) {
setState(() {
_availability[dayName] = !(_availability[dayName] ?? false);
// React code also updates mutation. We mock this.
// NOTE: In prototype we mock it. Refactor will move this to BLoC.
});
}
String _getDayKey(DateTime date) {
// DateTime.weekday: Mon=1...Sun=7.
// _dayNames array: 0=Sun, 1=Mon...
// Dart weekday: 7 is Sunday. 7 % 7 = 0.
return _dayNames[date.weekday % 7];
}
void _toggleTimeSlot(String slotId) {
final dayKey = _getDayKey(_selectedDate);
final currentDaySlots =
_timeSlotAvailability[dayKey] ??
{'morning': true, 'afternoon': true, 'evening': true};
final newValue = !(currentDaySlots[slotId] ?? true);
setState(() {
_timeSlotAvailability[dayKey] = {...currentDaySlots, slotId: newValue};
});
}
bool _isTimeSlotActive(String slotId) {
final dayKey = _getDayKey(_selectedDate);
final daySlots = _timeSlotAvailability[dayKey];
if (daySlots == null) return true;
return daySlots[slotId] != false;
}
String _getMonthYear() {
final middleDate = _currentWeekStart.add(const Duration(days: 3));
return DateFormat('MMMM yyyy').format(middleDate);
}
void _quickSet(String type) {
Map<String, bool> newAvailability = {};
switch (type) {
case 'all':
for (var day in _dayNames) newAvailability[day] = true;
break;
case 'weekdays':
for (var day in _dayNames)
newAvailability[day] = (day != 'saturday' && day != 'sunday');
break;
case 'weekends':
for (var day in _dayNames)
newAvailability[day] = (day == 'saturday' || day == 'sunday');
break;
case 'clear':
for (var day in _dayNames) newAvailability[day] = false;
break;
}
setState(() {
_availability = newAvailability;
});
_bloc.add(LoadAvailability(currentWeekStart));
}
@override
Widget build(BuildContext context) {
final selectedDayKey = _getDayKey(_selectedDate);
final isSelectedDayAvailable = _availability[selectedDayKey] ?? false;
final weekDates = _getWeekDates();
return Scaffold(
backgroundColor: const Color(
0xFFFAFBFC,
), // slate-50 to white gradient approximation
body: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 100),
child: Column(
children: [
_buildHeader(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildQuickSet(),
const SizedBox(height: 24),
_buildWeekNavigation(weekDates),
const SizedBox(height: 24),
_buildSelectedDayAvailability(
selectedDayKey,
isSelectedDayAvailable,
),
const SizedBox(height: 24),
_buildInfoCard(),
],
),
),
],
return BlocProvider.value(
value: _bloc,
child: Scaffold(
backgroundColor: AppColors.krowBackground,
appBar: UiAppBar(
title: 'My Availability',
showBackButton: true,
),
body: BlocBuilder<AvailabilityBloc, AvailabilityState>(
builder: (context, state) {
if (state is AvailabilityLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is AvailabilityLoaded) {
return SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 100),
child: Column(
children: [
//_buildHeader(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildQuickSet(context),
const SizedBox(height: 24),
_buildWeekNavigation(context, state),
const SizedBox(height: 24),
_buildSelectedDayAvailability(
context,
state.selectedDayAvailability,
),
const SizedBox(height: 24),
_buildInfoCard(),
],
),
),
],
),
);
} else if (state is AvailabilityError) {
return Center(child: Text('Error: ${state.message}'));
}
return const SizedBox.shrink();
},
),
),
);
@@ -244,73 +108,28 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
onPressed: () => Modular.to.pop(),
),
const SizedBox(width: 12),
Row(
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
border: Border.all(
color: AppColors.krowBlue.withOpacity(0.2),
width: 2,
),
shape: BoxShape.circle,
),
child: Center(
child: CircleAvatar(
backgroundColor: AppColors.krowBlue.withOpacity(
0.1,
),
radius: 18,
child: const Text(
'K', // Mock initial
style: TextStyle(
color: AppColors.krowBlue,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
Text(
'My Availability',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColors.krowCharcoal,
),
),
const SizedBox(width: 12),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'My Availability',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColors.krowCharcoal,
),
),
Text(
'Set when you can work',
style: TextStyle(
fontSize: 14,
color: AppColors.krowMuted,
),
),
],
Text(
'Set when you can work',
style: TextStyle(
fontSize: 14,
color: AppColors.krowMuted,
),
),
],
),
],
),
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.krowBlue.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.calendar,
color: AppColors.krowBlue,
size: 20,
),
),
],
),
],
@@ -318,7 +137,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
);
}
Widget _buildQuickSet() {
Widget _buildQuickSet(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
@@ -340,27 +159,34 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
Row(
children: [
Expanded(
child: _buildQuickSetButton('All Week', () => _quickSet('all')),
child: _buildQuickSetButton(
context,
'All Week',
'all',
),
),
const SizedBox(width: 8),
Expanded(
child: _buildQuickSetButton(
context,
'Weekdays',
() => _quickSet('weekdays'),
'weekdays',
),
),
const SizedBox(width: 8),
Expanded(
child: _buildQuickSetButton(
context,
'Weekends',
() => _quickSet('weekends'),
'weekends',
),
),
const SizedBox(width: 8),
Expanded(
child: _buildQuickSetButton(
context,
'Clear All',
() => _quickSet('clear'),
'clear',
isDestructive: true,
),
),
@@ -372,14 +198,15 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
}
Widget _buildQuickSetButton(
BuildContext context,
String label,
VoidCallback onTap, {
String type, {
bool isDestructive = false,
}) {
return SizedBox(
height: 32,
child: OutlinedButton(
onPressed: onTap,
onPressed: () => context.read<AvailabilityBloc>().add(PerformQuickSet(type)),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.zero,
side: BorderSide(
@@ -387,8 +214,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
? Colors.red.withOpacity(0.2)
: AppColors.krowBlue.withOpacity(0.2),
),
backgroundColor:
Colors.transparent,
backgroundColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
@@ -404,7 +230,11 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
);
}
Widget _buildWeekNavigation(List<DateTime> weekDates) {
Widget _buildWeekNavigation(BuildContext context, AvailabilityLoaded state) {
// Middle date for month display
final middleDate = state.currentWeekStart.add(const Duration(days: 3));
final monthYear = DateFormat('MMMM yyyy').format(middleDate);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
@@ -429,10 +259,10 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
children: [
_buildNavButton(
LucideIcons.chevronLeft,
() => _navigateWeek(-1),
() => context.read<AvailabilityBloc>().add(const NavigateWeek(-1)),
),
Text(
_getMonthYear(),
monthYear,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
@@ -441,7 +271,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
),
_buildNavButton(
LucideIcons.chevronRight,
() => _navigateWeek(1),
() => context.read<AvailabilityBloc>().add(const NavigateWeek(1)),
),
],
),
@@ -449,7 +279,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
// Days Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: weekDates.map((date) => _buildDayItem(date)).toList(),
children: state.days.map((day) => _buildDayItem(context, day, state.selectedDate)).toList(),
),
],
),
@@ -471,15 +301,14 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
);
}
Widget _buildDayItem(DateTime date) {
final isSelected = _isSelected(date);
final dayKey = _getDayKey(date);
final isAvailable = _availability[dayKey] ?? false;
final isToday = _isToday(date);
Widget _buildDayItem(BuildContext context, DayAvailability day, DateTime selectedDate) {
final isSelected = AvailabilityLoaded.isSameDay(day.date, selectedDate);
final isAvailable = day.isAvailable;
final isToday = AvailabilityLoaded.isSameDay(day.date, DateTime.now());
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _selectedDate = date),
onTap: () => context.read<AvailabilityBloc>().add(SelectDate(day.date)),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
padding: const EdgeInsets.symmetric(vertical: 12),
@@ -514,7 +343,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
Column(
children: [
Text(
date.day.toString().padLeft(2, '0'),
day.date.day.toString().padLeft(2, '0'),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@@ -527,7 +356,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
),
const SizedBox(height: 2),
Text(
_formatDay(date),
DateFormat('EEE').format(day.date),
style: TextStyle(
fontSize: 10,
color: isSelected
@@ -559,10 +388,11 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
}
Widget _buildSelectedDayAvailability(
String selectedDayKey,
bool isAvailable,
BuildContext context,
DayAvailability day,
) {
final dateStr = DateFormat('EEEE, MMM d').format(_selectedDate);
final dateStr = DateFormat('EEEE, MMM d').format(day.date);
final isAvailable = day.isAvailable;
return Container(
padding: const EdgeInsets.all(20),
@@ -606,7 +436,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
),
Switch(
value: isAvailable,
onChanged: (val) => _toggleDayAvailability(selectedDayKey),
onChanged: (val) => context.read<AvailabilityBloc>().add(ToggleDayStatus(day)),
activeColor: AppColors.krowBlue,
),
],
@@ -614,124 +444,164 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
const SizedBox(height: 16),
// Time Slots
..._timeSlots.map((slot) {
final isActive = _isTimeSlotActive(slot['slotId']);
// Determine styles based on state
final isEnabled =
isAvailable; // If day is off, slots are disabled visually
// Time Slots (only from Domain)
...day.slots.map((slot) {
// Get UI config for this slot ID
final uiConfig = _getSlotUiConfig(slot.id);
// Container style
Color bgColor;
Color borderColor;
if (!isEnabled) {
bgColor = const Color(0xFFF8FAFC); // slate-50
borderColor = const Color(0xFFF1F5F9); // slate-100
} else if (isActive) {
bgColor = AppColors.krowBlue.withOpacity(0.05);
borderColor = AppColors.krowBlue.withOpacity(0.2);
} else {
bgColor = const Color(0xFFF8FAFC); // slate-50
borderColor = const Color(0xFFE2E8F0); // slate-200
}
// Text colors
final titleColor = (isEnabled && isActive)
? AppColors.krowCharcoal
: AppColors.krowMuted;
final subtitleColor = (isEnabled && isActive)
? AppColors.krowMuted
: Colors.grey.shade400;
return GestureDetector(
onTap: isEnabled ? () => _toggleTimeSlot(slot['slotId']) : null,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: borderColor, width: 2),
),
child: Row(
children: [
// Icon
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: slot['bg'],
borderRadius: BorderRadius.circular(12),
),
child: Icon(
slot['icon'],
color: slot['iconColor'],
size: 20,
),
),
const SizedBox(width: 12),
// Text
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
slot['label'],
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: titleColor,
),
),
Text(
slot['timeRange'],
style: TextStyle(
fontSize: 12,
color: subtitleColor,
),
),
],
),
),
// Checkbox indicator
if (isEnabled && isActive)
Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
color: AppColors.krowBlue,
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.check,
size: 16,
color: Colors.white,
),
)
else if (isEnabled && !isActive)
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFFCBD5E1),
width: 2,
), // slate-300
),
),
],
),
),
);
return _buildTimeSlotItem(context, day, slot, uiConfig);
}).toList(),
],
),
);
}
Map<String, dynamic> _getSlotUiConfig(String slotId) {
switch (slotId) {
case 'morning':
return {
'icon': LucideIcons.sunrise,
'bg': const Color(0xFFE6EBF9), // bg-[#0032A0]/10
'iconColor': const Color(0xFF0032A0),
};
case 'afternoon':
return {
'icon': LucideIcons.sun,
'bg': const Color(0xFFCCD6EC), // bg-[#0032A0]/20
'iconColor': const Color(0xFF0032A0),
};
case 'evening':
return {
'icon': LucideIcons.moon,
'bg': const Color(0xFFEBEDEE), // bg-[#333F48]/10
'iconColor': const Color(0xFF333F48),
};
default:
return {
'icon': LucideIcons.clock,
'bg': Colors.grey.shade100,
'iconColor': Colors.grey,
};
}
}
Widget _buildTimeSlotItem(
BuildContext context,
DayAvailability day,
AvailabilitySlot slot,
Map<String, dynamic> uiConfig
) {
// Determine styles based on state
final isEnabled = day.isAvailable;
final isActive = slot.isAvailable;
// Container style
Color bgColor;
Color borderColor;
if (!isEnabled) {
bgColor = const Color(0xFFF8FAFC); // slate-50
borderColor = const Color(0xFFF1F5F9); // slate-100
} else if (isActive) {
bgColor = AppColors.krowBlue.withOpacity(0.05);
borderColor = AppColors.krowBlue.withOpacity(0.2);
} else {
bgColor = const Color(0xFFF8FAFC); // slate-50
borderColor = const Color(0xFFE2E8F0); // slate-200
}
// Text colors
final titleColor = (isEnabled && isActive)
? AppColors.krowCharcoal
: AppColors.krowMuted;
final subtitleColor = (isEnabled && isActive)
? AppColors.krowMuted
: Colors.grey.shade400;
return GestureDetector(
onTap: isEnabled ? () => context.read<AvailabilityBloc>().add(ToggleSlotStatus(day, slot.id)) : null,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: borderColor, width: 2),
),
child: Row(
children: [
// Icon
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: uiConfig['bg'],
borderRadius: BorderRadius.circular(12),
),
child: Icon(
uiConfig['icon'],
color: uiConfig['iconColor'],
size: 20,
),
),
const SizedBox(width: 12),
// Text
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
slot.label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: titleColor,
),
),
Text(
slot.timeRange,
style: TextStyle(
fontSize: 12,
color: subtitleColor,
),
),
],
),
),
// Checkbox indicator
if (isEnabled && isActive)
Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
color: AppColors.krowBlue,
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.check,
size: 16,
color: Colors.white,
),
)
else if (isEnabled && !isActive)
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFFCBD5E1),
width: 2,
), // slate-300
),
),
],
),
),
);
}
Widget _buildInfoCard() {
return Container(
padding: const EdgeInsets.all(16),

View File

@@ -1,693 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart' hide ModularWatchExtension;
import 'package:intl/intl.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../blocs/availability_bloc.dart';
import '../blocs/availability_event.dart';
import '../blocs/availability_state.dart';
import 'package:krow_domain/krow_domain.dart';
class AvailabilityPage extends StatefulWidget {
const AvailabilityPage({super.key});
@override
State<AvailabilityPage> createState() => _AvailabilityPageState();
}
class _AvailabilityPageState extends State<AvailabilityPage> {
final AvailabilityBloc _bloc = Modular.get<AvailabilityBloc>();
@override
void initState() {
super.initState();
_calculateInitialWeek();
}
void _calculateInitialWeek() {
final today = DateTime.now();
final day = today.weekday; // Mon=1, Sun=7
final diff = day - 1; // Assuming Monday start
DateTime currentWeekStart = today.subtract(Duration(days: diff));
currentWeekStart = DateTime(
currentWeekStart.year,
currentWeekStart.month,
currentWeekStart.day,
);
_bloc.add(LoadAvailability(currentWeekStart));
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _bloc,
child: Scaffold(
backgroundColor: AppColors.krowBackground,
body: BlocBuilder<AvailabilityBloc, AvailabilityState>(
builder: (context, state) {
if (state is AvailabilityLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is AvailabilityLoaded) {
return SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 100),
child: Column(
children: [
_buildHeader(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildQuickSet(context),
const SizedBox(height: 24),
_buildWeekNavigation(context, state),
const SizedBox(height: 24),
_buildSelectedDayAvailability(
context,
state.selectedDayAvailability,
),
const SizedBox(height: 24),
_buildInfoCard(),
],
),
),
],
),
);
} else if (state is AvailabilityError) {
return Center(child: Text('Error: ${state.message}'));
}
return const SizedBox.shrink();
},
),
),
);
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.fromLTRB(20, 60, 20, 20),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
IconButton(
icon: const Icon(
LucideIcons.arrowLeft,
color: AppColors.krowCharcoal,
),
onPressed: () => Modular.to.pop(),
),
const SizedBox(width: 12),
Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
border: Border.all(
color: AppColors.krowBlue.withOpacity(0.2),
width: 2,
),
shape: BoxShape.circle,
),
child: Center(
child: CircleAvatar(
backgroundColor: AppColors.krowBlue.withOpacity(
0.1,
),
radius: 18,
child: const Text(
'K', // Mock initial
style: TextStyle(
color: AppColors.krowBlue,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
),
),
const SizedBox(width: 12),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'My Availability',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColors.krowCharcoal,
),
),
Text(
'Set when you can work',
style: TextStyle(
fontSize: 14,
color: AppColors.krowMuted,
),
),
],
),
],
),
],
),
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.krowBlue.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.calendar,
color: AppColors.krowBlue,
size: 20,
),
),
],
),
],
),
);
}
Widget _buildQuickSet(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.krowBlue.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Quick Set Availability',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF333F48),
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildQuickSetButton(
context,
'All Week',
'all',
),
),
const SizedBox(width: 8),
Expanded(
child: _buildQuickSetButton(
context,
'Weekdays',
'weekdays',
),
),
const SizedBox(width: 8),
Expanded(
child: _buildQuickSetButton(
context,
'Weekends',
'weekends',
),
),
const SizedBox(width: 8),
Expanded(
child: _buildQuickSetButton(
context,
'Clear All',
'clear',
isDestructive: true,
),
),
],
),
],
),
);
}
Widget _buildQuickSetButton(
BuildContext context,
String label,
String type, {
bool isDestructive = false,
}) {
return SizedBox(
height: 32,
child: OutlinedButton(
onPressed: () => context.read<AvailabilityBloc>().add(PerformQuickSet(type)),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.zero,
side: BorderSide(
color: isDestructive
? Colors.red.withOpacity(0.2)
: AppColors.krowBlue.withOpacity(0.2),
),
backgroundColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
foregroundColor: isDestructive ? Colors.red : AppColors.krowBlue,
),
child: Text(
label,
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w500),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
);
}
Widget _buildWeekNavigation(BuildContext context, AvailabilityLoaded state) {
// Middle date for month display
final middleDate = state.currentWeekStart.add(const Duration(days: 3));
final monthYear = DateFormat('MMMM yyyy').format(middleDate);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade100),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Column(
children: [
// Nav Header
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildNavButton(
LucideIcons.chevronLeft,
() => context.read<AvailabilityBloc>().add(const NavigateWeek(-1)),
),
Text(
monthYear,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.krowCharcoal,
),
),
_buildNavButton(
LucideIcons.chevronRight,
() => context.read<AvailabilityBloc>().add(const NavigateWeek(1)),
),
],
),
),
// Days Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: state.days.map((day) => _buildDayItem(context, day, state.selectedDate)).toList(),
),
],
),
);
}
Widget _buildNavButton(IconData icon, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 32,
height: 32,
decoration: const BoxDecoration(
color: Color(0xFFF1F5F9), // slate-100
shape: BoxShape.circle,
),
child: Icon(icon, size: 20, color: AppColors.krowMuted),
),
);
}
Widget _buildDayItem(BuildContext context, DayAvailability day, DateTime selectedDate) {
final isSelected = AvailabilityLoaded.isSameDay(day.date, selectedDate);
final isAvailable = day.isAvailable;
final isToday = AvailabilityLoaded.isSameDay(day.date, DateTime.now());
return Expanded(
child: GestureDetector(
onTap: () => context.read<AvailabilityBloc>().add(SelectDate(day.date)),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isSelected
? AppColors.krowBlue
: (isAvailable
? const Color(0xFFECFDF5)
: const Color(0xFFF8FAFC)), // emerald-50 or slate-50
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected
? AppColors.krowBlue
: (isAvailable
? const Color(0xFFA7F3D0)
: Colors.transparent), // emerald-200
),
boxShadow: isSelected
? [
BoxShadow(
color: AppColors.krowBlue.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
]
: null,
),
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
Column(
children: [
Text(
day.date.day.toString().padLeft(2, '0'),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isSelected
? Colors.white
: (isAvailable
? const Color(0xFF047857)
: AppColors.krowMuted), // emerald-700
),
),
const SizedBox(height: 2),
Text(
DateFormat('EEE').format(day.date),
style: TextStyle(
fontSize: 10,
color: isSelected
? Colors.white.withOpacity(0.8)
: (isAvailable
? const Color(0xFF047857)
: AppColors.krowMuted),
),
),
],
),
if (isToday && !isSelected)
Positioned(
bottom: -8,
child: Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: AppColors.krowBlue,
shape: BoxShape.circle,
),
),
),
],
),
),
),
);
}
Widget _buildSelectedDayAvailability(
BuildContext context,
DayAvailability day,
) {
final dateStr = DateFormat('EEEE, MMM d').format(day.date);
final isAvailable = day.isAvailable;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade100),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Column(
children: [
// Header Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
dateStr,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.krowCharcoal,
),
),
Text(
isAvailable ? 'You are available' : 'Not available',
style: const TextStyle(
fontSize: 14,
color: AppColors.krowMuted,
),
),
],
),
Switch(
value: isAvailable,
onChanged: (val) => context.read<AvailabilityBloc>().add(ToggleDayStatus(day)),
activeColor: AppColors.krowBlue,
),
],
),
const SizedBox(height: 16),
// Time Slots (only from Domain)
...day.slots.map((slot) {
// Get UI config for this slot ID
final uiConfig = _getSlotUiConfig(slot.id);
return _buildTimeSlotItem(context, day, slot, uiConfig);
}).toList(),
],
),
);
}
Map<String, dynamic> _getSlotUiConfig(String slotId) {
switch (slotId) {
case 'morning':
return {
'icon': LucideIcons.sunrise,
'bg': const Color(0xFFE6EBF9), // bg-[#0032A0]/10
'iconColor': const Color(0xFF0032A0),
};
case 'afternoon':
return {
'icon': LucideIcons.sun,
'bg': const Color(0xFFCCD6EC), // bg-[#0032A0]/20
'iconColor': const Color(0xFF0032A0),
};
case 'evening':
return {
'icon': LucideIcons.moon,
'bg': const Color(0xFFEBEDEE), // bg-[#333F48]/10
'iconColor': const Color(0xFF333F48),
};
default:
return {
'icon': LucideIcons.clock,
'bg': Colors.grey.shade100,
'iconColor': Colors.grey,
};
}
}
Widget _buildTimeSlotItem(
BuildContext context,
DayAvailability day,
AvailabilitySlot slot,
Map<String, dynamic> uiConfig
) {
// Determine styles based on state
final isEnabled = day.isAvailable;
final isActive = slot.isAvailable;
// Container style
Color bgColor;
Color borderColor;
if (!isEnabled) {
bgColor = const Color(0xFFF8FAFC); // slate-50
borderColor = const Color(0xFFF1F5F9); // slate-100
} else if (isActive) {
bgColor = AppColors.krowBlue.withOpacity(0.05);
borderColor = AppColors.krowBlue.withOpacity(0.2);
} else {
bgColor = const Color(0xFFF8FAFC); // slate-50
borderColor = const Color(0xFFE2E8F0); // slate-200
}
// Text colors
final titleColor = (isEnabled && isActive)
? AppColors.krowCharcoal
: AppColors.krowMuted;
final subtitleColor = (isEnabled && isActive)
? AppColors.krowMuted
: Colors.grey.shade400;
return GestureDetector(
onTap: isEnabled ? () => context.read<AvailabilityBloc>().add(ToggleSlotStatus(day, slot.id)) : null,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: borderColor, width: 2),
),
child: Row(
children: [
// Icon
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: uiConfig['bg'],
borderRadius: BorderRadius.circular(12),
),
child: Icon(
uiConfig['icon'],
color: uiConfig['iconColor'],
size: 20,
),
),
const SizedBox(width: 12),
// Text
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
slot.label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: titleColor,
),
),
Text(
slot.timeRange,
style: TextStyle(
fontSize: 12,
color: subtitleColor,
),
),
],
),
),
// Checkbox indicator
if (isEnabled && isActive)
Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
color: AppColors.krowBlue,
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.check,
size: 16,
color: Colors.white,
),
)
else if (isEnabled && !isActive)
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFFCBD5E1),
width: 2,
), // slate-300
),
),
],
),
),
);
}
Widget _buildInfoCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.krowBlue.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
),
child: const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(LucideIcons.clock, size: 20, color: AppColors.krowBlue),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Auto-Match uses your availability',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.krowCharcoal,
),
),
SizedBox(height: 2),
Text(
"When enabled, you'll only be matched with shifts during your available times.",
style: TextStyle(fontSize: 12, color: AppColors.krowMuted),
),
],
),
),
],
),
);
}
}
class AppColors {
static const Color krowBlue = Color(0xFF0A39DF);
static const Color krowYellow = Color(0xFFFFED4A);
static const Color krowCharcoal = Color(0xFF121826);
static const Color krowMuted = Color(0xFF6A7382);
static const Color krowBorder = Color(0xFFE3E6E9);
static const Color krowBackground = Color(0xFFFAFBFC);
static const Color white = Colors.white;
static const Color black = Colors.black;
}

View File

@@ -1,7 +1,7 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:staff_availability/src/presentation/pages/availability_page_new.dart';
import 'package:staff_availability/src/presentation/pages/availability_page.dart';
import 'data/repositories_impl/availability_repository_impl.dart';
import 'domain/repositories/availability_repository.dart';

View File

@@ -29,6 +29,7 @@ dependencies:
krow_core:
path: ../../../core
firebase_data_connect: ^0.2.2+2
firebase_auth: ^6.1.4
dev_dependencies:
flutter_test: