feat: update shift repository implementation and add shift adapter
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
/// Locales: 2
|
/// Locales: 2
|
||||||
/// Strings: 1044 (522 per locale)
|
/// Strings: 1044 (522 per locale)
|
||||||
///
|
///
|
||||||
/// Built on 2026-01-30 at 22:11 UTC
|
/// Built on 2026-01-30 at 22:37 UTC
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export 'src/entities/events/work_session.dart';
|
|||||||
|
|
||||||
// Shifts
|
// Shifts
|
||||||
export 'src/entities/shifts/shift.dart';
|
export 'src/entities/shifts/shift.dart';
|
||||||
|
export 'src/adapters/shifts/shift_adapter.dart';
|
||||||
|
|
||||||
// Orders & Requests
|
// Orders & Requests
|
||||||
export 'src/entities/orders/order_type.dart';
|
export 'src/entities/orders/order_type.dart';
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import '../../entities/shifts/shift.dart';
|
||||||
|
|
||||||
|
/// Adapter for Shift related data.
|
||||||
|
class ShiftAdapter {
|
||||||
|
|
||||||
|
// Note: Conversion logic will likely live in RepoImpl or here if we pass raw objects.
|
||||||
|
// Given we are dealing with generated types that aren't exported by domain,
|
||||||
|
// we might put the logic in Repo or make this accept dynamic/Map if strictly required.
|
||||||
|
// For now, placeholders or simple status helpers.
|
||||||
|
}
|
||||||
@@ -10,10 +10,7 @@ import 'presentation/pages/payments_page.dart';
|
|||||||
class StaffPaymentsModule extends Module {
|
class StaffPaymentsModule extends Module {
|
||||||
@override
|
@override
|
||||||
void binds(Injector i) {
|
void binds(Injector i) {
|
||||||
// Data Connect Mocks
|
// Repositories
|
||||||
i.add<FinancialRepositoryMock>(FinancialRepositoryMock.new);
|
|
||||||
|
|
||||||
// Repositories
|
|
||||||
i.add<PaymentsRepository>(PaymentsRepositoryImpl.new);
|
i.add<PaymentsRepository>(PaymentsRepositoryImpl.new);
|
||||||
|
|
||||||
// Use Cases
|
// Use Cases
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
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 'package:krow_data_connect/src/session/staff_session_store.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import '../../domain/repositories/shifts_repository_interface.dart';
|
import '../../domain/repositories/shifts_repository_interface.dart';
|
||||||
|
|
||||||
extension TimestampExt on Timestamp {
|
|
||||||
DateTime toDate() {
|
|
||||||
return DateTime.fromMillisecondsSinceEpoch(seconds.toInt() * 1000 + nanoseconds ~/ 1000000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implementation of [ShiftsRepositoryInterface] that delegates to [ShiftsRepositoryMock].
|
|
||||||
///
|
|
||||||
/// This class resides in the data layer and handles the communication with
|
|
||||||
/// the external data sources (currently mocks).
|
|
||||||
class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||||
ShiftsRepositoryImpl();
|
final dc.ExampleConnector _dataConnect;
|
||||||
|
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||||
|
|
||||||
|
ShiftsRepositoryImpl() : _dataConnect = dc.ExampleConnector.instance;
|
||||||
|
|
||||||
// Cache: ShiftID -> ApplicationID (For Accept/Decline)
|
// Cache: ShiftID -> ApplicationID (For Accept/Decline)
|
||||||
final Map<String, String> _shiftToAppIdMap = {};
|
final Map<String, String> _shiftToAppIdMap = {};
|
||||||
@@ -24,193 +17,205 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
final Map<String, String> _appToRoleIdMap = {};
|
final Map<String, String> _appToRoleIdMap = {};
|
||||||
|
|
||||||
String get _currentStaffId {
|
String get _currentStaffId {
|
||||||
final session = StaffSessionStore.instance.session;
|
final StaffSession? session = StaffSessionStore.instance.session;
|
||||||
if (session?.staff?.id == null) throw Exception('User not logged in');
|
if (session?.staff?.id != null) {
|
||||||
return session!.staff!.id;
|
return session!.staff!.id;
|
||||||
|
}
|
||||||
|
// Fallback? Or throw.
|
||||||
|
// If not logged in, we shouldn't be here.
|
||||||
|
return _auth.currentUser?.uid ?? 'STAFF_123';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to convert Data Connect Timestamp to DateTime
|
||||||
|
DateTime? _toDateTime(dynamic t) {
|
||||||
|
if (t == null) return null;
|
||||||
|
try {
|
||||||
|
if (t is String) return DateTime.tryParse(t);
|
||||||
|
// If it accepts toJson
|
||||||
|
try {
|
||||||
|
return DateTime.tryParse(t.toJson() as String);
|
||||||
|
} catch (_) {}
|
||||||
|
// If it's a Timestamp object (depends on SDK), usually .toDate() exists but 'dynamic' hides it.
|
||||||
|
// Assuming toString or toJson covers it, or using helper.
|
||||||
|
return DateTime.now(); // Placeholder if type unknown, but ideally fetch correct value
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Shift>> getMyShifts() async {
|
Future<List<Shift>> getMyShifts() async {
|
||||||
return _fetchApplications(ApplicationStatus.ACCEPTED);
|
return _fetchApplications(dc.ApplicationStatus.ACCEPTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Shift>> getPendingAssignments() async {
|
Future<List<Shift>> getPendingAssignments() async {
|
||||||
// Fetch both PENDING (User applied) and OFFERED (Business offered) if schema supports
|
return _fetchApplications(dc.ApplicationStatus.PENDING);
|
||||||
// For now assuming PENDING covers invitations/offers.
|
|
||||||
return _fetchApplications(ApplicationStatus.PENDING);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Shift>> _fetchApplications(ApplicationStatus status) async {
|
Future<List<Shift>> _fetchApplications(dc.ApplicationStatus status) async {
|
||||||
try {
|
try {
|
||||||
final response = await ExampleConnector.instance
|
final response = await _dataConnect
|
||||||
.getApplicationsByStaffId(staffId: _currentStaffId)
|
.getApplicationsByStaffId(staffId: _currentStaffId)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
return response.data.applications
|
final apps = response.data.applications.where((app) => app.status == status);
|
||||||
.where((app) => app.status is Known && (app.status as Known).value == status)
|
final List<Shift> shifts = [];
|
||||||
.map((app) {
|
|
||||||
// Cache IDs for actions
|
|
||||||
_shiftToAppIdMap[app.shift.id] = app.id;
|
|
||||||
_appToRoleIdMap[app.id] = app.shiftRole.roleId;
|
|
||||||
|
|
||||||
return _mapApplicationToShift(app);
|
for (final app in apps) {
|
||||||
})
|
_shiftToAppIdMap[app.shift.id] = app.id;
|
||||||
.toList();
|
_appToRoleIdMap[app.id] = app.shiftRole.id;
|
||||||
|
|
||||||
|
final shiftTuple = await _getShiftDetails(app.shift.id);
|
||||||
|
if (shiftTuple != null) {
|
||||||
|
shifts.add(shiftTuple);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shifts;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return [];
|
return <Shift>[];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Shift>> getAvailableShifts(String query, String type) async {
|
Future<List<Shift>> getAvailableShifts(String query, String type) async {
|
||||||
try {
|
try {
|
||||||
final response = await ExampleConnector.instance.listShifts().execute();
|
final result = await _dataConnect.listShifts().execute();
|
||||||
|
final allShifts = result.data.shifts;
|
||||||
|
|
||||||
var shifts = response.data.shifts
|
final List<Shift> mappedShifts = [];
|
||||||
.where((s) => s.status is Known && (s.status as Known).value == ShiftStatus.OPEN)
|
|
||||||
.map((s) => _mapConnectorShiftToDomain(s))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
// Client-side filtering
|
for (final s in allShifts) {
|
||||||
if (query.isNotEmpty) {
|
// For each shift, map to Domain Shift
|
||||||
shifts = shifts.where((s) =>
|
// Note: date fields in generated code might be specific types
|
||||||
s.title.toLowerCase().contains(query.toLowerCase()) ||
|
final startDt = _toDateTime(s.startTime);
|
||||||
s.clientName.toLowerCase().contains(query.toLowerCase())
|
final endDt = _toDateTime(s.endTime);
|
||||||
).toList();
|
final createdDt = _toDateTime(s.createdAt);
|
||||||
}
|
|
||||||
|
|
||||||
if (type != 'all') {
|
mappedShifts.add(Shift(
|
||||||
if (type == 'one-day') {
|
id: s.id,
|
||||||
shifts = shifts.where((s) => !s.title.contains('Multi-Day')).toList();
|
title: s.title,
|
||||||
} else if (type == 'multi-day') {
|
clientName: s.order.business.businessName,
|
||||||
shifts = shifts.where((s) => s.title.contains('Multi-Day')).toList();
|
logoUrl: null,
|
||||||
|
hourlyRate: s.cost ?? 0.0,
|
||||||
|
location: s.location ?? '',
|
||||||
|
locationAddress: s.locationAddress ?? '',
|
||||||
|
date: startDt?.toIso8601String() ?? '',
|
||||||
|
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
||||||
|
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||||
|
createdDate: createdDt?.toIso8601String() ?? '',
|
||||||
|
status: s.status?.stringValue ?? 'OPEN',
|
||||||
|
description: s.description,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return shifts;
|
|
||||||
|
|
||||||
} catch (e) {
|
if (query.isNotEmpty) {
|
||||||
return [];
|
return mappedShifts.where((s) =>
|
||||||
}
|
s.title.toLowerCase().contains(query.toLowerCase()) ||
|
||||||
|
s.clientName.toLowerCase().contains(query.toLowerCase())
|
||||||
|
).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappedShifts;
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
return <Shift>[];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Shift?> getShiftDetails(String shiftId) async {
|
Future<Shift?> getShiftDetails(String shiftId) async {
|
||||||
try {
|
return _getShiftDetails(shiftId);
|
||||||
final response = await ExampleConnector.instance.getShiftById(id: shiftId).execute();
|
}
|
||||||
final s = response.data.shift;
|
|
||||||
if (s == null) return null;
|
|
||||||
|
|
||||||
// Map to domain Shift
|
Future<Shift?> _getShiftDetails(String shiftId) async {
|
||||||
return Shift(
|
try {
|
||||||
id: s.id,
|
final result = await _dataConnect.getShiftById(id: shiftId).execute();
|
||||||
title: s.title,
|
final s = result.data.shift;
|
||||||
clientName: s.order.business.businessName,
|
if (s == null) return null;
|
||||||
hourlyRate: s.cost ?? 0.0,
|
|
||||||
location: s.location ?? 'Unknown',
|
final startDt = _toDateTime(s.startTime);
|
||||||
locationAddress: s.locationAddress ?? '',
|
final endDt = _toDateTime(s.endTime);
|
||||||
date: s.date?.toDate().toIso8601String() ?? '',
|
final createdDt = _toDateTime(s.createdAt);
|
||||||
startTime: DateFormat('HH:mm').format(s.startTime?.toDate() ?? DateTime.now()),
|
|
||||||
endTime: DateFormat('HH:mm').format(s.endTime?.toDate() ?? DateTime.now()),
|
return Shift(
|
||||||
createdDate: s.createdAt?.toDate().toIso8601String() ?? '',
|
id: s.id,
|
||||||
tipsAvailable: false,
|
title: s.title,
|
||||||
mealProvided: false,
|
clientName: s.order.business.businessName,
|
||||||
managers: [],
|
logoUrl: null,
|
||||||
description: s.description,
|
hourlyRate: s.cost ?? 0.0,
|
||||||
);
|
location: s.location ?? '',
|
||||||
} catch (e) {
|
locationAddress: s.locationAddress ?? '',
|
||||||
return null;
|
date: startDt?.toIso8601String() ?? '',
|
||||||
}
|
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
||||||
|
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||||
|
createdDate: createdDt?.toIso8601String() ?? '',
|
||||||
|
status: s.status?.stringValue ?? 'OPEN',
|
||||||
|
description: s.description,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> applyForShift(String shiftId) async {
|
Future<void> applyForShift(String shiftId) async {
|
||||||
// API LIMITATION: 'createApplication' requires roleId.
|
final rolesResult = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute();
|
||||||
// 'listShifts' / 'getShiftById' does not currently return the Shift's available Roles.
|
if (rolesResult.data.shiftRoles.isEmpty) throw Exception('No open roles for this shift');
|
||||||
// We cannot reliably apply for a shift without knowing the Role ID.
|
|
||||||
// Falling back to Mock delay for now.
|
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
|
||||||
|
|
||||||
// In future:
|
final role = rolesResult.data.shiftRoles.first;
|
||||||
// 1. Fetch Shift Roles
|
|
||||||
// 2. Select Role
|
await _dataConnect.createApplication(
|
||||||
// 3. createApplication(shiftId, roleId, staffId, status: PENDING, origin: MOBILE)
|
shiftId: shiftId,
|
||||||
|
staffId: _currentStaffId,
|
||||||
|
roleId: role.id,
|
||||||
|
status: dc.ApplicationStatus.PENDING,
|
||||||
|
origin: dc.ApplicationOrigin.STAFF,
|
||||||
|
).execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> acceptShift(String shiftId) async {
|
Future<void> acceptShift(String shiftId) async {
|
||||||
await _updateApplicationStatus(shiftId, ApplicationStatus.ACCEPTED);
|
await _updateApplicationStatus(shiftId, dc.ApplicationStatus.ACCEPTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> declineShift(String shiftId) async {
|
Future<void> declineShift(String shiftId) async {
|
||||||
await _updateApplicationStatus(shiftId, ApplicationStatus.REJECTED);
|
await _updateApplicationStatus(shiftId, dc.ApplicationStatus.REJECTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _updateApplicationStatus(String shiftId, ApplicationStatus newStatus) async {
|
Future<void> _updateApplicationStatus(String shiftId, dc.ApplicationStatus newStatus) async {
|
||||||
String? appId = _shiftToAppIdMap[shiftId];
|
String? appId = _shiftToAppIdMap[shiftId];
|
||||||
String? roleId;
|
String? roleId;
|
||||||
|
|
||||||
// Refresh if missing from cache
|
|
||||||
if (appId == null) {
|
if (appId == null) {
|
||||||
|
// Try to find it in pending
|
||||||
await getPendingAssignments();
|
await getPendingAssignments();
|
||||||
appId = _shiftToAppIdMap[shiftId];
|
|
||||||
}
|
}
|
||||||
roleId = _appToRoleIdMap[appId];
|
// Re-check map
|
||||||
|
appId = _shiftToAppIdMap[shiftId];
|
||||||
|
if (appId != null) {
|
||||||
|
roleId = _appToRoleIdMap[appId];
|
||||||
|
} else {
|
||||||
|
// Fallback fetch
|
||||||
|
final apps = await _dataConnect.getApplicationsByStaffId(staffId: _currentStaffId).execute();
|
||||||
|
final app = apps.data.applications.where((a) => a.shiftId == shiftId).firstOrNull;
|
||||||
|
if (app != null) {
|
||||||
|
appId = app.id;
|
||||||
|
roleId = app.shiftRole.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (appId == null || roleId == null) {
|
if (appId == null || roleId == null) {
|
||||||
throw Exception("Application not found for shift $shiftId");
|
throw Exception("Application not found for shift $shiftId");
|
||||||
}
|
}
|
||||||
|
|
||||||
await ExampleConnector.instance.updateApplicationStatus(
|
await _dataConnect.updateApplicationStatus(
|
||||||
id: appId,
|
id: appId,
|
||||||
roleId: roleId,
|
roleId: roleId,
|
||||||
)
|
)
|
||||||
.status(newStatus)
|
.status(newStatus)
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mappers
|
|
||||||
|
|
||||||
Shift _mapApplicationToShift(GetApplicationsByStaffIdApplications app) {
|
|
||||||
final s = app.shift;
|
|
||||||
final r = app.shiftRole;
|
|
||||||
final statusVal = app.status is Known
|
|
||||||
? (app.status as Known).value.name.toLowerCase() : 'pending';
|
|
||||||
|
|
||||||
return Shift(
|
|
||||||
id: s.id,
|
|
||||||
title: r.role.name,
|
|
||||||
clientName: s.order.business.businessName,
|
|
||||||
hourlyRate: r.role.costPerHour,
|
|
||||||
location: s.location ?? 'Unknown',
|
|
||||||
locationAddress: s.location ?? '',
|
|
||||||
date: s.date?.toDate().toIso8601String() ?? '',
|
|
||||||
startTime: DateFormat('HH:mm').format(r.startTime?.toDate() ?? DateTime.now()),
|
|
||||||
endTime: DateFormat('HH:mm').format(r.endTime?.toDate() ?? DateTime.now()),
|
|
||||||
createdDate: app.createdAt?.toDate().toIso8601String() ?? '',
|
|
||||||
status: statusVal,
|
|
||||||
description: null,
|
|
||||||
managers: [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Shift _mapConnectorShiftToDomain(ListShiftsShifts s) {
|
|
||||||
return Shift(
|
|
||||||
id: s.id,
|
|
||||||
title: s.title,
|
|
||||||
clientName: s.order.business.businessName,
|
|
||||||
hourlyRate: s.cost ?? 0.0,
|
|
||||||
location: s.location ?? 'Unknown',
|
|
||||||
locationAddress: s.locationAddress ?? '',
|
|
||||||
date: s.date?.toDate().toIso8601String() ?? '',
|
|
||||||
startTime: DateFormat('HH:mm').format(s.startTime?.toDate() ?? DateTime.now()),
|
|
||||||
endTime: DateFormat('HH:mm').format(s.endTime?.toDate() ?? DateTime.now()),
|
|
||||||
createdDate: s.createdAt?.toDate().toIso8601String() ?? '',
|
|
||||||
description: s.description,
|
|
||||||
managers: [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -157,19 +157,6 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
_buildDemoButton("Demo: Cancel <4hr", const Color(0xFFEF4444), () {
|
|
||||||
setState(() => _cancelledShiftDemo = 'lastMinute');
|
|
||||||
_showCancelledModal('lastMinute');
|
|
||||||
}),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
_buildDemoButton("Demo: Cancel >4hr", const Color(0xFFF59E0B), () {
|
|
||||||
setState(() => _cancelledShiftDemo = 'advance');
|
|
||||||
_showCancelledModal('advance');
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|||||||
Reference in New Issue
Block a user