feat: integrate availability adapter and repository implementation for staff availability management

This commit is contained in:
Achintha Isuru
2026-01-30 16:04:05 -05:00
parent 4fb2f17ea5
commit 0b763bae44
5 changed files with 291 additions and 190 deletions

View File

@@ -79,6 +79,7 @@ export 'src/entities/home/home_dashboard_data.dart';
export 'src/entities/home/reorder_item.dart';
// Availability
export 'src/adapters/availability/availability_adapter.dart';
export 'src/entities/availability/availability_slot.dart';
export 'src/entities/availability/day_availability.dart';

View File

@@ -0,0 +1,33 @@
import '../../entities/availability/availability_slot.dart';
/// Adapter for [AvailabilitySlot] domain entity.
class AvailabilityAdapter {
static const Map<String, Map<String, String>> _slotDefinitions = {
'MORNING': {
'id': 'morning',
'label': 'Morning',
'timeRange': '4:00 AM - 12:00 PM',
},
'AFTERNOON': {
'id': 'afternoon',
'label': 'Afternoon',
'timeRange': '12:00 PM - 6:00 PM',
},
'EVENING': {
'id': 'evening',
'label': 'Evening',
'timeRange': '6:00 PM - 12:00 AM',
},
};
/// Converts a backend slot name (e.g. 'MORNING') to a Domain [AvailabilitySlot].
static AvailabilitySlot fromPrimitive(String slotName, {bool isAvailable = false}) {
final def = _slotDefinitions[slotName.toUpperCase()] ?? _slotDefinitions['MORNING']!;
return AvailabilitySlot(
id: def['id']!,
label: def['label']!,
timeRange: def['timeRange']!,
isAvailable: isAvailable,
);
}
}

View File

@@ -1,183 +0,0 @@
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_data_connect/src/session/staff_session_store.dart';
import '../../domain/repositories/availability_repository.dart';
import 'package:krow_domain/krow_domain.dart';
/// Implementation of [AvailabilityRepository].
///
/// Uses [StafRepositoryMock] (conceptually) from data_connect to fetch and store data.
class AvailabilityRepositoryImpl implements AvailabilityRepository {
AvailabilityRepositoryImpl();
String get _currentStaffId {
final session = StaffSessionStore.instance.session;
if (session?.staff?.id == null) throw Exception('User not logged in');
return session!.staff!.id;
}
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',
},
];
@override
Future<List<DayAvailability>> getAvailability(
DateTime start, DateTime end) async {
// 1. Fetch Weekly Template from Backend
Map<dc.DayOfWeek, Map<dc.AvailabilitySlot, bool>> weeklyTemplate = {};
try {
final response = await dc.ExampleConnector.instance
.getStaffAvailabilityStatsByStaffId(staffId: _currentStaffId)
.execute();
// Note: getStaffAvailabilityStatsByStaffId might not return detailed slots per day in this schema version?
// Wait, the previous code used `listStaffAvailabilitiesByStaffId` but that method didn't exist in generated code search?
// Genereted code showed `listStaffAvailabilityStats`.
// Let's assume there is a listStaffAvailabilities or similar, OR we use the stats.
// But for now, let's look at the generated.dart again.
// It has `CreateStaffAvailability`, `UpdateStaffAvailability`, `DeleteStaffAvailability`.
// But LIST seems to be `listStaffAvailabilityStats`? Maybe `listStaffAvailability` is missing?
// If we can't fetch it, we'll just return default empty.
// For the sake of fixing build, I will comment out the fetch logic if the method doesn't exist,
// AR replace it with a valid call if I can find one.
// The snippet showed `listStaffAvailabilityStats`.
// Let's try to infer from the code I saw earlier.
// `dc.ExampleConnector.instance.listStaffAvailabilitiesByStaffId` was used.
// If that produced an error "Method not defined", I should fix it.
// But the error log didn't show "Method not defined" for `listStaffAvailabilitiesByStaffId`.
// It showed mismatch in return types of `getAvailability`.
// So assuming `listStaffAvailabilitiesByStaffId` DOES exist or I should mock it.
// However, fixing the TYPE mismatch is the priority.
} catch (e) {
// If error (or empty), use default empty template
}
// 2. Map Template to Requested Date Range
final List<DayAvailability> days = [];
final dayCount = end.difference(start).inDays;
for (int i = 0; i <= dayCount; i++) {
final date = start.add(Duration(days: i));
// final dayOfWeek = _mapDateTimeToDayOfWeek(date.weekday);
// final daySlotsMap = weeklyTemplate[dayOfWeek] ?? {};
// Determine overall day availability (true if ANY slot is available)
// final bool isDayAvailable = daySlotsMap.values.any((val) => val == true);
final slots = _slotDefinitions.map((def) {
// Map string ID 'morning' -> Enum AvailabilitySlot.MORNING
// final slotEnum = _mapStringToSlotEnum(def['id']!);
// final isSlotAvailable = daySlotsMap[slotEnum] ?? false; // Default false if not set
return AvailabilitySlot(
id: def['id']!,
label: def['label']!,
timeRange: def['timeRange']!,
isAvailable: false, // Defaulting to false since fetch is commented out/incomplete
);
}).toList();
days.add(DayAvailability(
date: date,
isAvailable: false,
slots: slots,
));
}
return days;
}
@override
Future<DayAvailability> updateDayAvailability(
DayAvailability availability) async {
// Stub implementation to fix build
await Future.delayed(const Duration(milliseconds: 500));
return availability;
}
@override
Future<List<DayAvailability>> applyQuickSet(
DateTime start, DateTime end, String type) async {
final List<DayAvailability> updatedDays = [];
final dayCount = end.difference(start).inDays;
for (int i = 0; i <= dayCount; i++) {
final date = start.add(Duration(days: i));
bool dayEnabled = false;
switch (type) {
case 'all': dayEnabled = true; break;
case 'weekdays':
dayEnabled = date.weekday != DateTime.saturday && date.weekday != DateTime.sunday;
break;
case 'weekends':
dayEnabled = date.weekday == DateTime.saturday || date.weekday == DateTime.sunday;
break;
case 'clear': dayEnabled = false; break;
}
final slots = _slotDefinitions.map((def) {
return AvailabilitySlot(
id: def['id']!,
label: def['label']!,
timeRange: def['timeRange']!,
isAvailable: dayEnabled,
);
}).toList();
updatedDays.add(DayAvailability(
date: date,
isAvailable: dayEnabled,
slots: slots,
));
}
return updatedDays;
}
// --- Helpers ---
dc.DayOfWeek _mapDateTimeToDayOfWeek(DateTime date) {
switch (date.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;
}
}
dc.AvailabilitySlot _mapStringToSlotEnum(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;
}
}
}

View File

@@ -0,0 +1,243 @@
import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/availability_repository.dart';
/// Implementation of [AvailabilityRepository] using Firebase Data Connect.
///
/// 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.ExampleConnector _dataConnect;
final firebase.FirebaseAuth _firebaseAuth;
AvailabilityRepositoryImpl({
required dc.ExampleConnector dataConnect,
required firebase.FirebaseAuth firebaseAuth,
}) : _dataConnect = dataConnect,
_firebaseAuth = firebaseAuth;
Future<String> _getStaffId() async {
final firebase.User? user = _firebaseAuth.currentUser;
if (user == null) throw Exception('User not authenticated');
final QueryResult<dc.GetStaffByUserIdData, dc.GetStaffByUserIdVariables> result =
await _dataConnect.getStaffByUserId(userId: user.uid).execute();
if (result.data.staffs.isEmpty) {
throw Exception('Staff profile not found');
}
return result.data.staffs.first.id;
}
@override
Future<List<DayAvailability>> getAvailability(DateTime start, DateTime end) async {
final String staffId = await _getStaffId();
// 1. Fetch Weekly recurring availability
final QueryResult<dc.ListStaffAvailabilitiesByStaffIdData, dc.ListStaffAvailabilitiesByStaffIdVariables> result =
await _dataConnect.listStaffAvailabilitiesByStaffId(staffId: staffId).limit(100).execute();
final List<dc.ListStaffAvailabilitiesByStaffIdStaffAvailabilities> items = result.data.staffAvailabilities;
// 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;
}
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);
}
@override
Future<DayAvailability> updateDayAvailability(DayAvailability availability) async {
final String staffId = await _getStaffId();
final dc.DayOfWeek dow = _toBackendDay(availability.date.weekday);
// 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);
await _upsertSlot(staffId, dow, slotEnum, status);
}
return availability;
}
@override
Future<List<DayAvailability>> applyQuickSet(DateTime start, DateTime end, String type) async {
final String staffId = await _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 = [];
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)
// to avoid redundant calls if range > 1 week.
if (!processedDays.contains(dow)) {
processedDays.add(dow);
final dc.AvailabilityStatus status = _boolToStatus(enableDay);
await Future.wait([
_upsertSlot(staffId, dow, dc.AvailabilitySlot.MORNING, status),
_upsertSlot(staffId, dow, dc.AvailabilitySlot.AFTERNOON, status),
_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,
));
}
return resultDays;
}
Future<void> _upsertSlot(String staffId, dc.DayOfWeek day, dc.AvailabilitySlot slot, dc.AvailabilityStatus status) async {
// Check if exists
final result = await _dataConnect.getStaffAvailabilityByKey(
staffId: staffId,
day: day,
slot: slot,
).execute();
if (result.data.staffAvailability != null) {
// Update
await _dataConnect.updateStaffAvailability(
staffId: staffId,
day: day,
slot: slot,
).status(status).execute();
} else {
// Create
await _dataConnect.createStaffAvailability(
staffId: staffId,
day: day,
slot: slot,
).status(status).execute();
}
}
// --- 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;
}
}
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;
}
}

View File

@@ -1,21 +1,28 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'data/repositories/availability_repository_impl.dart';
import 'package:staff_availability/src/presentation/pages/availability_page_new.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';
import 'presentation/pages/availability_page.dart';
class StaffAvailabilityModule extends Module {
@override
void binds(i) {
// Data Sources
i.add(StaffRepositoryMock.new);
List<Module> get imports => [DataConnectModule()];
@override
void binds(Injector i) {
// Repository
i.add<AvailabilityRepository>(AvailabilityRepositoryImpl.new);
i.add<AvailabilityRepository>(
() => AvailabilityRepositoryImpl(
dataConnect: ExampleConnector.instance,
firebaseAuth: FirebaseAuth.instance,
),
);
// UseCases
i.add(GetWeeklyAvailabilityUseCase.new);
@@ -27,7 +34,7 @@ class StaffAvailabilityModule extends Module {
}
@override
void routes(r) {
void routes(RouteManager r) {
r.child('/', child: (_) => const AvailabilityPage());
}
}