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:
@@ -22,6 +22,11 @@
|
||||
### Why this task is blocked:
|
||||
- Although this page existed in the prototype, it was not connected to any other pages. In other words, there was no way to navigate to it from anywhere in the application. Therefore, this issue can be closed, as the page is not required in the main application.
|
||||
|
||||
### Github issue
|
||||
- https://github.com/Oloodi/krow-workforce/issues/262
|
||||
### Why this task is blocked:
|
||||
- Although this page existed in the prototype, it was not connected to any other pages. In other words, there was no way to navigate to it from anywhere in the application. Therefore, this issue can be closed, as the page is not required in the main application.
|
||||
|
||||
# Deviations
|
||||
|
||||
## App
|
||||
|
||||
@@ -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