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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
rules:
|
||||
public_member_api_docs: false
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
library staff_availability;
|
||||
|
||||
export 'src/staff_availability_module.dart';
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ dependencies:
|
||||
path: ../payments
|
||||
staff_time_card:
|
||||
path: ../profile_sections/finances/time_card
|
||||
staff_availability:
|
||||
path: ../availability
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user