feat: integrate availability adapter and repository implementation for staff availability management
This commit is contained in:
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user