feat: Implement staff availability feature with BLoC architecture

- Created AvailabilityPage with UI for managing staff availability.
- Integrated BLoC for state management, including LoadAvailability and ToggleDayStatus events.
- Added quick set options for availability (All Week, Weekdays, Weekends, Clear All).
- Implemented week navigation and day selection with visual feedback.
- Developed time slot management for each day, allowing toggling of availability.
- Established StaffAvailabilityModule for dependency injection and routing.
- Updated pubspec.yaml with necessary dependencies for the feature.
This commit is contained in:
Achintha Isuru
2026-01-25 22:35:09 -05:00
parent 323f0a9370
commit 2a820b3e4f
24 changed files with 2164 additions and 2 deletions

View File

@@ -6,7 +6,7 @@
/// Locales: 2
/// Strings: 1026 (513 per locale)
///
/// Built on 2026-01-26 at 02:14 UTC
/// Built on 2026-01-26 at 03:26 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import

View File

@@ -36,4 +36,23 @@ class StaffRepositoryMock {
await Future.delayed(const Duration(milliseconds: 500));
return staff;
}
// Mock Availability Data Store
final Map<String, dynamic> _mockAvailability = {};
Future<Map<String, dynamic>> getAvailability(String userId, DateTime start, DateTime end) async {
await Future.delayed(const Duration(milliseconds: 300));
// Return mock structure: Date ISO String -> { isAvailable: bool, slots: { id: bool } }
// Auto-generate some data if empty
if (_mockAvailability.isEmpty) {
// Just return empty, let the caller handle defaults
}
return _mockAvailability;
}
Future<void> updateAvailability(String userId, String dateIso, Map<String, dynamic> data) async {
await Future.delayed(const Duration(milliseconds: 200));
_mockAvailability[dateIso] = data;
}
}

View File

@@ -73,3 +73,7 @@ export 'src/entities/support/working_area.dart';
// Home
export 'src/entities/home/home_dashboard_data.dart';
export 'src/entities/home/reorder_item.dart';
// Availability
export 'src/entities/availability/availability_slot.dart';
export 'src/entities/availability/day_availability.dart';

View File

@@ -0,0 +1,33 @@
import 'package:equatable/equatable.dart';
/// Represents a specific time slot within a day (e.g., Morning, Afternoon, Evening).
class AvailabilitySlot extends Equatable {
final String id;
final String label;
final String timeRange;
final bool isAvailable;
const AvailabilitySlot({
required this.id,
required this.label,
required this.timeRange,
this.isAvailable = true,
});
AvailabilitySlot copyWith({
String? id,
String? label,
String? timeRange,
bool? isAvailable,
}) {
return AvailabilitySlot(
id: id ?? this.id,
label: label ?? this.label,
timeRange: timeRange ?? this.timeRange,
isAvailable: isAvailable ?? this.isAvailable,
);
}
@override
List<Object?> get props => [id, label, timeRange, isAvailable];
}

View File

@@ -0,0 +1,31 @@
import 'package:equatable/equatable.dart';
import 'availability_slot.dart';
/// Represents availability configuration for a specific date.
class DayAvailability extends Equatable {
final DateTime date;
final bool isAvailable;
final List<AvailabilitySlot> slots;
const DayAvailability({
required this.date,
this.isAvailable = false,
this.slots = const [],
});
DayAvailability copyWith({
DateTime? date,
bool? isAvailable,
List<AvailabilitySlot>? slots,
}) {
return DayAvailability(
date: date ?? this.date,
isAvailable: isAvailable ?? this.isAvailable,
slots: slots ?? this.slots,
);
}
@override
List<Object?> get props => [date, isAvailable, slots];
}

View File

@@ -0,0 +1,5 @@
include: package:flutter_lints/flutter.yaml
linter:
rules:
public_member_api_docs: false

View File

@@ -0,0 +1,164 @@
import 'package:krow_data_connect/krow_data_connect.dart' hide AvailabilitySlot;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/availability_repository.dart';
import 'package:intl/intl.dart';
/// Implementation of [AvailabilityRepository].
///
/// Uses [StaffRepositoryMock] from data_connect to fetch and store data.
class AvailabilityRepositoryImpl implements AvailabilityRepository {
final StaffRepositoryMock _dataSource;
// Mock User ID - in real app invoke AuthUseCase to get current user
final String _userId = 'mock_user_123';
static const List<Map<String, String>> _slotDefinitions = [
{
'id': 'morning',
'label': 'Morning',
'timeRange': '4:00 AM - 12:00 PM',
},
{
'id': 'afternoon',
'label': 'Afternoon',
'timeRange': '12:00 PM - 6:00 PM',
},
{
'id': 'evening',
'label': 'Evening',
'timeRange': '6:00 PM - 12:00 AM',
},
];
AvailabilityRepositoryImpl({StaffRepositoryMock? dataSource})
: _dataSource = dataSource ?? StaffRepositoryMock();
@override
Future<List<DayAvailability>> getAvailability(
DateTime start, DateTime end) async {
final rawData = await _dataSource.getAvailability(_userId, start, end);
final List<DayAvailability> days = [];
// Loop through each day in range
for (int i = 0; i <= end.difference(start).inDays; i++) {
final date = start.add(Duration(days: i));
final dateKey = DateFormat('yyyy-MM-dd').format(date);
final dayData = rawData[dateKey];
if (dayData != null) {
days.add(_mapFromData(date, dayData));
} else {
// Default: Available M-F, Not Sat-Sun (matching prototype logic)
final isWeekend = date.weekday == DateTime.saturday || date.weekday == DateTime.sunday;
// Prototype: Sat/Sun false
days.add(DayAvailability(
date: date,
isAvailable: !isWeekend,
slots: _generateDefaultSlots(isEnabled: !isWeekend),
));
}
}
return days;
}
@override
Future<DayAvailability> updateDayAvailability(
DayAvailability availability) async {
final dateKey = DateFormat('yyyy-MM-dd').format(availability.date);
final data = _mapToData(availability);
await _dataSource.updateAvailability(_userId, dateKey, data);
return availability;
}
@override
Future<List<DayAvailability>> applyQuickSet(
DateTime start, DateTime end, String type) async {
final List<DayAvailability> updatedDays = [];
for (int i = 0; i <= end.difference(start).inDays; i++) {
final date = start.add(Duration(days: i));
bool isAvailable = false;
switch (type) {
case 'all':
isAvailable = true;
break;
case 'weekdays':
isAvailable = date.weekday != DateTime.saturday && date.weekday != DateTime.sunday;
break;
case 'weekends':
isAvailable = date.weekday == DateTime.saturday || date.weekday == DateTime.sunday;
break;
case 'clear':
isAvailable = false;
break;
}
// Keep existing slot preferences, just toggle main switch?
// Or reset slots too? Prototype behavior: just sets map[day] = bool.
// But it implies slots are active if day is active?
// For now, allow slots to be default true if day is enabled.
final day = DayAvailability(
date: date,
isAvailable: isAvailable,
slots: _generateDefaultSlots(isEnabled: isAvailable),
);
await updateDayAvailability(day);
updatedDays.add(day);
}
return updatedDays;
}
// --- Helpers ---
List<AvailabilitySlot> _generateDefaultSlots({bool isEnabled = true}) {
return _slotDefinitions.map((def) {
return AvailabilitySlot(
id: def['id']!,
label: def['label']!,
timeRange: def['timeRange']!,
isAvailable: true, // Default slots to true
);
}).toList();
}
DayAvailability _mapFromData(DateTime date, Map<String, dynamic> data) {
final isAvailable = data['isAvailable'] as bool? ?? false;
final Map<String, dynamic> slotsMap = data['slots'] ?? {};
final slots = _slotDefinitions.map((def) {
final slotId = def['id']!;
final slotEnabled = slotsMap[slotId] as bool? ?? true; // Default true if not stored
return AvailabilitySlot(
id: slotId,
label: def['label']!,
timeRange: def['timeRange']!,
isAvailable: slotEnabled,
);
}).toList();
return DayAvailability(
date: date,
isAvailable: isAvailable,
slots: slots,
);
}
Map<String, dynamic> _mapToData(DayAvailability day) {
Map<String, bool> slotsMap = {};
for (var slot in day.slots) {
slotsMap[slot.id] = slot.isAvailable;
}
return {
'isAvailable': day.isAvailable,
'slots': slotsMap,
};
}
}

View File

@@ -0,0 +1,12 @@
import 'package:krow_domain/krow_domain.dart';
abstract class AvailabilityRepository {
/// Fetches availability for a given date range (usually a week).
Future<List<DayAvailability>> getAvailability(DateTime start, DateTime end);
/// Updates the availability for a specific day.
Future<DayAvailability> updateDayAvailability(DayAvailability availability);
/// Applies a preset configuration (e.g. All Week, Weekdays only) to a range.
Future<List<DayAvailability>> applyQuickSet(DateTime start, DateTime end, String type);
}

View File

@@ -0,0 +1,28 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/availability_repository.dart';
/// Use case to apply a quick-set availability pattern (e.g., "Weekdays", "All Week") to a week.
class ApplyQuickSetUseCase extends UseCase<ApplyQuickSetParams, List<DayAvailability>> {
final AvailabilityRepository repository;
ApplyQuickSetUseCase(this.repository);
/// [type] can be 'all', 'weekdays', 'weekends', 'clear'
@override
Future<List<DayAvailability>> call(ApplyQuickSetParams params) {
final end = params.start.add(const Duration(days: 6));
return repository.applyQuickSet(params.start, end, params.type);
}
}
/// Parameters for [ApplyQuickSetUseCase].
class ApplyQuickSetParams extends UseCaseArgument {
final DateTime start;
final String type;
const ApplyQuickSetParams(this.start, this.type);
@override
List<Object?> get props => [start, type];
}

View File

@@ -0,0 +1,30 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/availability_repository.dart';
/// Use case to fetch availability for a specific week.
///
/// This encapsulates the logic of calculating the week range and fetching data
/// from the repository.
class GetWeeklyAvailabilityUseCase extends UseCase<GetWeeklyAvailabilityParams, List<DayAvailability>> {
final AvailabilityRepository repository;
GetWeeklyAvailabilityUseCase(this.repository);
@override
Future<List<DayAvailability>> call(GetWeeklyAvailabilityParams params) async {
// Calculate end of week (assuming start is start of week)
final end = params.start.add(const Duration(days: 6));
return repository.getAvailability(params.start, end);
}
}
/// Parameters for [GetWeeklyAvailabilityUseCase].
class GetWeeklyAvailabilityParams extends UseCaseArgument {
final DateTime start;
const GetWeeklyAvailabilityParams(this.start);
@override
List<Object?> get props => [start];
}

View File

@@ -0,0 +1,25 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/availability_repository.dart';
/// Use case to update the availability configuration for a specific day.
class UpdateDayAvailabilityUseCase extends UseCase<UpdateDayAvailabilityParams, DayAvailability> {
final AvailabilityRepository repository;
UpdateDayAvailabilityUseCase(this.repository);
@override
Future<DayAvailability> call(UpdateDayAvailabilityParams params) {
return repository.updateDayAvailability(params.availability);
}
}
/// Parameters for [UpdateDayAvailabilityUseCase].
class UpdateDayAvailabilityParams extends UseCaseArgument {
final DayAvailability availability;
const UpdateDayAvailabilityParams(this.availability);
@override
List<Object?> get props => [availability];
}

View File

@@ -0,0 +1,136 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/usecases/apply_quick_set_usecase.dart';
import '../../domain/usecases/get_weekly_availability_usecase.dart';
import '../../domain/usecases/update_day_availability_usecase.dart';
import 'availability_event.dart';
import 'availability_state.dart';
class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState> {
final GetWeeklyAvailabilityUseCase getWeeklyAvailability;
final UpdateDayAvailabilityUseCase updateDayAvailability;
final ApplyQuickSetUseCase applyQuickSet;
AvailabilityBloc({
required this.getWeeklyAvailability,
required this.updateDayAvailability,
required this.applyQuickSet,
}) : super(AvailabilityInitial()) {
on<LoadAvailability>(_onLoadAvailability);
on<SelectDate>(_onSelectDate);
on<NavigateWeek>(_onNavigateWeek);
on<ToggleDayStatus>(_onToggleDayStatus);
on<ToggleSlotStatus>(_onToggleSlotStatus);
on<PerformQuickSet>(_onPerformQuickSet);
}
Future<void> _onLoadAvailability(
LoadAvailability event,
Emitter<AvailabilityState> emit,
) async {
emit(AvailabilityLoading());
try {
final days = await getWeeklyAvailability(
GetWeeklyAvailabilityParams(event.weekStart));
emit(AvailabilityLoaded(
days: days,
currentWeekStart: event.weekStart,
selectedDate: event.preselectedDate ??
(days.isNotEmpty ? days.first.date : DateTime.now()),
));
} catch (e) {
emit(AvailabilityError(e.toString()));
}
}
void _onSelectDate(SelectDate event, Emitter<AvailabilityState> emit) {
if (state is AvailabilityLoaded) {
emit((state as AvailabilityLoaded).copyWith(selectedDate: event.date));
}
}
Future<void> _onNavigateWeek(
NavigateWeek event,
Emitter<AvailabilityState> emit,
) async {
if (state is AvailabilityLoaded) {
final currentState = state as AvailabilityLoaded;
final newWeekStart = currentState.currentWeekStart
.add(Duration(days: event.direction * 7));
final diff = currentState.selectedDate.difference(currentState.currentWeekStart).inDays;
final newSelectedDate = newWeekStart.add(Duration(days: diff));
add(LoadAvailability(newWeekStart, preselectedDate: newSelectedDate));
}
}
Future<void> _onToggleDayStatus(
ToggleDayStatus event,
Emitter<AvailabilityState> emit,
) async {
if (state is AvailabilityLoaded) {
final currentState = state as AvailabilityLoaded;
final newDay = event.day.copyWith(isAvailable: !event.day.isAvailable);
final updatedDays = currentState.days.map((d) {
return d.date == event.day.date ? newDay : d;
}).toList();
emit(currentState.copyWith(days: updatedDays));
try {
await updateDayAvailability(UpdateDayAvailabilityParams(newDay));
} catch (e) {
emit(currentState.copyWith(days: currentState.days));
}
}
}
Future<void> _onToggleSlotStatus(
ToggleSlotStatus event,
Emitter<AvailabilityState> emit,
) async {
if (state is AvailabilityLoaded) {
final currentState = state as AvailabilityLoaded;
final updatedSlots = event.day.slots.map((s) {
if (s.id == event.slotId) {
return s.copyWith(isAvailable: !s.isAvailable);
}
return s;
}).toList();
final newDay = event.day.copyWith(slots: updatedSlots);
final updatedDays = currentState.days.map((d) {
return d.date == event.day.date ? newDay : d;
}).toList();
emit(currentState.copyWith(days: updatedDays));
try {
await updateDayAvailability(UpdateDayAvailabilityParams(newDay));
} catch (e) {
emit(currentState.copyWith(days: currentState.days));
}
}
}
Future<void> _onPerformQuickSet(
PerformQuickSet event,
Emitter<AvailabilityState> emit,
) async {
if (state is AvailabilityLoaded) {
final currentState = state as AvailabilityLoaded;
try {
final newDays = await applyQuickSet(
ApplyQuickSetParams(currentState.currentWeekStart, event.type));
emit(currentState.copyWith(days: newDays));
} catch (e) {
// Handle error
}
}
}
}

View File

@@ -0,0 +1,54 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
abstract class AvailabilityEvent extends Equatable {
const AvailabilityEvent();
@override
List<Object?> get props => [];
}
class LoadAvailability extends AvailabilityEvent {
final DateTime weekStart;
final DateTime? preselectedDate; // Maintain selection after reload
const LoadAvailability(this.weekStart, {this.preselectedDate});
@override
List<Object?> get props => [weekStart, preselectedDate];
}
class SelectDate extends AvailabilityEvent {
final DateTime date;
const SelectDate(this.date);
@override
List<Object?> get props => [date];
}
class ToggleDayStatus extends AvailabilityEvent {
final DayAvailability day;
const ToggleDayStatus(this.day);
@override
List<Object?> get props => [day];
}
class ToggleSlotStatus extends AvailabilityEvent {
final DayAvailability day;
final String slotId;
const ToggleSlotStatus(this.day, this.slotId);
@override
List<Object?> get props => [day, slotId];
}
class NavigateWeek extends AvailabilityEvent {
final int direction; // -1 or 1
const NavigateWeek(this.direction);
@override
List<Object?> get props => [direction];
}
class PerformQuickSet extends AvailabilityEvent {
final String type; // all, weekdays, weekends, clear
const PerformQuickSet(this.type);
@override
List<Object?> get props => [type];
}

View File

@@ -0,0 +1,58 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
abstract class AvailabilityState extends Equatable {
const AvailabilityState();
@override
List<Object?> get props => [];
}
class AvailabilityInitial extends AvailabilityState {}
class AvailabilityLoading extends AvailabilityState {}
class AvailabilityLoaded extends AvailabilityState {
final List<DayAvailability> days;
final DateTime currentWeekStart;
final DateTime selectedDate;
const AvailabilityLoaded({
required this.days,
required this.currentWeekStart,
required this.selectedDate,
});
/// Helper to get the currently selected day's availability object
DayAvailability get selectedDayAvailability {
return days.firstWhere(
(d) => isSameDay(d.date, selectedDate),
orElse: () => DayAvailability(date: selectedDate), // Fallback
);
}
AvailabilityLoaded copyWith({
List<DayAvailability>? days,
DateTime? currentWeekStart,
DateTime? selectedDate,
}) {
return AvailabilityLoaded(
days: days ?? this.days,
currentWeekStart: currentWeekStart ?? this.currentWeekStart,
selectedDate: selectedDate ?? this.selectedDate,
);
}
static bool isSameDay(DateTime a, DateTime b) {
return a.year == b.year && a.month == b.month && a.day == b.day;
}
@override
List<Object?> get props => [days, currentWeekStart, selectedDate];
}
class AvailabilityError extends AvailabilityState {
final String message;
const AvailabilityError(this.message);
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,783 @@
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import 'package:lucide_icons/lucide_icons.dart';
class AvailabilityPage extends StatefulWidget {
const AvailabilityPage({super.key});
@override
State<AvailabilityPage> createState() => _AvailabilityPageState();
}
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),
},
];
@override
void initState() {
super.initState();
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,
);
_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;
});
}
@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(),
],
),
),
],
),
),
);
}
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() {
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('All Week', () => _quickSet('all')),
),
const SizedBox(width: 8),
Expanded(
child: _buildQuickSetButton(
'Weekdays',
() => _quickSet('weekdays'),
),
),
const SizedBox(width: 8),
Expanded(
child: _buildQuickSetButton(
'Weekends',
() => _quickSet('weekends'),
),
),
const SizedBox(width: 8),
Expanded(
child: _buildQuickSetButton(
'Clear All',
() => _quickSet('clear'),
isDestructive: true,
),
),
],
),
],
),
);
}
Widget _buildQuickSetButton(
String label,
VoidCallback onTap, {
bool isDestructive = false,
}) {
return SizedBox(
height: 32,
child: OutlinedButton(
onPressed: onTap,
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(List<DateTime> weekDates) {
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,
() => _navigateWeek(-1),
),
Text(
_getMonthYear(),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.krowCharcoal,
),
),
_buildNavButton(
LucideIcons.chevronRight,
() => _navigateWeek(1),
),
],
),
),
// Days Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: weekDates.map((date) => _buildDayItem(date)).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(DateTime date) {
final isSelected = _isSelected(date);
final dayKey = _getDayKey(date);
final isAvailable = _availability[dayKey] ?? false;
final isToday = _isToday(date);
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _selectedDate = 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(
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(
_formatDay(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(
String selectedDayKey,
bool isAvailable,
) {
final dateStr = DateFormat('EEEE, MMM d').format(_selectedDate);
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) => _toggleDayAvailability(selectedDayKey),
activeColor: AppColors.krowBlue,
),
],
),
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
// 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
),
),
],
),
),
);
}).toList(),
],
),
);
}
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

@@ -0,0 +1,693 @@
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

@@ -0,0 +1,33 @@
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'data/repositories/availability_repository_impl.dart';
import 'domain/repositories/availability_repository.dart';
import 'domain/usecases/apply_quick_set_usecase.dart';
import 'domain/usecases/get_weekly_availability_usecase.dart';
import 'domain/usecases/update_day_availability_usecase.dart';
import 'presentation/blocs/availability_bloc.dart';
import 'presentation/pages/availability_page.dart';
class StaffAvailabilityModule extends Module {
@override
void binds(i) {
// Data Sources
i.add(StaffRepositoryMock.new);
// Repository
i.add<AvailabilityRepository>(AvailabilityRepositoryImpl.new);
// UseCases
i.add(GetWeeklyAvailabilityUseCase.new);
i.add(UpdateDayAvailabilityUseCase.new);
i.add(ApplyQuickSetUseCase.new);
// BLoC
i.add(AvailabilityBloc.new);
}
@override
void routes(r) {
r.child('/', child: (_) => const AvailabilityPage());
}
}

View File

@@ -0,0 +1,3 @@
library staff_availability;
export 'src/staff_availability_module.dart';

View File

@@ -0,0 +1,35 @@
name: staff_availability
description: Staff Availability Feature
version: 0.0.1
publish_to: 'none'
resolution: workspace
environment:
sdk: '>=3.0.0 <4.0.0'
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.3
equatable: ^2.0.5
intl: ^0.20.0
lucide_icons: ^0.257.0
flutter_modular: ^6.3.2
# Internal packages
core_localization:
path: ../../../core_localization
design_system:
path: ../../../design_system
krow_domain:
path: ../../../domain
krow_data_connect:
path: ../../../data_connect
krow_core:
path: ../../../core
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0

View File

@@ -13,7 +13,7 @@ extension HomeNavigator on IModularNavigator {
/// Navigates to the availability page.
void pushAvailability() {
pushNamed('/availability');
pushNamed('/worker-main/availability');
}
/// Navigates to the messages page.

View File

@@ -13,6 +13,7 @@ import 'package:staff_attire/staff_attire.dart';
import 'package:staff_shifts/staff_shifts.dart';
import 'package:staff_payments/staff_payements.dart';
import 'package:staff_time_card/staff_time_card.dart';
import 'package:staff_availability/staff_availability.dart';
import 'package:staff_main/src/presentation/blocs/staff_main_cubit.dart';
import 'package:staff_main/src/presentation/constants/staff_main_routes.dart';
@@ -72,5 +73,6 @@ class StaffMainModule extends Module {
'/time-card',
module: StaffTimeCardModule(),
);
r.module('/availability', module: StaffAvailabilityModule());
}
}

View File

@@ -49,6 +49,8 @@ dependencies:
path: ../payments
staff_time_card:
path: ../profile_sections/finances/time_card
staff_availability:
path: ../availability
dev_dependencies:
flutter_test:

View File

@@ -1079,6 +1079,13 @@ packages:
relative: true
source: path
version: "0.0.1"
staff_availability:
dependency: transitive
description:
path: "packages/features/staff/availability"
relative: true
source: path
version: "0.0.1"
staff_bank_account:
dependency: transitive
description: