feat: Migrate staff profile features from Data Connect to V2 REST API
- Removed data_connect package from mobile pubspec.yaml. - Added documentation for V2 profile migration status and QA findings. - Implemented new session management with ClientSessionStore and StaffSessionStore. - Created V2SessionService for handling user sessions via the V2 API. - Developed use cases for cancelling late worker assignments and submitting worker reviews. - Added arguments and use cases for payment chart retrieval and profile completion checks. - Implemented repository interfaces and their implementations for staff main and profile features. - Ensured proper error handling and validation in use cases.
This commit is contained in:
@@ -1,235 +1,108 @@
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/repositories/availability_repository.dart';
|
||||
import 'package:staff_availability/src/domain/repositories/availability_repository.dart';
|
||||
|
||||
/// Implementation of [AvailabilityRepository] using Firebase Data Connect.
|
||||
/// V2 API implementation of [AvailabilityRepository].
|
||||
///
|
||||
/// Note: The backend schema supports recurring availablity (Weekly/DayOfWeek),
|
||||
/// not specific date availability. Therefore, updating availability for a specific
|
||||
/// date will update the availability for that Day of Week globally (Recurring).
|
||||
class AvailabilityRepositoryImpl
|
||||
implements AvailabilityRepository {
|
||||
final dc.DataConnectService _service;
|
||||
/// Uses the unified REST API for all read/write operations.
|
||||
/// - `GET /staff/availability` to list availability for a date range.
|
||||
/// - `PUT /staff/availability` to update a single day.
|
||||
/// - `POST /staff/availability/quick-set` to apply a preset.
|
||||
class AvailabilityRepositoryImpl implements AvailabilityRepository {
|
||||
/// Creates an [AvailabilityRepositoryImpl].
|
||||
AvailabilityRepositoryImpl({required BaseApiService apiService})
|
||||
: _apiService = apiService;
|
||||
|
||||
AvailabilityRepositoryImpl() : _service = dc.DataConnectService.instance;
|
||||
/// The API service used for network requests.
|
||||
final BaseApiService _apiService;
|
||||
|
||||
@override
|
||||
Future<List<DayAvailability>> getAvailability(DateTime start, DateTime end) async {
|
||||
return _service.run(() async {
|
||||
final String staffId = await _service.getStaffId();
|
||||
|
||||
// 1. Fetch Weekly recurring availability
|
||||
final QueryResult<dc.ListStaffAvailabilitiesByStaffIdData, dc.ListStaffAvailabilitiesByStaffIdVariables> result =
|
||||
await _service.connector.listStaffAvailabilitiesByStaffId(staffId: staffId).limit(100).execute();
|
||||
Future<List<AvailabilityDay>> getAvailability(
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
) async {
|
||||
final String startDate = _toIsoDate(start);
|
||||
final String endDate = _toIsoDate(end);
|
||||
|
||||
final List<dc.ListStaffAvailabilitiesByStaffIdStaffAvailabilities> items = result.data.staffAvailabilities;
|
||||
final ApiResponse response = await _apiService.get(
|
||||
V2ApiEndpoints.staffAvailability,
|
||||
params: <String, dynamic>{
|
||||
'startDate': startDate,
|
||||
'endDate': endDate,
|
||||
},
|
||||
);
|
||||
|
||||
// 2. Map to lookup: DayOfWeek -> Map<SlotName, IsAvailable>
|
||||
final Map<dc.DayOfWeek, Map<dc.AvailabilitySlot, bool>> weeklyMap = {};
|
||||
|
||||
for (final item in items) {
|
||||
dc.DayOfWeek day;
|
||||
try {
|
||||
day = dc.DayOfWeek.values.byName(item.day.stringValue);
|
||||
} catch (_) {
|
||||
continue;
|
||||
}
|
||||
final Map<String, dynamic> body = response.data as Map<String, dynamic>;
|
||||
final List<dynamic> items = body['items'] as List<dynamic>;
|
||||
|
||||
dc.AvailabilitySlot slot;
|
||||
try {
|
||||
slot = dc.AvailabilitySlot.values.byName(item.slot.stringValue);
|
||||
} catch (_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool isAvailable = false;
|
||||
try {
|
||||
final dc.AvailabilityStatus status = dc.AvailabilityStatus.values.byName(item.status.stringValue);
|
||||
isAvailable = _statusToBool(status);
|
||||
} catch (_) {
|
||||
isAvailable = false;
|
||||
}
|
||||
|
||||
if (!weeklyMap.containsKey(day)) {
|
||||
weeklyMap[day] = {};
|
||||
}
|
||||
weeklyMap[day]![slot] = isAvailable;
|
||||
}
|
||||
|
||||
// 3. Generate DayAvailability for requested range
|
||||
final List<DayAvailability> days = [];
|
||||
final int dayCount = end.difference(start).inDays;
|
||||
|
||||
for (int i = 0; i <= dayCount; i++) {
|
||||
final DateTime date = start.add(Duration(days: i));
|
||||
final dc.DayOfWeek dow = _toBackendDay(date.weekday);
|
||||
|
||||
final Map<dc.AvailabilitySlot, bool> daySlots = weeklyMap[dow] ?? {};
|
||||
|
||||
// We define 3 standard slots for every day
|
||||
final List<AvailabilitySlot> slots = [
|
||||
_createSlot(date, dow, daySlots, dc.AvailabilitySlot.MORNING),
|
||||
_createSlot(date, dow, daySlots, dc.AvailabilitySlot.AFTERNOON),
|
||||
_createSlot(date, dow, daySlots, dc.AvailabilitySlot.EVENING),
|
||||
];
|
||||
|
||||
final bool isDayAvailable = slots.any((s) => s.isAvailable);
|
||||
|
||||
days.add(DayAvailability(
|
||||
date: date,
|
||||
isAvailable: isDayAvailable,
|
||||
slots: slots,
|
||||
));
|
||||
}
|
||||
return days;
|
||||
});
|
||||
}
|
||||
|
||||
AvailabilitySlot _createSlot(
|
||||
DateTime date,
|
||||
dc.DayOfWeek dow,
|
||||
Map<dc.AvailabilitySlot, bool> existingSlots,
|
||||
dc.AvailabilitySlot slotEnum,
|
||||
) {
|
||||
final bool isAvailable = existingSlots[slotEnum] ?? false;
|
||||
return AvailabilityAdapter.fromPrimitive(slotEnum.name, isAvailable: isAvailable);
|
||||
return items
|
||||
.map((dynamic e) =>
|
||||
AvailabilityDay.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DayAvailability> updateDayAvailability(DayAvailability availability) async {
|
||||
return _service.run(() async {
|
||||
final String staffId = await _service.getStaffId();
|
||||
final dc.DayOfWeek dow = _toBackendDay(availability.date.weekday);
|
||||
Future<AvailabilityDay> updateDayAvailability({
|
||||
required int dayOfWeek,
|
||||
required AvailabilityStatus status,
|
||||
required List<TimeSlot> slots,
|
||||
}) async {
|
||||
final ApiResponse response = await _apiService.put(
|
||||
V2ApiEndpoints.staffAvailability,
|
||||
data: <String, dynamic>{
|
||||
'dayOfWeek': dayOfWeek,
|
||||
'availabilityStatus': status.toJson(),
|
||||
'slots': slots.map((TimeSlot s) => s.toJson()).toList(),
|
||||
},
|
||||
);
|
||||
|
||||
// Update each slot in the backend.
|
||||
// This updates the recurring rule for this DayOfWeek.
|
||||
for (final AvailabilitySlot slot in availability.slots) {
|
||||
final dc.AvailabilitySlot slotEnum = _toBackendSlot(slot.id);
|
||||
final dc.AvailabilityStatus status = _boolToStatus(slot.isAvailable);
|
||||
final Map<String, dynamic> body = response.data as Map<String, dynamic>;
|
||||
|
||||
await _upsertSlot(staffId, dow, slotEnum, status);
|
||||
}
|
||||
|
||||
return availability;
|
||||
});
|
||||
// The PUT response returns the updated day info.
|
||||
return AvailabilityDay(
|
||||
date: '',
|
||||
dayOfWeek: body['dayOfWeek'] as int,
|
||||
availabilityStatus:
|
||||
AvailabilityStatus.fromJson(body['availabilityStatus'] as String?),
|
||||
slots: _parseSlotsFromResponse(body['slots']),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<DayAvailability>> applyQuickSet(DateTime start, DateTime end, String type) async {
|
||||
return _service.run(() async {
|
||||
final String staffId = await _service.getStaffId();
|
||||
|
||||
// QuickSet updates the Recurring schedule for all days involved.
|
||||
// However, if the user selects a range that covers e.g. Mon-Fri, we update Mon-Fri.
|
||||
|
||||
final int dayCount = end.difference(start).inDays;
|
||||
final Set<dc.DayOfWeek> processedDays = {};
|
||||
final List<DayAvailability> resultDays = [];
|
||||
Future<void> applyQuickSet({
|
||||
required String quickSetType,
|
||||
required DateTime start,
|
||||
required DateTime end,
|
||||
List<TimeSlot>? slots,
|
||||
}) async {
|
||||
final Map<String, dynamic> data = <String, dynamic>{
|
||||
'quickSetType': quickSetType,
|
||||
'startDate': start.toUtc().toIso8601String(),
|
||||
'endDate': end.toUtc().toIso8601String(),
|
||||
};
|
||||
|
||||
final List<Future<void>> futures = [];
|
||||
|
||||
for (int i = 0; i <= dayCount; i++) {
|
||||
final DateTime date = start.add(Duration(days: i));
|
||||
final dc.DayOfWeek dow = _toBackendDay(date.weekday);
|
||||
|
||||
// Logic to determine if enabled based on type
|
||||
bool enableDay = false;
|
||||
if (type == 'all') {
|
||||
enableDay = true;
|
||||
} else if (type == 'clear') {
|
||||
enableDay = false;
|
||||
} else if (type == 'weekdays') {
|
||||
enableDay = (dow != dc.DayOfWeek.SATURDAY && dow != dc.DayOfWeek.SUNDAY);
|
||||
} else if (type == 'weekends') {
|
||||
enableDay = (dow == dc.DayOfWeek.SATURDAY || dow == dc.DayOfWeek.SUNDAY);
|
||||
}
|
||||
|
||||
// Only update backend once per DayOfWeek (since it's recurring)
|
||||
if (!processedDays.contains(dow)) {
|
||||
processedDays.add(dow);
|
||||
final dc.AvailabilityStatus status = _boolToStatus(enableDay);
|
||||
|
||||
futures.add(_upsertSlot(staffId, dow, dc.AvailabilitySlot.MORNING, status));
|
||||
futures.add(_upsertSlot(staffId, dow, dc.AvailabilitySlot.AFTERNOON, status));
|
||||
futures.add(_upsertSlot(staffId, dow, dc.AvailabilitySlot.EVENING, status));
|
||||
}
|
||||
|
||||
// Prepare return object
|
||||
final slots = [
|
||||
AvailabilityAdapter.fromPrimitive('MORNING', isAvailable: enableDay),
|
||||
AvailabilityAdapter.fromPrimitive('AFTERNOON', isAvailable: enableDay),
|
||||
AvailabilityAdapter.fromPrimitive('EVENING', isAvailable: enableDay),
|
||||
];
|
||||
|
||||
resultDays.add(DayAvailability(
|
||||
date: date,
|
||||
isAvailable: enableDay,
|
||||
slots: slots,
|
||||
));
|
||||
}
|
||||
|
||||
// Execute all updates in parallel
|
||||
await Future.wait(futures);
|
||||
|
||||
return resultDays;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _upsertSlot(String staffId, dc.DayOfWeek day, dc.AvailabilitySlot slot, dc.AvailabilityStatus status) async {
|
||||
// Check if exists
|
||||
final result = await _service.connector.getStaffAvailabilityByKey(
|
||||
staffId: staffId,
|
||||
day: day,
|
||||
slot: slot,
|
||||
).execute();
|
||||
|
||||
if (result.data.staffAvailability != null) {
|
||||
// Update
|
||||
await _service.connector.updateStaffAvailability(
|
||||
staffId: staffId,
|
||||
day: day,
|
||||
slot: slot,
|
||||
).status(status).execute();
|
||||
} else {
|
||||
// Create
|
||||
await _service.connector.createStaffAvailability(
|
||||
staffId: staffId,
|
||||
day: day,
|
||||
slot: slot,
|
||||
).status(status).execute();
|
||||
if (slots != null && slots.isNotEmpty) {
|
||||
data['slots'] = slots.map((TimeSlot s) => s.toJson()).toList();
|
||||
}
|
||||
|
||||
await _apiService.post(
|
||||
V2ApiEndpoints.staffAvailabilityQuickSet,
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
|
||||
// --- Private Helpers ---
|
||||
|
||||
dc.DayOfWeek _toBackendDay(int weekday) {
|
||||
switch (weekday) {
|
||||
case DateTime.monday: return dc.DayOfWeek.MONDAY;
|
||||
case DateTime.tuesday: return dc.DayOfWeek.TUESDAY;
|
||||
case DateTime.wednesday: return dc.DayOfWeek.WEDNESDAY;
|
||||
case DateTime.thursday: return dc.DayOfWeek.THURSDAY;
|
||||
case DateTime.friday: return dc.DayOfWeek.FRIDAY;
|
||||
case DateTime.saturday: return dc.DayOfWeek.SATURDAY;
|
||||
case DateTime.sunday: return dc.DayOfWeek.SUNDAY;
|
||||
default: return dc.DayOfWeek.MONDAY;
|
||||
}
|
||||
/// Formats a [DateTime] as `YYYY-MM-DD`.
|
||||
String _toIsoDate(DateTime date) {
|
||||
return '${date.year.toString().padLeft(4, '0')}-'
|
||||
'${date.month.toString().padLeft(2, '0')}-'
|
||||
'${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
dc.AvailabilitySlot _toBackendSlot(String id) {
|
||||
switch (id.toLowerCase()) {
|
||||
case 'morning': return dc.AvailabilitySlot.MORNING;
|
||||
case 'afternoon': return dc.AvailabilitySlot.AFTERNOON;
|
||||
case 'evening': return dc.AvailabilitySlot.EVENING;
|
||||
default: return dc.AvailabilitySlot.MORNING;
|
||||
}
|
||||
}
|
||||
|
||||
bool _statusToBool(dc.AvailabilityStatus status) {
|
||||
return status == dc.AvailabilityStatus.CONFIRMED_AVAILABLE;
|
||||
}
|
||||
|
||||
dc.AvailabilityStatus _boolToStatus(bool isAvailable) {
|
||||
return isAvailable ? dc.AvailabilityStatus.CONFIRMED_AVAILABLE : dc.AvailabilityStatus.BLOCKED;
|
||||
/// Safely parses a dynamic slots value into [TimeSlot] list.
|
||||
List<TimeSlot> _parseSlotsFromResponse(dynamic rawSlots) {
|
||||
if (rawSlots is! List<dynamic>) return <TimeSlot>[];
|
||||
return rawSlots
|
||||
.map((dynamic e) => TimeSlot.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Contract for fetching and updating staff availability.
|
||||
abstract class AvailabilityRepository {
|
||||
/// Fetches availability for a given date range (usually a week).
|
||||
Future<List<DayAvailability>> getAvailability(DateTime start, DateTime end);
|
||||
Future<List<AvailabilityDay>> 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);
|
||||
/// Updates the availability for a specific day of the week.
|
||||
Future<AvailabilityDay> updateDayAvailability({
|
||||
required int dayOfWeek,
|
||||
required AvailabilityStatus status,
|
||||
required List<TimeSlot> slots,
|
||||
});
|
||||
|
||||
/// Applies a preset configuration (e.g. "all", "weekdays") to the week.
|
||||
Future<void> applyQuickSet({
|
||||
required String quickSetType,
|
||||
required DateTime start,
|
||||
required DateTime end,
|
||||
List<TimeSlot> slots,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,28 +1,38 @@
|
||||
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;
|
||||
import 'package:staff_availability/src/domain/repositories/availability_repository.dart';
|
||||
|
||||
/// Use case to apply a quick-set availability pattern to the current week.
|
||||
///
|
||||
/// Supported types: `all`, `weekdays`, `weekends`, `clear`.
|
||||
class ApplyQuickSetUseCase extends UseCase<ApplyQuickSetParams, void> {
|
||||
/// Creates an [ApplyQuickSetUseCase].
|
||||
ApplyQuickSetUseCase(this.repository);
|
||||
|
||||
/// [type] can be 'all', 'weekdays', 'weekends', 'clear'
|
||||
/// The availability repository.
|
||||
final AvailabilityRepository repository;
|
||||
|
||||
@override
|
||||
Future<List<DayAvailability>> call(ApplyQuickSetParams params) {
|
||||
final end = params.start.add(const Duration(days: 6));
|
||||
return repository.applyQuickSet(params.start, end, params.type);
|
||||
Future<void> call(ApplyQuickSetParams params) {
|
||||
final DateTime end = params.start.add(const Duration(days: 6));
|
||||
return repository.applyQuickSet(
|
||||
quickSetType: params.type,
|
||||
start: params.start,
|
||||
end: end,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters for [ApplyQuickSetUseCase].
|
||||
class ApplyQuickSetParams extends UseCaseArgument {
|
||||
final DateTime start;
|
||||
final String type;
|
||||
|
||||
/// Creates [ApplyQuickSetParams].
|
||||
const ApplyQuickSetParams(this.start, this.type);
|
||||
|
||||
/// The Monday of the target week.
|
||||
final DateTime start;
|
||||
|
||||
/// Quick-set type: `all`, `weekdays`, `weekends`, or `clear`.
|
||||
final String type;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [start, type];
|
||||
List<Object?> get props => <Object?>[start, type];
|
||||
}
|
||||
|
||||
@@ -1,30 +1,36 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/availability_repository.dart';
|
||||
import 'package:staff_availability/src/domain/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;
|
||||
|
||||
///
|
||||
/// Calculates the week range from the given start date and delegates
|
||||
/// to the repository.
|
||||
class GetWeeklyAvailabilityUseCase
|
||||
extends UseCase<GetWeeklyAvailabilityParams, List<AvailabilityDay>> {
|
||||
/// Creates a [GetWeeklyAvailabilityUseCase].
|
||||
GetWeeklyAvailabilityUseCase(this.repository);
|
||||
|
||||
/// The availability repository.
|
||||
final AvailabilityRepository 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));
|
||||
Future<List<AvailabilityDay>> call(
|
||||
GetWeeklyAvailabilityParams params,
|
||||
) async {
|
||||
final DateTime end = params.start.add(const Duration(days: 6));
|
||||
return repository.getAvailability(params.start, end);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters for [GetWeeklyAvailabilityUseCase].
|
||||
class GetWeeklyAvailabilityParams extends UseCaseArgument {
|
||||
final DateTime start;
|
||||
|
||||
/// Creates [GetWeeklyAvailabilityParams].
|
||||
const GetWeeklyAvailabilityParams(this.start);
|
||||
|
||||
/// The Monday of the target week.
|
||||
final DateTime start;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [start];
|
||||
List<Object?> get props => <Object?>[start];
|
||||
}
|
||||
|
||||
@@ -1,25 +1,44 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/availability_repository.dart';
|
||||
import 'package:staff_availability/src/domain/repositories/availability_repository.dart';
|
||||
|
||||
/// Use case to update the availability configuration for a specific day.
|
||||
class UpdateDayAvailabilityUseCase extends UseCase<UpdateDayAvailabilityParams, DayAvailability> {
|
||||
final AvailabilityRepository repository;
|
||||
|
||||
class UpdateDayAvailabilityUseCase
|
||||
extends UseCase<UpdateDayAvailabilityParams, AvailabilityDay> {
|
||||
/// Creates an [UpdateDayAvailabilityUseCase].
|
||||
UpdateDayAvailabilityUseCase(this.repository);
|
||||
|
||||
/// The availability repository.
|
||||
final AvailabilityRepository repository;
|
||||
|
||||
@override
|
||||
Future<DayAvailability> call(UpdateDayAvailabilityParams params) {
|
||||
return repository.updateDayAvailability(params.availability);
|
||||
Future<AvailabilityDay> call(UpdateDayAvailabilityParams params) {
|
||||
return repository.updateDayAvailability(
|
||||
dayOfWeek: params.dayOfWeek,
|
||||
status: params.status,
|
||||
slots: params.slots,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters for [UpdateDayAvailabilityUseCase].
|
||||
class UpdateDayAvailabilityParams extends UseCaseArgument {
|
||||
final DayAvailability availability;
|
||||
/// Creates [UpdateDayAvailabilityParams].
|
||||
const UpdateDayAvailabilityParams({
|
||||
required this.dayOfWeek,
|
||||
required this.status,
|
||||
required this.slots,
|
||||
});
|
||||
|
||||
const UpdateDayAvailabilityParams(this.availability);
|
||||
/// Day of week (0 = Sunday, 6 = Saturday).
|
||||
final int dayOfWeek;
|
||||
|
||||
/// New availability status.
|
||||
final AvailabilityStatus status;
|
||||
|
||||
/// Time slots for this day.
|
||||
final List<TimeSlot> slots;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [availability];
|
||||
List<Object?> get props => <Object?>[dayOfWeek, status, slots];
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.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 'package:krow_core/core.dart';
|
||||
import 'availability_event.dart';
|
||||
import 'availability_state.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:staff_availability/src/domain/usecases/apply_quick_set_usecase.dart';
|
||||
import 'package:staff_availability/src/domain/usecases/get_weekly_availability_usecase.dart';
|
||||
import 'package:staff_availability/src/domain/usecases/update_day_availability_usecase.dart';
|
||||
import 'package:staff_availability/src/presentation/blocs/availability_event.dart';
|
||||
import 'package:staff_availability/src/presentation/blocs/availability_state.dart';
|
||||
|
||||
/// Manages availability state for the staff availability page.
|
||||
///
|
||||
/// Coordinates loading, toggling, and quick-set operations through
|
||||
/// domain use cases.
|
||||
class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState>
|
||||
with BlocErrorHandler<AvailabilityState> {
|
||||
final GetWeeklyAvailabilityUseCase getWeeklyAvailability;
|
||||
final UpdateDayAvailabilityUseCase updateDayAvailability;
|
||||
final ApplyQuickSetUseCase applyQuickSet;
|
||||
|
||||
/// Creates an [AvailabilityBloc].
|
||||
AvailabilityBloc({
|
||||
required this.getWeeklyAvailability,
|
||||
required this.updateDayAvailability,
|
||||
@@ -25,6 +27,15 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState>
|
||||
on<PerformQuickSet>(_onPerformQuickSet);
|
||||
}
|
||||
|
||||
/// Use case for loading weekly availability.
|
||||
final GetWeeklyAvailabilityUseCase getWeeklyAvailability;
|
||||
|
||||
/// Use case for updating a single day.
|
||||
final UpdateDayAvailabilityUseCase updateDayAvailability;
|
||||
|
||||
/// Use case for applying a quick-set preset.
|
||||
final ApplyQuickSetUseCase applyQuickSet;
|
||||
|
||||
Future<void> _onLoadAvailability(
|
||||
LoadAvailability event,
|
||||
Emitter<AvailabilityState> emit,
|
||||
@@ -33,15 +44,18 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState>
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
final days = await getWeeklyAvailability(
|
||||
final List<AvailabilityDay> days = await getWeeklyAvailability(
|
||||
GetWeeklyAvailabilityParams(event.weekStart),
|
||||
);
|
||||
|
||||
// Determine selected date: preselected, or first day of the week.
|
||||
final DateTime selectedDate = event.preselectedDate ?? event.weekStart;
|
||||
|
||||
emit(
|
||||
AvailabilityLoaded(
|
||||
days: days,
|
||||
currentWeekStart: event.weekStart,
|
||||
selectedDate: event.preselectedDate ??
|
||||
(days.isNotEmpty ? days.first.date : DateTime.now()),
|
||||
selectedDate: selectedDate,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -51,7 +65,6 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState>
|
||||
|
||||
void _onSelectDate(SelectDate event, Emitter<AvailabilityState> emit) {
|
||||
if (state is AvailabilityLoaded) {
|
||||
// Clear success message on navigation
|
||||
emit(
|
||||
(state as AvailabilityLoaded).copyWith(
|
||||
selectedDate: event.date,
|
||||
@@ -66,19 +79,18 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState>
|
||||
Emitter<AvailabilityState> emit,
|
||||
) async {
|
||||
if (state is AvailabilityLoaded) {
|
||||
final currentState = state as AvailabilityLoaded;
|
||||
|
||||
// Clear message
|
||||
final AvailabilityLoaded currentState = state as AvailabilityLoaded;
|
||||
emit(currentState.copyWith(clearSuccessMessage: true));
|
||||
|
||||
final newWeekStart = currentState.currentWeekStart.add(
|
||||
final DateTime newWeekStart = currentState.currentWeekStart.add(
|
||||
Duration(days: event.direction * 7),
|
||||
);
|
||||
|
||||
final diff = currentState.selectedDate
|
||||
// Preserve the relative day offset when navigating.
|
||||
final int diff = currentState.selectedDate
|
||||
.difference(currentState.currentWeekStart)
|
||||
.inDays;
|
||||
final newSelectedDate = newWeekStart.add(Duration(days: diff));
|
||||
final DateTime newSelectedDate = newWeekStart.add(Duration(days: diff));
|
||||
|
||||
add(LoadAvailability(newWeekStart, preselectedDate: newSelectedDate));
|
||||
}
|
||||
@@ -89,14 +101,22 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState>
|
||||
Emitter<AvailabilityState> emit,
|
||||
) async {
|
||||
if (state is AvailabilityLoaded) {
|
||||
final currentState = state as AvailabilityLoaded;
|
||||
final AvailabilityLoaded 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();
|
||||
// Toggle: available -> unavailable, anything else -> available.
|
||||
final AvailabilityStatus newStatus = event.day.isAvailable
|
||||
? AvailabilityStatus.unavailable
|
||||
: AvailabilityStatus.available;
|
||||
|
||||
final AvailabilityDay newDay = event.day.copyWith(
|
||||
availabilityStatus: newStatus,
|
||||
);
|
||||
|
||||
// Optimistic update.
|
||||
final List<AvailabilityDay> updatedDays = currentState.days
|
||||
.map((AvailabilityDay d) => d.date == event.day.date ? newDay : d)
|
||||
.toList();
|
||||
|
||||
// Optimistic update
|
||||
emit(currentState.copyWith(
|
||||
days: updatedDays,
|
||||
clearSuccessMessage: true,
|
||||
@@ -105,8 +125,13 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState>
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
await updateDayAvailability(UpdateDayAvailabilityParams(newDay));
|
||||
// Success feedback
|
||||
await updateDayAvailability(
|
||||
UpdateDayAvailabilityParams(
|
||||
dayOfWeek: newDay.dayOfWeek,
|
||||
status: newStatus,
|
||||
slots: newDay.slots,
|
||||
),
|
||||
);
|
||||
if (state is AvailabilityLoaded) {
|
||||
emit(
|
||||
(state as AvailabilityLoaded).copyWith(
|
||||
@@ -116,7 +141,7 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState>
|
||||
}
|
||||
},
|
||||
onError: (String errorKey) {
|
||||
// Revert
|
||||
// Revert on failure.
|
||||
if (state is AvailabilityLoaded) {
|
||||
return (state as AvailabilityLoaded).copyWith(
|
||||
days: currentState.days,
|
||||
@@ -133,22 +158,41 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState>
|
||||
Emitter<AvailabilityState> emit,
|
||||
) async {
|
||||
if (state is AvailabilityLoaded) {
|
||||
final currentState = state as AvailabilityLoaded;
|
||||
final AvailabilityLoaded 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();
|
||||
// Remove the slot at the given index to toggle it off,
|
||||
// or re-add if already removed. For V2, toggling a slot means
|
||||
// removing it from the list (unavailable) or the day remains
|
||||
// with the remaining slots.
|
||||
// For simplicity, we toggle the overall day status instead of
|
||||
// individual slot removal since the V2 API sends full slot arrays.
|
||||
|
||||
final newDay = event.day.copyWith(slots: updatedSlots);
|
||||
// Build a new slots list by removing or keeping the target slot.
|
||||
final List<TimeSlot> currentSlots =
|
||||
List<TimeSlot>.from(event.day.slots);
|
||||
|
||||
final updatedDays = currentState.days.map((d) {
|
||||
return d.date == event.day.date ? newDay : d;
|
||||
}).toList();
|
||||
// If there's only one slot and we remove it, day becomes unavailable.
|
||||
// If there are multiple, remove the indexed one.
|
||||
if (event.slotIndex >= 0 && event.slotIndex < currentSlots.length) {
|
||||
currentSlots.removeAt(event.slotIndex);
|
||||
}
|
||||
|
||||
// Optimistic update
|
||||
final AvailabilityStatus newStatus = currentSlots.isEmpty
|
||||
? AvailabilityStatus.unavailable
|
||||
: (currentSlots.length < event.day.slots.length
|
||||
? AvailabilityStatus.partial
|
||||
: event.day.availabilityStatus);
|
||||
|
||||
final AvailabilityDay newDay = event.day.copyWith(
|
||||
availabilityStatus: newStatus,
|
||||
slots: currentSlots,
|
||||
);
|
||||
|
||||
final List<AvailabilityDay> updatedDays = currentState.days
|
||||
.map((AvailabilityDay d) => d.date == event.day.date ? newDay : d)
|
||||
.toList();
|
||||
|
||||
// Optimistic update.
|
||||
emit(currentState.copyWith(
|
||||
days: updatedDays,
|
||||
clearSuccessMessage: true,
|
||||
@@ -157,8 +201,13 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState>
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
await updateDayAvailability(UpdateDayAvailabilityParams(newDay));
|
||||
// Success feedback
|
||||
await updateDayAvailability(
|
||||
UpdateDayAvailabilityParams(
|
||||
dayOfWeek: newDay.dayOfWeek,
|
||||
status: newStatus,
|
||||
slots: currentSlots,
|
||||
),
|
||||
);
|
||||
if (state is AvailabilityLoaded) {
|
||||
emit(
|
||||
(state as AvailabilityLoaded).copyWith(
|
||||
@@ -168,7 +217,7 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState>
|
||||
}
|
||||
},
|
||||
onError: (String errorKey) {
|
||||
// Revert
|
||||
// Revert on failure.
|
||||
if (state is AvailabilityLoaded) {
|
||||
return (state as AvailabilityLoaded).copyWith(
|
||||
days: currentState.days,
|
||||
@@ -185,7 +234,7 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState>
|
||||
Emitter<AvailabilityState> emit,
|
||||
) async {
|
||||
if (state is AvailabilityLoaded) {
|
||||
final currentState = state as AvailabilityLoaded;
|
||||
final AvailabilityLoaded currentState = state as AvailabilityLoaded;
|
||||
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
@@ -197,13 +246,18 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState>
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
final newDays = await applyQuickSet(
|
||||
await applyQuickSet(
|
||||
ApplyQuickSetParams(currentState.currentWeekStart, event.type),
|
||||
);
|
||||
|
||||
// Reload the week to get updated data from the server.
|
||||
final List<AvailabilityDay> refreshed = await getWeeklyAvailability(
|
||||
GetWeeklyAvailabilityParams(currentState.currentWeekStart),
|
||||
);
|
||||
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
days: newDays,
|
||||
days: refreshed,
|
||||
isActionInProgress: false,
|
||||
successMessage: 'Availability updated',
|
||||
),
|
||||
@@ -221,4 +275,3 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
// --- State ---
|
||||
class AvailabilityState extends Equatable {
|
||||
final DateTime currentWeekStart;
|
||||
final DateTime selectedDate;
|
||||
final Map<String, bool> dayAvailability;
|
||||
final Map<String, Map<String, bool>> timeSlotAvailability;
|
||||
|
||||
const AvailabilityState({
|
||||
required this.currentWeekStart,
|
||||
required this.selectedDate,
|
||||
required this.dayAvailability,
|
||||
required this.timeSlotAvailability,
|
||||
});
|
||||
|
||||
AvailabilityState copyWith({
|
||||
DateTime? currentWeekStart,
|
||||
DateTime? selectedDate,
|
||||
Map<String, bool>? dayAvailability,
|
||||
Map<String, Map<String, bool>>? timeSlotAvailability,
|
||||
}) {
|
||||
return AvailabilityState(
|
||||
currentWeekStart: currentWeekStart ?? this.currentWeekStart,
|
||||
selectedDate: selectedDate ?? this.selectedDate,
|
||||
dayAvailability: dayAvailability ?? this.dayAvailability,
|
||||
timeSlotAvailability: timeSlotAvailability ?? this.timeSlotAvailability,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object> get props => [
|
||||
currentWeekStart,
|
||||
selectedDate,
|
||||
dayAvailability,
|
||||
timeSlotAvailability,
|
||||
];
|
||||
}
|
||||
|
||||
// --- Cubit ---
|
||||
class AvailabilityCubit extends Cubit<AvailabilityState> {
|
||||
AvailabilityCubit()
|
||||
: super(AvailabilityState(
|
||||
currentWeekStart: _getStartOfWeek(DateTime.now()),
|
||||
selectedDate: DateTime.now(),
|
||||
dayAvailability: {
|
||||
'monday': true,
|
||||
'tuesday': true,
|
||||
'wednesday': true,
|
||||
'thursday': true,
|
||||
'friday': true,
|
||||
'saturday': false,
|
||||
'sunday': false,
|
||||
},
|
||||
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},
|
||||
},
|
||||
));
|
||||
|
||||
static DateTime _getStartOfWeek(DateTime date) {
|
||||
final diff = date.weekday - 1; // Mon=1 -> 0
|
||||
final start = date.subtract(Duration(days: diff));
|
||||
return DateTime(start.year, start.month, start.day);
|
||||
}
|
||||
|
||||
void selectDate(DateTime date) {
|
||||
emit(state.copyWith(selectedDate: date));
|
||||
}
|
||||
|
||||
void navigateWeek(int weeks) {
|
||||
emit(state.copyWith(
|
||||
currentWeekStart: state.currentWeekStart.add(Duration(days: weeks * 7)),
|
||||
));
|
||||
}
|
||||
|
||||
void toggleDay(String dayKey) {
|
||||
final currentObj = Map<String, bool>.from(state.dayAvailability);
|
||||
currentObj[dayKey] = !(currentObj[dayKey] ?? false);
|
||||
emit(state.copyWith(dayAvailability: currentObj));
|
||||
}
|
||||
|
||||
void toggleSlot(String dayKey, String slotId) {
|
||||
final allSlots = Map<String, Map<String, bool>>.from(state.timeSlotAvailability);
|
||||
final daySlots = Map<String, bool>.from(allSlots[dayKey] ?? {});
|
||||
|
||||
// Default to true if missing, so we toggle to false
|
||||
final currentVal = daySlots[slotId] ?? true;
|
||||
daySlots[slotId] = !currentVal;
|
||||
|
||||
allSlots[dayKey] = daySlots;
|
||||
emit(state.copyWith(timeSlotAvailability: allSlots));
|
||||
}
|
||||
|
||||
void quickSet(String type) {
|
||||
final newAvailability = <String, bool>{};
|
||||
final days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
|
||||
switch (type) {
|
||||
case 'all':
|
||||
for (var d in days) {
|
||||
newAvailability[d] = true;
|
||||
}
|
||||
break;
|
||||
case 'weekdays':
|
||||
for (var d in days) {
|
||||
newAvailability[d] = (d != 'saturday' && d != 'sunday');
|
||||
}
|
||||
break;
|
||||
case 'weekends':
|
||||
for (var d in days) {
|
||||
newAvailability[d] = (d == 'saturday' || d == 'sunday');
|
||||
}
|
||||
break;
|
||||
case 'clear':
|
||||
for (var d in days) {
|
||||
newAvailability[d] = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
emit(state.copyWith(dayAvailability: newAvailability));
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,89 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Base class for availability events.
|
||||
abstract class AvailabilityEvent extends Equatable {
|
||||
/// Creates an [AvailabilityEvent].
|
||||
const AvailabilityEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
/// Requests loading availability for a given week.
|
||||
class LoadAvailability extends AvailabilityEvent {
|
||||
final DateTime weekStart;
|
||||
final DateTime? preselectedDate; // Maintain selection after reload
|
||||
|
||||
/// Creates a [LoadAvailability] event.
|
||||
const LoadAvailability(this.weekStart, {this.preselectedDate});
|
||||
|
||||
|
||||
/// The Monday of the week to load.
|
||||
final DateTime weekStart;
|
||||
|
||||
/// Optional date to pre-select after loading.
|
||||
final DateTime? preselectedDate;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [weekStart, preselectedDate];
|
||||
List<Object?> get props => <Object?>[weekStart, preselectedDate];
|
||||
}
|
||||
|
||||
/// User selected a date in the week strip.
|
||||
class SelectDate extends AvailabilityEvent {
|
||||
final DateTime date;
|
||||
/// Creates a [SelectDate] event.
|
||||
const SelectDate(this.date);
|
||||
|
||||
/// The selected date.
|
||||
final DateTime date;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [date];
|
||||
List<Object?> get props => <Object?>[date];
|
||||
}
|
||||
|
||||
/// Toggles the overall availability status of a day.
|
||||
class ToggleDayStatus extends AvailabilityEvent {
|
||||
final DayAvailability day;
|
||||
/// Creates a [ToggleDayStatus] event.
|
||||
const ToggleDayStatus(this.day);
|
||||
|
||||
/// The day to toggle.
|
||||
final AvailabilityDay day;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [day];
|
||||
List<Object?> get props => <Object?>[day];
|
||||
}
|
||||
|
||||
/// Toggles an individual time slot within a day.
|
||||
class ToggleSlotStatus extends AvailabilityEvent {
|
||||
final DayAvailability day;
|
||||
final String slotId;
|
||||
const ToggleSlotStatus(this.day, this.slotId);
|
||||
/// Creates a [ToggleSlotStatus] event.
|
||||
const ToggleSlotStatus(this.day, this.slotIndex);
|
||||
|
||||
/// The parent day.
|
||||
final AvailabilityDay day;
|
||||
|
||||
/// Index of the slot to toggle within [day.slots].
|
||||
final int slotIndex;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [day, slotId];
|
||||
List<Object?> get props => <Object?>[day, slotIndex];
|
||||
}
|
||||
|
||||
/// Navigates forward or backward by one week.
|
||||
class NavigateWeek extends AvailabilityEvent {
|
||||
final int direction; // -1 or 1
|
||||
/// Creates a [NavigateWeek] event.
|
||||
const NavigateWeek(this.direction);
|
||||
|
||||
/// -1 for previous week, 1 for next week.
|
||||
final int direction;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [direction];
|
||||
List<Object?> get props => <Object?>[direction];
|
||||
}
|
||||
|
||||
/// Applies a quick-set preset to the current week.
|
||||
class PerformQuickSet extends AvailabilityEvent {
|
||||
final String type; // all, weekdays, weekends, clear
|
||||
/// Creates a [PerformQuickSet] event.
|
||||
const PerformQuickSet(this.type);
|
||||
|
||||
/// One of: `all`, `weekdays`, `weekends`, `clear`.
|
||||
final String type;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [type];
|
||||
List<Object?> get props => <Object?>[type];
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Base class for availability states.
|
||||
abstract class AvailabilityState extends Equatable {
|
||||
/// Creates an [AvailabilityState].
|
||||
const AvailabilityState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
/// Initial state before any data is loaded.
|
||||
class AvailabilityInitial extends AvailabilityState {}
|
||||
|
||||
/// Loading state while fetching availability data.
|
||||
class AvailabilityLoading extends AvailabilityState {}
|
||||
|
||||
/// State when availability data has been loaded.
|
||||
class AvailabilityLoaded extends AvailabilityState {
|
||||
final List<DayAvailability> days;
|
||||
final DateTime currentWeekStart;
|
||||
final DateTime selectedDate;
|
||||
final bool isActionInProgress;
|
||||
final String? successMessage;
|
||||
|
||||
/// Creates an [AvailabilityLoaded] state.
|
||||
const AvailabilityLoaded({
|
||||
required this.days,
|
||||
required this.currentWeekStart,
|
||||
@@ -26,20 +27,41 @@ class AvailabilityLoaded extends AvailabilityState {
|
||||
this.successMessage,
|
||||
});
|
||||
|
||||
/// Helper to get the currently selected day's availability object
|
||||
DayAvailability get selectedDayAvailability {
|
||||
/// The list of daily availability entries for the current week.
|
||||
final List<AvailabilityDay> days;
|
||||
|
||||
/// The Monday of the currently displayed week.
|
||||
final DateTime currentWeekStart;
|
||||
|
||||
/// The currently selected date in the week strip.
|
||||
final DateTime selectedDate;
|
||||
|
||||
/// Whether a background action (update/quick-set) is in progress.
|
||||
final bool isActionInProgress;
|
||||
|
||||
/// Optional success message for snackbar feedback.
|
||||
final String? successMessage;
|
||||
|
||||
/// The [AvailabilityDay] matching the current [selectedDate].
|
||||
AvailabilityDay get selectedDayAvailability {
|
||||
final String selectedIso = _toIsoDate(selectedDate);
|
||||
return days.firstWhere(
|
||||
(d) => isSameDay(d.date, selectedDate),
|
||||
orElse: () => DayAvailability(date: selectedDate), // Fallback
|
||||
(AvailabilityDay d) => d.date == selectedIso,
|
||||
orElse: () => AvailabilityDay(
|
||||
date: selectedIso,
|
||||
dayOfWeek: selectedDate.weekday % 7,
|
||||
availabilityStatus: AvailabilityStatus.unavailable,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a copy with optionally replaced fields.
|
||||
AvailabilityLoaded copyWith({
|
||||
List<DayAvailability>? days,
|
||||
List<AvailabilityDay>? days,
|
||||
DateTime? currentWeekStart,
|
||||
DateTime? selectedDate,
|
||||
bool? isActionInProgress,
|
||||
String? successMessage, // Nullable override
|
||||
String? successMessage,
|
||||
bool clearSuccessMessage = false,
|
||||
}) {
|
||||
return AvailabilityLoaded(
|
||||
@@ -47,21 +69,41 @@ class AvailabilityLoaded extends AvailabilityState {
|
||||
currentWeekStart: currentWeekStart ?? this.currentWeekStart,
|
||||
selectedDate: selectedDate ?? this.selectedDate,
|
||||
isActionInProgress: isActionInProgress ?? this.isActionInProgress,
|
||||
successMessage: clearSuccessMessage ? null : (successMessage ?? this.successMessage),
|
||||
successMessage:
|
||||
clearSuccessMessage ? null : (successMessage ?? this.successMessage),
|
||||
);
|
||||
}
|
||||
|
||||
/// Checks whether two [DateTime]s represent the same calendar day.
|
||||
static bool isSameDay(DateTime a, DateTime b) {
|
||||
return a.year == b.year && a.month == b.month && a.day == b.day;
|
||||
}
|
||||
|
||||
/// Formats a [DateTime] as `YYYY-MM-DD`.
|
||||
static String _toIsoDate(DateTime date) {
|
||||
return '${date.year.toString().padLeft(4, '0')}-'
|
||||
'${date.month.toString().padLeft(2, '0')}-'
|
||||
'${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [days, currentWeekStart, selectedDate, isActionInProgress, successMessage];
|
||||
List<Object?> get props => <Object?>[
|
||||
days,
|
||||
currentWeekStart,
|
||||
selectedDate,
|
||||
isActionInProgress,
|
||||
successMessage,
|
||||
];
|
||||
}
|
||||
|
||||
/// Error state when availability loading or an action fails.
|
||||
class AvailabilityError extends AvailabilityState {
|
||||
final String message;
|
||||
/// Creates an [AvailabilityError] state.
|
||||
const AvailabilityError(this.message);
|
||||
|
||||
/// Error key for localisation.
|
||||
final String message;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
List<Object?> get props => <Object?>[message];
|
||||
}
|
||||
|
||||
@@ -6,13 +6,14 @@ import 'package:flutter_modular/flutter_modular.dart'
|
||||
hide ModularWatchExtension;
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:staff_availability/src/presentation/blocs/availability_bloc.dart';
|
||||
import 'package:staff_availability/src/presentation/blocs/availability_event.dart';
|
||||
import 'package:staff_availability/src/presentation/blocs/availability_state.dart';
|
||||
import 'package:staff_availability/src/presentation/widgets/availability_page_skeleton/availability_page_skeleton.dart';
|
||||
|
||||
import '../blocs/availability_bloc.dart';
|
||||
import '../blocs/availability_event.dart';
|
||||
import '../blocs/availability_state.dart';
|
||||
import '../widgets/availability_page_skeleton/availability_page_skeleton.dart';
|
||||
|
||||
/// Page for managing staff weekly availability.
|
||||
class AvailabilityPage extends StatefulWidget {
|
||||
/// Creates an [AvailabilityPage].
|
||||
const AvailabilityPage({super.key});
|
||||
|
||||
@override
|
||||
@@ -28,10 +29,10 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
_calculateInitialWeek();
|
||||
}
|
||||
|
||||
/// Computes the Monday of the current week and triggers initial load.
|
||||
void _calculateInitialWeek() {
|
||||
final today = DateTime.now();
|
||||
final day = today.weekday; // Mon=1, Sun=7
|
||||
final diff = day - 1; // Assuming Monday start
|
||||
final DateTime today = DateTime.now();
|
||||
final int diff = today.weekday - 1;
|
||||
DateTime currentWeekStart = today.subtract(Duration(days: diff));
|
||||
currentWeekStart = DateTime(
|
||||
currentWeekStart.year,
|
||||
@@ -43,25 +44,25 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = Translations.of(context).staff.availability;
|
||||
return BlocProvider.value(
|
||||
final dynamic i18n = Translations.of(context).staff.availability;
|
||||
return BlocProvider<AvailabilityBloc>.value(
|
||||
value: _bloc,
|
||||
child: Scaffold(
|
||||
appBar: UiAppBar(
|
||||
title: i18n.title,
|
||||
title: i18n.title as String,
|
||||
centerTitle: false,
|
||||
showBackButton: true,
|
||||
),
|
||||
body: BlocListener<AvailabilityBloc, AvailabilityState>(
|
||||
listener: (context, state) {
|
||||
if (state is AvailabilityLoaded && state.successMessage != null) {
|
||||
listener: (BuildContext context, AvailabilityState state) {
|
||||
if (state is AvailabilityLoaded &&
|
||||
state.successMessage != null) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: state.successMessage!,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
}
|
||||
|
||||
if (state is AvailabilityError) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
@@ -71,59 +72,19 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
}
|
||||
},
|
||||
child: BlocBuilder<AvailabilityBloc, AvailabilityState>(
|
||||
builder: (context, state) {
|
||||
builder: (BuildContext context, AvailabilityState state) {
|
||||
if (state is AvailabilityLoading) {
|
||||
return const AvailabilityPageSkeleton();
|
||||
} else if (state is AvailabilityLoaded) {
|
||||
return Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 100),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
spacing: UiConstants.space6,
|
||||
children: [
|
||||
_buildQuickSet(context),
|
||||
_buildWeekNavigation(context, state),
|
||||
_buildSelectedDayAvailability(
|
||||
context,
|
||||
state.selectedDayAvailability,
|
||||
),
|
||||
_buildInfoCard(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (state.isActionInProgress)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: UiColors.white.withValues(alpha: 0.5),
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
return _buildLoaded(context, state);
|
||||
} else if (state is AvailabilityError) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
translateErrorKey(state.message),
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
],
|
||||
child: Text(
|
||||
translateErrorKey(state.message),
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -136,8 +97,48 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoaded(BuildContext context, AvailabilityLoaded state) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 100),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
_buildQuickSet(context),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
_buildWeekNavigation(context, state),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
_buildSelectedDayAvailability(
|
||||
context,
|
||||
state.selectedDayAvailability,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
_buildInfoCard(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state.isActionInProgress)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: UiColors.white.withValues(alpha: 0.5),
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ── Quick Set Section ─────────────────────────────────────────────────
|
||||
|
||||
Widget _buildQuickSet(BuildContext context) {
|
||||
final i18n = Translations.of(context).staff.availability;
|
||||
final dynamic i18n = Translations.of(context).staff.availability;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
@@ -146,30 +147,39 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
i18n.quick_set_title,
|
||||
style: UiTypography.body2b,
|
||||
),
|
||||
children: <Widget>[
|
||||
Text(i18n.quick_set_title as String, style: UiTypography.body2b),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: _buildQuickSetButton(context, i18n.all_week, 'all'),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: _buildQuickSetButton(context, i18n.weekdays, 'weekdays'),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: _buildQuickSetButton(context, i18n.weekends, 'weekends'),
|
||||
child: _buildQuickSetButton(
|
||||
context,
|
||||
i18n.all_week as String,
|
||||
'all',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: _buildQuickSetButton(
|
||||
context,
|
||||
i18n.clear_all,
|
||||
i18n.weekdays as String,
|
||||
'weekdays',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: _buildQuickSetButton(
|
||||
context,
|
||||
i18n.weekends as String,
|
||||
'weekends',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: _buildQuickSetButton(
|
||||
context,
|
||||
i18n.clear_all as String,
|
||||
'clear',
|
||||
isDestructive: true,
|
||||
),
|
||||
@@ -203,9 +213,8 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
foregroundColor: isDestructive
|
||||
? UiColors.destructive
|
||||
: UiColors.primary,
|
||||
foregroundColor:
|
||||
isDestructive ? UiColors.destructive : UiColors.primary,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
@@ -217,10 +226,15 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
// ── Week Navigation ───────────────────────────────────────────────────
|
||||
|
||||
Widget _buildWeekNavigation(
|
||||
BuildContext context,
|
||||
AvailabilityLoaded state,
|
||||
) {
|
||||
final DateTime middleDate =
|
||||
state.currentWeekStart.add(const Duration(days: 3));
|
||||
final String monthYear = DateFormat('MMMM yyyy').format(middleDate);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
@@ -230,37 +244,33 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Nav Header
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
_buildNavButton(
|
||||
UiIcons.chevronLeft,
|
||||
() => context.read<AvailabilityBloc>().add(
|
||||
const NavigateWeek(-1),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
monthYear,
|
||||
style: UiTypography.title2b,
|
||||
const NavigateWeek(-1),
|
||||
),
|
||||
),
|
||||
Text(monthYear, style: UiTypography.title2b),
|
||||
_buildNavButton(
|
||||
UiIcons.chevronRight,
|
||||
() => context.read<AvailabilityBloc>().add(
|
||||
const NavigateWeek(1),
|
||||
),
|
||||
const NavigateWeek(1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Days Row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: state.days
|
||||
.map((day) => _buildDayItem(context, day, state.selectedDate))
|
||||
.map((AvailabilityDay day) =>
|
||||
_buildDayItem(context, day, state.selectedDate))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
@@ -285,16 +295,19 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
|
||||
Widget _buildDayItem(
|
||||
BuildContext context,
|
||||
DayAvailability day,
|
||||
AvailabilityDay day,
|
||||
DateTime selectedDate,
|
||||
) {
|
||||
final isSelected = AvailabilityLoaded.isSameDay(day.date, selectedDate);
|
||||
final isAvailable = day.isAvailable;
|
||||
final isToday = AvailabilityLoaded.isSameDay(day.date, DateTime.now());
|
||||
final DateTime dayDate = DateTime.parse(day.date);
|
||||
final bool isSelected = AvailabilityLoaded.isSameDay(dayDate, selectedDate);
|
||||
final bool isAvailable = day.isAvailable;
|
||||
final bool isToday =
|
||||
AvailabilityLoaded.isSameDay(dayDate, DateTime.now());
|
||||
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => context.read<AvailabilityBloc>().add(SelectDate(day.date)),
|
||||
onTap: () =>
|
||||
context.read<AvailabilityBloc>().add(SelectDate(dayDate)),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
padding: const EdgeInsets.symmetric(vertical: UiConstants.space3),
|
||||
@@ -314,11 +327,11 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Text(
|
||||
day.date.day.toString().padLeft(2, '0'),
|
||||
dayDate.day.toString().padLeft(2, '0'),
|
||||
style: UiTypography.title1m.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isSelected
|
||||
@@ -330,7 +343,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
DateFormat('EEE').format(day.date),
|
||||
DateFormat('EEE').format(dayDate),
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color: isSelected
|
||||
? UiColors.white.withValues(alpha: 0.8)
|
||||
@@ -360,12 +373,15 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Selected Day Detail ───────────────────────────────────────────────
|
||||
|
||||
Widget _buildSelectedDayAvailability(
|
||||
BuildContext context,
|
||||
DayAvailability day,
|
||||
AvailabilityDay day,
|
||||
) {
|
||||
final dateStr = DateFormat('EEEE, MMM d').format(day.date);
|
||||
final isAvailable = day.isAvailable;
|
||||
final DateTime dayDate = DateTime.parse(day.date);
|
||||
final String dateStr = DateFormat('EEEE, MMM d').format(dayDate);
|
||||
final bool isAvailable = day.isAvailable;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
@@ -375,18 +391,14 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Header Row
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
dateStr,
|
||||
style: UiTypography.title2b,
|
||||
),
|
||||
children: <Widget>[
|
||||
Text(dateStr, style: UiTypography.title2b),
|
||||
Text(
|
||||
isAvailable
|
||||
? Translations.of(context)
|
||||
@@ -403,94 +415,54 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
),
|
||||
Switch(
|
||||
value: isAvailable,
|
||||
onChanged: (val) =>
|
||||
context.read<AvailabilityBloc>().add(ToggleDayStatus(day)),
|
||||
onChanged: (bool val) => context
|
||||
.read<AvailabilityBloc>()
|
||||
.add(ToggleDayStatus(day)),
|
||||
activeThumbColor: UiColors.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// 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);
|
||||
...day.slots.asMap().entries.map((MapEntry<int, TimeSlot> entry) {
|
||||
final int index = entry.key;
|
||||
final TimeSlot slot = entry.value;
|
||||
return _buildTimeSlotItem(context, day, slot, index);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getSlotUiConfig(String slotId) {
|
||||
switch (slotId) {
|
||||
case 'morning':
|
||||
return {
|
||||
'icon': UiIcons.sunrise,
|
||||
'bg': UiColors.primary.withValues(alpha: 0.1),
|
||||
'iconColor': UiColors.primary,
|
||||
};
|
||||
case 'afternoon':
|
||||
return {
|
||||
'icon': UiIcons.sun,
|
||||
'bg': UiColors.primary.withValues(alpha: 0.2),
|
||||
'iconColor': UiColors.primary,
|
||||
};
|
||||
case 'evening':
|
||||
return {
|
||||
'icon': UiIcons.moon,
|
||||
'bg': UiColors.bgSecondary,
|
||||
'iconColor': UiColors.foreground,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
'icon': UiIcons.clock,
|
||||
'bg': UiColors.bgSecondary,
|
||||
'iconColor': UiColors.iconSecondary,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildTimeSlotItem(
|
||||
BuildContext context,
|
||||
DayAvailability day,
|
||||
AvailabilitySlot slot,
|
||||
Map<String, dynamic> uiConfig,
|
||||
AvailabilityDay day,
|
||||
TimeSlot slot,
|
||||
int index,
|
||||
) {
|
||||
// Determine styles based on state
|
||||
final isEnabled = day.isAvailable;
|
||||
final isActive = slot.isAvailable;
|
||||
final bool isEnabled = day.isAvailable;
|
||||
final Map<String, dynamic> uiConfig = _getSlotUiConfig(slot);
|
||||
|
||||
// Container style
|
||||
Color bgColor;
|
||||
Color borderColor;
|
||||
|
||||
if (!isEnabled) {
|
||||
bgColor = UiColors.bgSecondary;
|
||||
borderColor = UiColors.borderInactive;
|
||||
} else if (isActive) {
|
||||
} else {
|
||||
bgColor = UiColors.primary.withValues(alpha: 0.05);
|
||||
borderColor = UiColors.primary.withValues(alpha: 0.2);
|
||||
} else {
|
||||
bgColor = UiColors.bgSecondary;
|
||||
borderColor = UiColors.borderPrimary;
|
||||
}
|
||||
|
||||
// Text colors
|
||||
final titleColor = (isEnabled && isActive)
|
||||
? UiColors.foreground
|
||||
: UiColors.mutedForeground;
|
||||
final subtitleColor = (isEnabled && isActive)
|
||||
? UiColors.mutedForeground
|
||||
: UiColors.textInactive;
|
||||
final Color titleColor =
|
||||
isEnabled ? UiColors.foreground : UiColors.mutedForeground;
|
||||
final Color subtitleColor =
|
||||
isEnabled ? UiColors.mutedForeground : UiColors.textInactive;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: isEnabled
|
||||
? () => context.read<AvailabilityBloc>().add(
|
||||
ToggleSlotStatus(day, slot.id),
|
||||
)
|
||||
? () => context
|
||||
.read<AvailabilityBloc>()
|
||||
.add(ToggleSlotStatus(day, index))
|
||||
: null,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
@@ -502,40 +474,38 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
border: Border.all(color: borderColor, width: 2),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Icon
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: uiConfig['bg'],
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
color: uiConfig['bg'] as Color,
|
||||
borderRadius:
|
||||
BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
child: Icon(
|
||||
uiConfig['icon'],
|
||||
color: uiConfig['iconColor'],
|
||||
uiConfig['icon'] as IconData,
|
||||
color: uiConfig['iconColor'] as Color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
// Text
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Text(
|
||||
slot.label,
|
||||
'${slot.startTime} - ${slot.endTime}',
|
||||
style: UiTypography.body2m.copyWith(color: titleColor),
|
||||
),
|
||||
Text(
|
||||
slot.timeRange,
|
||||
_slotPeriodLabel(slot),
|
||||
style: UiTypography.body3r.copyWith(color: subtitleColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Checkbox indicator
|
||||
if (isEnabled && isActive)
|
||||
if (isEnabled)
|
||||
Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
@@ -548,18 +518,6 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
size: 16,
|
||||
color: UiColors.white,
|
||||
),
|
||||
)
|
||||
else if (isEnabled && !isActive)
|
||||
Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: UiColors.borderStill,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -567,8 +525,48 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns UI config (icon, bg, iconColor) based on time slot hours.
|
||||
Map<String, dynamic> _getSlotUiConfig(TimeSlot slot) {
|
||||
final int hour = _parseHour(slot.startTime);
|
||||
if (hour < 12) {
|
||||
return <String, dynamic>{
|
||||
'icon': UiIcons.sunrise,
|
||||
'bg': UiColors.primary.withValues(alpha: 0.1),
|
||||
'iconColor': UiColors.primary,
|
||||
};
|
||||
} else if (hour < 17) {
|
||||
return <String, dynamic>{
|
||||
'icon': UiIcons.sun,
|
||||
'bg': UiColors.primary.withValues(alpha: 0.2),
|
||||
'iconColor': UiColors.primary,
|
||||
};
|
||||
} else {
|
||||
return <String, dynamic>{
|
||||
'icon': UiIcons.moon,
|
||||
'bg': UiColors.bgSecondary,
|
||||
'iconColor': UiColors.foreground,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the hour from an `HH:MM` string.
|
||||
int _parseHour(String time) {
|
||||
final List<String> parts = time.split(':');
|
||||
return int.tryParse(parts.first) ?? 0;
|
||||
}
|
||||
|
||||
/// Returns a human-readable period label for a slot.
|
||||
String _slotPeriodLabel(TimeSlot slot) {
|
||||
final int hour = _parseHour(slot.startTime);
|
||||
if (hour < 12) return 'Morning';
|
||||
if (hour < 17) return 'Afternoon';
|
||||
return 'Evening';
|
||||
}
|
||||
|
||||
// ── Info Card ─────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildInfoCard() {
|
||||
final i18n = Translations.of(context).staff.availability;
|
||||
final dynamic i18n = Translations.of(context).staff.availability;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
@@ -577,20 +575,20 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: UiConstants.space3,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.clock, size: 20, color: UiColors.primary),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: UiConstants.space1,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Text(
|
||||
i18n.auto_match_title,
|
||||
i18n.auto_match_title as String,
|
||||
style: UiTypography.body2m,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
i18n.auto_match_description,
|
||||
i18n.auto_match_description as String,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,31 +1,49 @@
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:staff_availability/src/data/repositories_impl/availability_repository_impl.dart';
|
||||
import 'package:staff_availability/src/domain/repositories/availability_repository.dart';
|
||||
import 'package:staff_availability/src/domain/usecases/apply_quick_set_usecase.dart';
|
||||
import 'package:staff_availability/src/domain/usecases/get_weekly_availability_usecase.dart';
|
||||
import 'package:staff_availability/src/domain/usecases/update_day_availability_usecase.dart';
|
||||
import 'package:staff_availability/src/presentation/blocs/availability_bloc.dart';
|
||||
import 'package:staff_availability/src/presentation/pages/availability_page.dart';
|
||||
|
||||
import 'data/repositories_impl/availability_repository_impl.dart';
|
||||
import 'domain/repositories/availability_repository.dart';
|
||||
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';
|
||||
|
||||
/// Module for the staff availability feature.
|
||||
///
|
||||
/// Uses the V2 REST API via [BaseApiService] for all backend access.
|
||||
class StaffAvailabilityModule extends Module {
|
||||
@override
|
||||
List<Module> get imports => [DataConnectModule()];
|
||||
List<Module> get imports => <Module>[CoreModule()];
|
||||
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repository
|
||||
i.addLazySingleton<AvailabilityRepository>(AvailabilityRepositoryImpl.new);
|
||||
// Repository — V2 API
|
||||
i.addLazySingleton<AvailabilityRepository>(
|
||||
() => AvailabilityRepositoryImpl(
|
||||
apiService: i.get<BaseApiService>(),
|
||||
),
|
||||
);
|
||||
|
||||
// UseCases
|
||||
i.addLazySingleton(GetWeeklyAvailabilityUseCase.new);
|
||||
i.addLazySingleton(UpdateDayAvailabilityUseCase.new);
|
||||
i.addLazySingleton(ApplyQuickSetUseCase.new);
|
||||
// Use cases
|
||||
i.addLazySingleton<GetWeeklyAvailabilityUseCase>(
|
||||
() => GetWeeklyAvailabilityUseCase(i.get<AvailabilityRepository>()),
|
||||
);
|
||||
i.addLazySingleton<UpdateDayAvailabilityUseCase>(
|
||||
() => UpdateDayAvailabilityUseCase(i.get<AvailabilityRepository>()),
|
||||
);
|
||||
i.addLazySingleton<ApplyQuickSetUseCase>(
|
||||
() => ApplyQuickSetUseCase(i.get<AvailabilityRepository>()),
|
||||
);
|
||||
|
||||
// BLoC
|
||||
i.add(AvailabilityBloc.new);
|
||||
i.add<AvailabilityBloc>(
|
||||
() => AvailabilityBloc(
|
||||
getWeeklyAvailability: i.get<GetWeeklyAvailabilityUseCase>(),
|
||||
updateDayAvailability: i.get<UpdateDayAvailabilityUseCase>(),
|
||||
applyQuickSet: i.get<ApplyQuickSetUseCase>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
Reference in New Issue
Block a user