This commit is contained in:
José Salazar
2026-02-01 22:39:40 +09:00
parent 3cebb37dfd
commit 6277b9f5e2
19 changed files with 20900 additions and 18729 deletions

View File

@@ -168,7 +168,11 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
avatar: staffRecord.photoUrl,
);
StaffSessionStore.instance.setSession(
StaffSession(user: domainUser, staff: domainStaff),
StaffSession(
user: domainUser,
staff: domainStaff,
ownerId: staffRecord?.ownerId,
),
);
return domainUser;
}

View File

@@ -60,7 +60,7 @@ class ProfileSetupRepositoryImpl implements ProfileSetupRepository {
if (session != null) {
StaffSessionStore.instance.setSession(
StaffSession(user: session.user, staff: staff),
StaffSession(user: session.user, staff: staff, ownerId: session.ownerId),
);
}
}

View File

@@ -156,120 +156,254 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
@override
Future<List<Shift>> getAvailableShifts(String query, String type) async {
try {
final result = await _dataConnect.listShifts().execute();
final allShifts = result.data.shifts;
final List<Shift> mappedShifts = [];
for (final s in allShifts) {
// For each shift, map to Domain Shift
// Note: date fields in generated code might be specific types
final startDt = _toDateTime(s.startTime);
final endDt = _toDateTime(s.endTime);
final createdDt = _toDateTime(s.createdAt);
mappedShifts.add(Shift(
id: s.id,
title: s.title,
clientName: s.order.business.businessName,
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.toLowerCase() ?? 'open',
description: s.description,
durationDays: s.durationDays,
requiredSlots: null, // Basic list doesn't fetch detailed role stats yet
filledSlots: null,
));
}
if (query.isNotEmpty) {
return mappedShifts.where((s) =>
s.title.toLowerCase().contains(query.toLowerCase()) ||
s.clientName.toLowerCase().contains(query.toLowerCase())
).toList();
}
return mappedShifts;
} catch (e) {
return <Shift>[];
try {
final String? vendorId =
dc.StaffSessionStore.instance.session?.ownerId;
if (vendorId == null || vendorId.isEmpty) {
return <Shift>[];
}
}
@override
Future<Shift?> getShiftDetails(String shiftId) async {
return _getShiftDetails(shiftId);
}
Future<Shift?> _getShiftDetails(String shiftId) async {
try {
final result = await _dataConnect.getShiftById(id: shiftId).execute();
final s = result.data.shift;
if (s == null) return null;
int? required;
int? filled;
try {
final rolesRes = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute();
if (rolesRes.data.shiftRoles.isNotEmpty) {
required = 0;
filled = 0;
for(var r in rolesRes.data.shiftRoles) {
required = (required ?? 0) + r.count;
filled = (filled ?? 0) + (r.assigned ?? 0);
}
}
} catch (_) {}
final result = await _dataConnect
.listShiftRolesByVendorId(vendorId: vendorId)
.execute();
final allShiftRoles = result.data.shiftRoles;
final startDt = _toDateTime(s.startTime);
final endDt = _toDateTime(s.endTime);
final createdDt = _toDateTime(s.createdAt);
return Shift(
id: s.id,
title: s.title,
clientName: s.order.business.businessName,
final List<Shift> mappedShifts = [];
for (final sr in allShiftRoles) {
final startDt = _toDateTime(sr.startTime);
final endDt = _toDateTime(sr.endTime);
final createdDt = _toDateTime(sr.createdAt);
mappedShifts.add(
Shift(
id: sr.shiftId,
roleId: sr.roleId,
title: sr.role.name,
clientName: sr.shift.order.business.businessName,
logoUrl: null,
hourlyRate: s.cost ?? 0.0,
location: s.location ?? '',
locationAddress: s.locationAddress ?? '',
hourlyRate: sr.role.costPerHour,
location: sr.shift.location ?? '',
locationAddress: sr.shift.locationAddress ?? '',
date: startDt?.toIso8601String() ?? '',
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
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,
durationDays: s.durationDays,
requiredSlots: required,
filledSlots: filled,
status: sr.shift.status?.stringValue.toLowerCase() ?? 'open',
description: sr.shift.description,
durationDays: sr.shift.durationDays,
requiredSlots: sr.count,
filledSlots: sr.assigned ?? 0,
),
);
} catch (e) {
return null;
}
if (query.isNotEmpty) {
return mappedShifts
.where(
(s) =>
s.title.toLowerCase().contains(query.toLowerCase()) ||
s.clientName.toLowerCase().contains(query.toLowerCase()),
)
.toList();
}
return mappedShifts;
} catch (e) {
return <Shift>[];
}
}
@override
Future<void> applyForShift(String shiftId, {bool isInstantBook = false}) async {
final rolesResult = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute();
if (rolesResult.data.shiftRoles.isEmpty) throw Exception('No open roles for this shift');
Future<Shift?> getShiftDetails(String shiftId, {String? roleId}) async {
return _getShiftDetails(shiftId, roleId: roleId);
}
Future<Shift?> _getShiftDetails(String shiftId, {String? roleId}) async {
try {
if (roleId != null && roleId.isNotEmpty) {
final roleResult = await _dataConnect
.getShiftRoleById(shiftId: shiftId, roleId: roleId)
.execute();
final sr = roleResult.data.shiftRole;
if (sr == null) return null;
final DateTime? startDt = _toDateTime(sr.startTime);
final DateTime? endDt = _toDateTime(sr.endTime);
final DateTime? createdDt = _toDateTime(sr.createdAt);
final String? staffId = _auth.currentUser?.uid;
bool hasApplied = false;
String status = 'open';
if (staffId != null) {
final apps =
await _dataConnect.getApplicationsByStaffId(staffId: staffId).execute();
final app = apps.data.applications
.where(
(a) =>
a.shiftId == shiftId && a.shiftRole.roleId == roleId,
)
.firstOrNull;
if (app != null) {
hasApplied = true;
if (app.status is dc.Known<dc.ApplicationStatus>) {
final dc.ApplicationStatus s =
(app.status as dc.Known<dc.ApplicationStatus>).value;
status = _mapStatus(s);
}
}
}
return Shift(
id: sr.shiftId,
roleId: sr.roleId,
title: sr.shift.order.business.businessName,
clientName: sr.shift.order.business.businessName,
logoUrl: sr.shift.order.business.companyLogoUrl,
hourlyRate: sr.role.costPerHour,
location: sr.shift.order.teamHub.hubName,
locationAddress: '',
date: startDt?.toIso8601String() ?? '',
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
createdDate: createdDt?.toIso8601String() ?? '',
status: status,
description: sr.shift.description,
durationDays: null,
requiredSlots: sr.count,
filledSlots: sr.assigned ?? 0,
hasApplied: hasApplied,
totalValue: sr.totalValue,
);
}
final result = await _dataConnect.getShiftById(id: shiftId).execute();
final s = result.data.shift;
if (s == null) return null;
final role = rolesResult.data.shiftRoles.first;
final staffId = await _getStaffId();
await _dataConnect.createApplication(
shiftId: shiftId,
staffId: staffId,
roleId: role.roleId,
status: isInstantBook ? dc.ApplicationStatus.ACCEPTED : dc.ApplicationStatus.PENDING,
origin: dc.ApplicationOrigin.STAFF,
).execute();
int? required;
int? filled;
try {
final rolesRes = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute();
if (rolesRes.data.shiftRoles.isNotEmpty) {
required = 0;
filled = 0;
for(var r in rolesRes.data.shiftRoles) {
required = (required ?? 0) + r.count;
filled = (filled ?? 0) + (r.assigned ?? 0);
}
}
} catch (_) {}
final startDt = _toDateTime(s.startTime);
final endDt = _toDateTime(s.endTime);
final createdDt = _toDateTime(s.createdAt);
return Shift(
id: s.id,
title: s.title,
clientName: s.order.business.businessName,
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,
durationDays: s.durationDays,
requiredSlots: required,
filledSlots: filled,
);
} catch (e) {
return null;
}
}
@override
Future<void> applyForShift(
String shiftId, {
bool isInstantBook = false,
String? roleId,
}) async {
final staffId = await _getStaffId();
String targetRoleId = roleId ?? '';
if (targetRoleId.isEmpty) {
final rolesResult =
await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute();
if (rolesResult.data.shiftRoles.isEmpty) {
throw Exception('No open roles for this shift');
}
final sr = rolesResult.data.shiftRoles.firstWhere(
(r) => (r.assigned ?? 0) < r.count,
orElse: () => rolesResult.data.shiftRoles.first,
);
targetRoleId = sr.roleId;
}
final roleResult = await _dataConnect
.getShiftRoleById(shiftId: shiftId, roleId: targetRoleId)
.execute();
final role = roleResult.data.shiftRole;
if (role == null) {
throw Exception('Shift role not found');
}
final int assigned = role.assigned ?? 0;
if (assigned >= role.count) {
throw Exception('This shift is full.');
}
final shiftResult = await _dataConnect.getShiftById(id: shiftId).execute();
final shift = shiftResult.data.shift;
if (shift == null) {
throw Exception('Shift not found');
}
final int filled = shift.filled ?? 0;
String? appId;
bool updatedRole = false;
bool updatedShift = false;
try {
final appResult = await _dataConnect
.createApplication(
shiftId: shiftId,
staffId: staffId,
roleId: targetRoleId,
status: dc.ApplicationStatus.ACCEPTED,
origin: dc.ApplicationOrigin.STAFF,
)
// TODO: this should be PENDING so a vendor can accept it.
.execute();
appId = appResult.data.application_insert.id;
await _dataConnect
.updateShiftRole(shiftId: shiftId, roleId: targetRoleId)
.assigned(assigned + 1)
.execute();
updatedRole = true;
await _dataConnect
.updateShift(id: shiftId)
.filled(filled + 1)
.execute();
updatedShift = true;
} catch (e) {
if (updatedShift) {
await _dataConnect.updateShift(id: shiftId).filled(filled).execute();
}
if (updatedRole) {
await _dataConnect
.updateShiftRole(shiftId: shiftId, roleId: targetRoleId)
.assigned(assigned)
.execute();
}
if (appId != null) {
await _dataConnect.deleteApplication(id: appId).execute();
}
rethrow;
}
}
@override
@@ -333,4 +467,3 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
.execute();
}
}

View File

@@ -0,0 +1,14 @@
import 'package:equatable/equatable.dart';
class GetShiftDetailsArguments extends Equatable {
final String shiftId;
final String? roleId;
const GetShiftDetailsArguments({
required this.shiftId,
this.roleId,
});
@override
List<Object?> get props => [shiftId, roleId];
}

View File

@@ -15,12 +15,16 @@ abstract interface class ShiftsRepositoryInterface {
Future<List<Shift>> getPendingAssignments();
/// Retrieves detailed information for a specific shift by [shiftId].
Future<Shift?> getShiftDetails(String shiftId);
Future<Shift?> getShiftDetails(String shiftId, {String? roleId});
/// Applies for a specific open shift.
///
/// [isInstantBook] determines if the application should be immediately accepted.
Future<void> applyForShift(String shiftId, {bool isInstantBook = false});
Future<void> applyForShift(
String shiftId, {
bool isInstantBook = false,
String? roleId,
});
/// Accepts a pending shift assignment.
Future<void> acceptShift(String shiftId);

View File

@@ -5,7 +5,15 @@ class ApplyForShiftUseCase {
ApplyForShiftUseCase(this.repository);
Future<void> call(String shiftId, {bool isInstantBook = false}) async {
return repository.applyForShift(shiftId, isInstantBook: isInstantBook);
Future<void> call(
String shiftId, {
bool isInstantBook = false,
String? roleId,
}) async {
return repository.applyForShift(
shiftId,
isInstantBook: isInstantBook,
roleId: roleId,
);
}
}

View File

@@ -1,14 +1,18 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../arguments/get_shift_details_arguments.dart';
import '../repositories/shifts_repository_interface.dart';
class GetShiftDetailsUseCase extends UseCase<String, Shift?> {
class GetShiftDetailsUseCase extends UseCase<GetShiftDetailsArguments, Shift?> {
final ShiftsRepositoryInterface repository;
GetShiftDetailsUseCase(this.repository);
@override
Future<Shift?> call(String params) {
return repository.getShiftDetails(params);
Future<Shift?> call(GetShiftDetailsArguments params) {
return repository.getShiftDetails(
params.shiftId,
roleId: params.roleId,
);
}
}

View File

@@ -2,6 +2,7 @@ import 'package:bloc/bloc.dart';
import '../../../domain/usecases/apply_for_shift_usecase.dart';
import '../../../domain/usecases/decline_shift_usecase.dart';
import '../../../domain/usecases/get_shift_details_usecase.dart';
import '../../../domain/arguments/get_shift_details_arguments.dart';
import 'shift_details_event.dart';
import 'shift_details_state.dart';
@@ -26,7 +27,12 @@ class ShiftDetailsBloc extends Bloc<ShiftDetailsEvent, ShiftDetailsState> {
) async {
emit(ShiftDetailsLoading());
try {
final shift = await getShiftDetails(event.shiftId);
final shift = await getShiftDetails(
GetShiftDetailsArguments(
shiftId: event.shiftId,
roleId: event.roleId,
),
);
if (shift != null) {
emit(ShiftDetailsLoaded(shift));
} else {
@@ -42,7 +48,11 @@ class ShiftDetailsBloc extends Bloc<ShiftDetailsEvent, ShiftDetailsState> {
Emitter<ShiftDetailsState> emit,
) async {
try {
await applyForShift(event.shiftId, isInstantBook: true);
await applyForShift(
event.shiftId,
isInstantBook: true,
roleId: event.roleId,
);
emit(const ShiftActionSuccess("Shift successfully booked!"));
} catch (e) {
emit(ShiftDetailsError(e.toString()));

View File

@@ -9,18 +9,20 @@ abstract class ShiftDetailsEvent extends Equatable {
class LoadShiftDetailsEvent extends ShiftDetailsEvent {
final String shiftId;
const LoadShiftDetailsEvent(this.shiftId);
final String? roleId;
const LoadShiftDetailsEvent(this.shiftId, {this.roleId});
@override
List<Object?> get props => [shiftId];
List<Object?> get props => [shiftId, roleId];
}
class BookShiftDetailsEvent extends ShiftDetailsEvent {
final String shiftId;
const BookShiftDetailsEvent(this.shiftId);
final String? roleId;
const BookShiftDetailsEvent(this.shiftId, {this.roleId});
@override
List<Object?> get props => [shiftId];
List<Object?> get props => [shiftId, roleId];
}
class DeclineShiftDetailsEvent extends ShiftDetailsEvent {

View File

@@ -122,7 +122,12 @@ class ShiftDetailsPage extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider<ShiftDetailsBloc>(
create: (_) =>
Modular.get<ShiftDetailsBloc>()..add(LoadShiftDetailsEvent(shiftId)),
Modular.get<ShiftDetailsBloc>()..add(
LoadShiftDetailsEvent(
shiftId,
roleId: shift?.roleId,
),
),
child: BlocListener<ShiftDetailsBloc, ShiftDetailsState>(
listener: (context, state) {
if (state is ShiftActionSuccess) {
@@ -164,7 +169,8 @@ class ShiftDetailsPage extends StatelessWidget {
}
final duration = _calculateDuration(displayShift);
final estimatedTotal = (displayShift.hourlyRate) * duration;
final estimatedTotal =
displayShift.totalValue ?? (displayShift.hourlyRate * duration);
final openSlots =
(displayShift.requiredSlots ?? 0) -
(displayShift.filledSlots ?? 0);
@@ -457,20 +463,28 @@ class ShiftDetailsPage extends StatelessWidget {
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: () =>
_bookShift(context, displayShift!.id),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF10B981),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 16,
if ((displayShift!.hasApplied != true) &&
(displayShift!.requiredSlots == null ||
displayShift!.filledSlots == null ||
displayShift!.filledSlots! <
displayShift!.requiredSlots!))
Expanded(
child: ElevatedButton(
onPressed: () => _bookShift(
context,
displayShift!.id,
displayShift!.roleId,
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF10B981),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 16,
),
),
child: const Text("Book Shift"),
),
child: const Text("Book Shift"),
),
),
],
),
SizedBox(
@@ -489,7 +503,7 @@ class ShiftDetailsPage extends StatelessWidget {
);
}
void _bookShift(BuildContext context, String id) {
void _bookShift(BuildContext context, String id, String? roleId) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
@@ -505,7 +519,7 @@ class ShiftDetailsPage extends StatelessWidget {
Navigator.of(ctx).pop();
BlocProvider.of<ShiftDetailsBloc>(
context,
).add(BookShiftDetailsEvent(id));
).add(BookShiftDetailsEvent(id, roleId: roleId));
},
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF10B981),