Merge branch 'dev' into feature/session-persistence-424
This commit is contained in:
@@ -98,7 +98,11 @@ extension StaffNavigator on IModularNavigator {
|
||||
/// Parameters:
|
||||
/// * [selectedDate] - Optional date to pre-select in the shifts view
|
||||
/// * [initialTab] - Optional initial tab (via query parameter)
|
||||
void toShifts({DateTime? selectedDate, String? initialTab}) {
|
||||
void toShifts({
|
||||
DateTime? selectedDate,
|
||||
String? initialTab,
|
||||
bool? refreshAvailable,
|
||||
}) {
|
||||
final Map<String, dynamic> args = <String, dynamic>{};
|
||||
if (selectedDate != null) {
|
||||
args['selectedDate'] = selectedDate;
|
||||
@@ -106,6 +110,9 @@ extension StaffNavigator on IModularNavigator {
|
||||
if (initialTab != null) {
|
||||
args['initialTab'] = initialTab;
|
||||
}
|
||||
if (refreshAvailable == true) {
|
||||
args['refreshAvailable'] = true;
|
||||
}
|
||||
navigate(
|
||||
StaffPaths.shifts,
|
||||
arguments: args.isEmpty ? null : args,
|
||||
|
||||
@@ -2,6 +2,38 @@ import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/src/entities/shifts/break/break.dart';
|
||||
|
||||
class Shift extends Equatable {
|
||||
final String id;
|
||||
final String title;
|
||||
final String clientName;
|
||||
final String? logoUrl;
|
||||
final double hourlyRate;
|
||||
final String location;
|
||||
final String locationAddress;
|
||||
final String date;
|
||||
final String startTime;
|
||||
final String endTime;
|
||||
final String createdDate;
|
||||
final bool? tipsAvailable;
|
||||
final bool? travelTime;
|
||||
final bool? mealProvided;
|
||||
final bool? parkingAvailable;
|
||||
final bool? gasCompensation;
|
||||
final String? description;
|
||||
final String? instructions;
|
||||
final List<ShiftManager>? managers;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final String? status;
|
||||
final int? durationDays; // For multi-day shifts
|
||||
final int? requiredSlots;
|
||||
final int? filledSlots;
|
||||
final String? roleId;
|
||||
final bool? hasApplied;
|
||||
final double? totalValue;
|
||||
final Break? breakInfo;
|
||||
final String? orderId;
|
||||
final String? orderType;
|
||||
final List<ShiftSchedule>? schedules;
|
||||
|
||||
const Shift({
|
||||
required this.id,
|
||||
@@ -33,6 +65,9 @@ class Shift extends Equatable {
|
||||
this.hasApplied,
|
||||
this.totalValue,
|
||||
this.breakInfo,
|
||||
this.orderId,
|
||||
this.orderType,
|
||||
this.schedules,
|
||||
});
|
||||
final String id;
|
||||
final String title;
|
||||
@@ -95,9 +130,27 @@ class Shift extends Equatable {
|
||||
hasApplied,
|
||||
totalValue,
|
||||
breakInfo,
|
||||
orderId,
|
||||
orderType,
|
||||
schedules,
|
||||
];
|
||||
}
|
||||
|
||||
class ShiftSchedule extends Equatable {
|
||||
const ShiftSchedule({
|
||||
required this.date,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
});
|
||||
|
||||
final String date;
|
||||
final String startTime;
|
||||
final String endTime;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[date, startTime, endTime];
|
||||
}
|
||||
|
||||
class ShiftManager extends Equatable {
|
||||
const ShiftManager({required this.name, required this.phone, this.avatar});
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
.state(hub.state)
|
||||
.street(hub.street)
|
||||
.country(hub.country)
|
||||
.status(dc.ShiftStatus.CONFIRMED)
|
||||
.status(dc.ShiftStatus.OPEN)
|
||||
.workersNeeded(workersNeeded)
|
||||
.filled(0)
|
||||
.durationDays(1)
|
||||
@@ -224,7 +224,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
.state(hub.state)
|
||||
.street(hub.street)
|
||||
.country(hub.country)
|
||||
.status(dc.ShiftStatus.CONFIRMED)
|
||||
.status(dc.ShiftStatus.OPEN)
|
||||
.workersNeeded(workersNeeded)
|
||||
.filled(0)
|
||||
.durationDays(1)
|
||||
@@ -342,7 +342,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
.state(hub.state)
|
||||
.street(hub.street)
|
||||
.country(hub.country)
|
||||
.status(dc.ShiftStatus.CONFIRMED)
|
||||
.status(dc.ShiftStatus.OPEN)
|
||||
.workersNeeded(workersNeeded)
|
||||
.filled(0)
|
||||
.durationDays(1)
|
||||
|
||||
@@ -20,6 +20,42 @@ class PermanentOrderView extends StatelessWidget {
|
||||
/// Creates a [PermanentOrderView].
|
||||
const PermanentOrderView({super.key});
|
||||
|
||||
DateTime _firstPermanentShiftDate(
|
||||
DateTime startDate,
|
||||
List<String> permanentDays,
|
||||
) {
|
||||
final DateTime start = DateTime(startDate.year, startDate.month, startDate.day);
|
||||
final DateTime end = start.add(const Duration(days: 29));
|
||||
final Set<String> selected = permanentDays.toSet();
|
||||
for (DateTime day = start; !day.isAfter(end); day = day.add(const Duration(days: 1))) {
|
||||
if (selected.contains(_weekdayLabel(day))) {
|
||||
return day;
|
||||
}
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
String _weekdayLabel(DateTime date) {
|
||||
switch (date.weekday) {
|
||||
case DateTime.monday:
|
||||
return 'MON';
|
||||
case DateTime.tuesday:
|
||||
return 'TUE';
|
||||
case DateTime.wednesday:
|
||||
return 'WED';
|
||||
case DateTime.thursday:
|
||||
return 'THU';
|
||||
case DateTime.friday:
|
||||
return 'FRI';
|
||||
case DateTime.saturday:
|
||||
return 'SAT';
|
||||
case DateTime.sunday:
|
||||
return 'SUN';
|
||||
default:
|
||||
return 'SUN';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsClientCreateOrderPermanentEn labels =
|
||||
@@ -42,6 +78,10 @@ class PermanentOrderView extends StatelessWidget {
|
||||
},
|
||||
builder: (BuildContext context, PermanentOrderState state) {
|
||||
if (state.status == PermanentOrderStatus.success) {
|
||||
final DateTime initialDate = _firstPermanentShiftDate(
|
||||
state.startDate,
|
||||
state.permanentDays,
|
||||
);
|
||||
return PermanentOrderSuccessView(
|
||||
title: labels.title,
|
||||
message: labels.subtitle,
|
||||
@@ -50,7 +90,7 @@ class PermanentOrderView extends StatelessWidget {
|
||||
ClientPaths.orders,
|
||||
(_) => false,
|
||||
arguments: <String, dynamic>{
|
||||
'initialDate': state.startDate.toIso8601String(),
|
||||
'initialDate': initialDate.toIso8601String(),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -20,6 +20,43 @@ class RecurringOrderView extends StatelessWidget {
|
||||
/// Creates a [RecurringOrderView].
|
||||
const RecurringOrderView({super.key});
|
||||
|
||||
DateTime _firstRecurringShiftDate(
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
List<String> recurringDays,
|
||||
) {
|
||||
final DateTime start = DateTime(startDate.year, startDate.month, startDate.day);
|
||||
final DateTime end = DateTime(endDate.year, endDate.month, endDate.day);
|
||||
final Set<String> selected = recurringDays.toSet();
|
||||
for (DateTime day = start; !day.isAfter(end); day = day.add(const Duration(days: 1))) {
|
||||
if (selected.contains(_weekdayLabel(day))) {
|
||||
return day;
|
||||
}
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
String _weekdayLabel(DateTime date) {
|
||||
switch (date.weekday) {
|
||||
case DateTime.monday:
|
||||
return 'MON';
|
||||
case DateTime.tuesday:
|
||||
return 'TUE';
|
||||
case DateTime.wednesday:
|
||||
return 'WED';
|
||||
case DateTime.thursday:
|
||||
return 'THU';
|
||||
case DateTime.friday:
|
||||
return 'FRI';
|
||||
case DateTime.saturday:
|
||||
return 'SAT';
|
||||
case DateTime.sunday:
|
||||
return 'SUN';
|
||||
default:
|
||||
return 'SUN';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsClientCreateOrderRecurringEn labels =
|
||||
@@ -44,6 +81,15 @@ class RecurringOrderView extends StatelessWidget {
|
||||
},
|
||||
builder: (BuildContext context, RecurringOrderState state) {
|
||||
if (state.status == RecurringOrderStatus.success) {
|
||||
final DateTime maxEndDate =
|
||||
state.startDate.add(const Duration(days: 29));
|
||||
final DateTime effectiveEndDate =
|
||||
state.endDate.isAfter(maxEndDate) ? maxEndDate : state.endDate;
|
||||
final DateTime initialDate = _firstRecurringShiftDate(
|
||||
state.startDate,
|
||||
effectiveEndDate,
|
||||
state.recurringDays,
|
||||
);
|
||||
return RecurringOrderSuccessView(
|
||||
title: labels.title,
|
||||
message: labels.subtitle,
|
||||
@@ -52,7 +98,7 @@ class RecurringOrderView extends StatelessWidget {
|
||||
ClientPaths.orders,
|
||||
(_) => false,
|
||||
arguments: <String, dynamic>{
|
||||
'initialDate': state.startDate.toIso8601String(),
|
||||
'initialDate': initialDate.toIso8601String(),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -265,7 +265,7 @@ class _ShiftOrderFormSheetState extends State<ShiftOrderFormSheet> {
|
||||
.state(selectedHub.state)
|
||||
.street(selectedHub.street)
|
||||
.country(selectedHub.country)
|
||||
.status(dc.ShiftStatus.PENDING)
|
||||
.status(dc.ShiftStatus.OPEN)
|
||||
.workersNeeded(workersNeeded)
|
||||
.filled(0)
|
||||
.durationDays(1)
|
||||
|
||||
@@ -46,6 +46,145 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
Future<List<Shift>> getHistoryShifts() async {
|
||||
final staffId = await _service.getStaffId();
|
||||
return _connectorRepository.getHistoryShifts(staffId: staffId);
|
||||
final fdc.QueryResult<dc.ListCompletedApplicationsByStaffIdData, dc.ListCompletedApplicationsByStaffIdVariables> response = await _service.executeProtected(() => _service.connector
|
||||
.listCompletedApplicationsByStaffId(staffId: staffId)
|
||||
.execute());
|
||||
final List<Shift> shifts = [];
|
||||
|
||||
for (final app in response.data.applications) {
|
||||
_shiftToAppIdMap[app.shift.id] = app.id;
|
||||
_appToRoleIdMap[app.id] = app.shiftRole.id;
|
||||
|
||||
final String roleName = app.shiftRole.role.name;
|
||||
final String orderName =
|
||||
(app.shift.order.eventName ?? '').trim().isNotEmpty
|
||||
? app.shift.order.eventName!
|
||||
: app.shift.order.business.businessName;
|
||||
final String title = '$roleName - $orderName';
|
||||
final DateTime? shiftDate = _service.toDateTime(app.shift.date);
|
||||
final DateTime? startDt = _service.toDateTime(app.shiftRole.startTime);
|
||||
final DateTime? endDt = _service.toDateTime(app.shiftRole.endTime);
|
||||
final DateTime? createdDt = _service.toDateTime(app.createdAt);
|
||||
|
||||
shifts.add(
|
||||
Shift(
|
||||
id: app.shift.id,
|
||||
roleId: app.shiftRole.roleId,
|
||||
title: title,
|
||||
clientName: app.shift.order.business.businessName,
|
||||
logoUrl: app.shift.order.business.companyLogoUrl,
|
||||
hourlyRate: app.shiftRole.role.costPerHour,
|
||||
location: app.shift.location ?? '',
|
||||
locationAddress: app.shift.order.teamHub.hubName,
|
||||
date: shiftDate?.toIso8601String() ?? '',
|
||||
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: _mapStatus(dc.ApplicationStatus.CHECKED_OUT),
|
||||
description: app.shift.description,
|
||||
durationDays: app.shift.durationDays,
|
||||
requiredSlots: app.shiftRole.count,
|
||||
filledSlots: app.shiftRole.assigned ?? 0,
|
||||
hasApplied: true,
|
||||
latitude: app.shift.latitude,
|
||||
longitude: app.shift.longitude,
|
||||
breakInfo: BreakAdapter.fromData(
|
||||
isPaid: app.shiftRole.isBreakPaid ?? false,
|
||||
breakTime: app.shiftRole.breakType?.stringValue,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return shifts;
|
||||
}
|
||||
|
||||
Future<List<Shift>> _fetchApplications({
|
||||
DateTime? start,
|
||||
DateTime? end,
|
||||
}) async {
|
||||
final staffId = await _service.getStaffId();
|
||||
var query = _service.connector.getMyApplicationsByStaffId(staffId: staffId);
|
||||
if (start != null && end != null) {
|
||||
query = query.dayStart(_service.toTimestamp(start)).dayEnd(_service.toTimestamp(end));
|
||||
}
|
||||
final fdc.QueryResult<dc.GetMyApplicationsByStaffIdData, dc.GetMyApplicationsByStaffIdVariables> response =
|
||||
await _service.executeProtected(() => query.execute());
|
||||
|
||||
final apps = response.data.applications;
|
||||
final List<Shift> shifts = [];
|
||||
|
||||
for (final app in apps) {
|
||||
_shiftToAppIdMap[app.shift.id] = app.id;
|
||||
_appToRoleIdMap[app.id] = app.shiftRole.id;
|
||||
|
||||
final String roleName = app.shiftRole.role.name;
|
||||
final String orderName =
|
||||
(app.shift.order.eventName ?? '').trim().isNotEmpty
|
||||
? app.shift.order.eventName!
|
||||
: app.shift.order.business.businessName;
|
||||
final String title = '$roleName - $orderName';
|
||||
final DateTime? shiftDate = _service.toDateTime(app.shift.date);
|
||||
final DateTime? startDt = _service.toDateTime(app.shiftRole.startTime);
|
||||
final DateTime? endDt = _service.toDateTime(app.shiftRole.endTime);
|
||||
final DateTime? createdDt = _service.toDateTime(app.createdAt);
|
||||
|
||||
// Override status to reflect the application state (e.g., CHECKED_OUT, CONFIRMED)
|
||||
final bool hasCheckIn = app.checkInTime != null;
|
||||
final bool hasCheckOut = app.checkOutTime != null;
|
||||
dc.ApplicationStatus? appStatus;
|
||||
if (app.status is dc.Known<dc.ApplicationStatus>) {
|
||||
appStatus = (app.status as dc.Known<dc.ApplicationStatus>).value;
|
||||
}
|
||||
final String mappedStatus = hasCheckOut
|
||||
? 'completed'
|
||||
: hasCheckIn
|
||||
? 'checked_in'
|
||||
: _mapStatus(appStatus ?? dc.ApplicationStatus.CONFIRMED);
|
||||
shifts.add(
|
||||
Shift(
|
||||
id: app.shift.id,
|
||||
roleId: app.shiftRole.roleId,
|
||||
title: title,
|
||||
clientName: app.shift.order.business.businessName,
|
||||
logoUrl: app.shift.order.business.companyLogoUrl,
|
||||
hourlyRate: app.shiftRole.role.costPerHour,
|
||||
location: app.shift.location ?? '',
|
||||
locationAddress: app.shift.order.teamHub.hubName,
|
||||
date: shiftDate?.toIso8601String() ?? '',
|
||||
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: mappedStatus,
|
||||
description: app.shift.description,
|
||||
durationDays: app.shift.durationDays,
|
||||
requiredSlots: app.shiftRole.count,
|
||||
filledSlots: app.shiftRole.assigned ?? 0,
|
||||
hasApplied: true,
|
||||
latitude: app.shift.latitude,
|
||||
longitude: app.shift.longitude,
|
||||
breakInfo: BreakAdapter.fromData(
|
||||
isPaid: app.shiftRole.isBreakPaid ?? false,
|
||||
breakTime: app.shiftRole.breakType?.stringValue,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return shifts;
|
||||
}
|
||||
|
||||
String _mapStatus(dc.ApplicationStatus status) {
|
||||
switch (status) {
|
||||
case dc.ApplicationStatus.CONFIRMED:
|
||||
return 'confirmed';
|
||||
case dc.ApplicationStatus.PENDING:
|
||||
return 'pending';
|
||||
case dc.ApplicationStatus.CHECKED_OUT:
|
||||
return 'completed';
|
||||
case dc.ApplicationStatus.REJECTED:
|
||||
return 'cancelled';
|
||||
default:
|
||||
return 'open';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -56,6 +195,76 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
query: query,
|
||||
type: type,
|
||||
);
|
||||
final String? vendorId = dc.StaffSessionStore.instance.session?.ownerId;
|
||||
if (vendorId == null || vendorId.isEmpty) {
|
||||
return <Shift>[];
|
||||
}
|
||||
|
||||
final fdc.QueryResult<dc.ListShiftRolesByVendorIdData, dc.ListShiftRolesByVendorIdVariables> result = await _service.executeProtected(() => _service.connector
|
||||
.listShiftRolesByVendorId(vendorId: vendorId)
|
||||
.execute());
|
||||
|
||||
final allShiftRoles = result.data.shiftRoles;
|
||||
|
||||
// Fetch my applications to filter out already booked shifts
|
||||
final List<Shift> myShifts = await _fetchApplications();
|
||||
final Set<String> myShiftIds = myShifts.map((s) => s.id).toSet();
|
||||
|
||||
final List<Shift> mappedShifts = [];
|
||||
for (final sr in allShiftRoles) {
|
||||
// Skip if I have already applied/booked this shift
|
||||
if (myShiftIds.contains(sr.shiftId)) continue;
|
||||
|
||||
|
||||
final DateTime? shiftDate = _service.toDateTime(sr.shift.date);
|
||||
final startDt = _service.toDateTime(sr.startTime);
|
||||
final endDt = _service.toDateTime(sr.endTime);
|
||||
final createdDt = _service.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: sr.role.costPerHour,
|
||||
location: sr.shift.location ?? '',
|
||||
locationAddress: sr.shift.locationAddress ?? '',
|
||||
date: shiftDate?.toIso8601String() ?? '',
|
||||
startTime: startDt != null
|
||||
? DateFormat('HH:mm').format(startDt)
|
||||
: '',
|
||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: sr.shift.status?.stringValue.toLowerCase() ?? 'open',
|
||||
description: sr.shift.description,
|
||||
durationDays: sr.shift.durationDays,
|
||||
requiredSlots: sr.count,
|
||||
filledSlots: sr.assigned ?? 0,
|
||||
latitude: sr.shift.latitude,
|
||||
longitude: sr.shift.longitude,
|
||||
orderId: sr.shift.order.id,
|
||||
orderType: sr.shift.order.orderType?.stringValue,
|
||||
breakInfo: BreakAdapter.fromData(
|
||||
isPaid: sr.isBreakPaid ?? false,
|
||||
breakTime: sr.breakType?.stringValue,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (query.isNotEmpty) {
|
||||
return mappedShifts
|
||||
.where(
|
||||
(s) =>
|
||||
s.title.toLowerCase().contains(query.toLowerCase()) ||
|
||||
s.clientName.toLowerCase().contains(query.toLowerCase()),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
return mappedShifts;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -112,9 +112,15 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
|
||||
) async {
|
||||
final currentState = state;
|
||||
if (currentState is! ShiftsLoaded) return;
|
||||
if (currentState.availableLoading || currentState.availableLoaded) return;
|
||||
if (!event.force &&
|
||||
(currentState.availableLoading || currentState.availableLoaded)) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit(currentState.copyWith(availableLoading: true));
|
||||
emit(currentState.copyWith(
|
||||
availableLoading: true,
|
||||
availableLoaded: false,
|
||||
));
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
|
||||
@@ -12,7 +12,13 @@ class LoadShiftsEvent extends ShiftsEvent {}
|
||||
|
||||
class LoadHistoryShiftsEvent extends ShiftsEvent {}
|
||||
|
||||
class LoadAvailableShiftsEvent extends ShiftsEvent {}
|
||||
class LoadAvailableShiftsEvent extends ShiftsEvent {
|
||||
final bool force;
|
||||
const LoadAvailableShiftsEvent({this.force = false});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [force];
|
||||
}
|
||||
|
||||
class LoadFindFirstEvent extends ShiftsEvent {}
|
||||
|
||||
|
||||
@@ -93,7 +93,11 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
message: state.message,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
Modular.to.toShifts(selectedDate: state.shiftDate);
|
||||
Modular.to.toShifts(
|
||||
selectedDate: state.shiftDate,
|
||||
initialTab: 'find',
|
||||
refreshAvailable: true,
|
||||
);
|
||||
} else if (state is ShiftDetailsError) {
|
||||
if (_isApplying) {
|
||||
UiSnackbar.show(
|
||||
@@ -112,7 +116,8 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Shift displayShift = widget.shift;
|
||||
final Shift displayShift =
|
||||
state is ShiftDetailsLoaded ? state.shift : widget.shift;
|
||||
final i18n = Translations.of(context).staff_shifts.shift_details;
|
||||
|
||||
final duration = _calculateDuration(displayShift);
|
||||
|
||||
@@ -12,7 +12,13 @@ import '../widgets/tabs/history_shifts_tab.dart';
|
||||
class ShiftsPage extends StatefulWidget {
|
||||
final String? initialTab;
|
||||
final DateTime? selectedDate;
|
||||
const ShiftsPage({super.key, this.initialTab, this.selectedDate});
|
||||
final bool refreshAvailable;
|
||||
const ShiftsPage({
|
||||
super.key,
|
||||
this.initialTab,
|
||||
this.selectedDate,
|
||||
this.refreshAvailable = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ShiftsPage> createState() => _ShiftsPageState();
|
||||
@@ -22,6 +28,8 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
late String _activeTab;
|
||||
DateTime? _selectedDate;
|
||||
bool _prioritizeFind = false;
|
||||
bool _refreshAvailable = false;
|
||||
bool _pendingAvailableRefresh = false;
|
||||
final ShiftsBloc _bloc = Modular.get<ShiftsBloc>();
|
||||
|
||||
@override
|
||||
@@ -30,6 +38,8 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
_activeTab = widget.initialTab ?? 'myshifts';
|
||||
_selectedDate = widget.selectedDate;
|
||||
_prioritizeFind = widget.initialTab == 'find';
|
||||
_refreshAvailable = widget.refreshAvailable;
|
||||
_pendingAvailableRefresh = widget.refreshAvailable;
|
||||
if (_prioritizeFind) {
|
||||
_bloc.add(LoadFindFirstEvent());
|
||||
} else {
|
||||
@@ -40,7 +50,9 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
}
|
||||
if (_activeTab == 'find') {
|
||||
if (!_prioritizeFind) {
|
||||
_bloc.add(LoadAvailableShiftsEvent());
|
||||
_bloc.add(
|
||||
LoadAvailableShiftsEvent(force: _refreshAvailable),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Check profile completion
|
||||
@@ -61,6 +73,10 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
_selectedDate = widget.selectedDate;
|
||||
});
|
||||
}
|
||||
if (widget.refreshAvailable) {
|
||||
_refreshAvailable = true;
|
||||
_pendingAvailableRefresh = true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -79,6 +95,10 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (_pendingAvailableRefresh && state is ShiftsLoaded) {
|
||||
_pendingAvailableRefresh = false;
|
||||
_bloc.add(const LoadAvailableShiftsEvent(force: true));
|
||||
}
|
||||
final bool baseLoaded = state is ShiftsLoaded;
|
||||
final List<Shift> myShifts = (state is ShiftsLoaded)
|
||||
? state.myShifts
|
||||
|
||||
@@ -77,6 +77,13 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
String _getShiftType() {
|
||||
// Handling potential localization key availability
|
||||
try {
|
||||
final String orderType = (widget.shift.orderType ?? '').toUpperCase();
|
||||
if (orderType == 'PERMANENT') {
|
||||
return t.staff_shifts.filter.long_term;
|
||||
}
|
||||
if (orderType == 'RECURRING') {
|
||||
return t.staff_shifts.filter.multi_day;
|
||||
}
|
||||
if (widget.shift.durationDays != null && widget.shift.durationDays! > 30) {
|
||||
return t.staff_shifts.filter.long_term;
|
||||
}
|
||||
@@ -133,6 +140,24 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
statusText = status?.toUpperCase() ?? "";
|
||||
}
|
||||
|
||||
final schedules = widget.shift.schedules ?? <ShiftSchedule>[];
|
||||
final hasSchedules = schedules.isNotEmpty;
|
||||
final List<ShiftSchedule> visibleSchedules = schedules.length <= 5
|
||||
? schedules
|
||||
: schedules.take(3).toList();
|
||||
final int remainingSchedules =
|
||||
schedules.length <= 5 ? 0 : schedules.length - 3;
|
||||
final String scheduleRange = hasSchedules
|
||||
? () {
|
||||
final first = schedules.first.date;
|
||||
final last = schedules.last.date;
|
||||
if (first == last) {
|
||||
return _formatDate(first);
|
||||
}
|
||||
return '${_formatDate(first)} – ${_formatDate(last)}';
|
||||
}()
|
||||
: '';
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Modular.to.pushNamed(
|
||||
@@ -191,8 +216,8 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
// Shift Type Badge
|
||||
if (status == 'open' || status == 'pending') ...[
|
||||
// Shift Type Badge (Order type)
|
||||
if ((widget.shift.orderType ?? '').isNotEmpty) ...[
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -200,13 +225,14 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withValues(alpha: 0.1),
|
||||
color: UiColors.background,
|
||||
borderRadius: UiConstants.radiusSm,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Text(
|
||||
_getShiftType(),
|
||||
style: UiTypography.footnote2m.copyWith(
|
||||
color: UiColors.primary,
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -299,7 +325,55 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
|
||||
// Date & Time
|
||||
if (widget.shift.durationDays != null &&
|
||||
if (hasSchedules) ...[
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
UiIcons.clock,
|
||||
size: UiConstants.iconXs,
|
||||
color: UiColors.primary,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Text(
|
||||
'${schedules.length} schedules',
|
||||
style: UiTypography.footnote2m.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Text(
|
||||
scheduleRange,
|
||||
style: UiTypography.footnote2r.copyWith(color: UiColors.primary),
|
||||
),
|
||||
),
|
||||
...visibleSchedules.map(
|
||||
(schedule) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Text(
|
||||
'${_formatDate(schedule.date)}, ${_formatTime(schedule.startTime)} – ${_formatTime(schedule.endTime)}',
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (remainingSchedules > 0)
|
||||
Text(
|
||||
'+$remainingSchedules more schedules',
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color: UiColors.primary.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
] else if (widget.shift.durationDays != null &&
|
||||
widget.shift.durationDays! > 1) ...[
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -324,17 +398,22 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Text(
|
||||
'${_formatDate(widget.shift.date)}, ${_formatTime(widget.shift.startTime)} – ${_formatTime(widget.shift.endTime)}',
|
||||
style: UiTypography.footnote2r.copyWith(color: UiColors.primary),
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Text(
|
||||
'${_formatDate(widget.shift.date)}, ${_formatTime(widget.shift.startTime)} – ${_formatTime(widget.shift.endTime)}',
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.shift.durationDays! > 1)
|
||||
Text(
|
||||
'... +${widget.shift.durationDays! - 1} more days',
|
||||
style: UiTypography.footnote2r.copyWith(color: UiColors.primary.withOpacity(0.7)),
|
||||
)
|
||||
Text(
|
||||
'... +${widget.shift.durationDays! - 1} more days',
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color:
|
||||
UiColors.primary.withOpacity(0.7),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
] else ...[
|
||||
|
||||
@@ -20,6 +20,119 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
String _searchQuery = '';
|
||||
String _jobType = 'all';
|
||||
|
||||
bool _isRecurring(Shift shift) =>
|
||||
(shift.orderType ?? '').toUpperCase() == 'RECURRING';
|
||||
|
||||
bool _isPermanent(Shift shift) =>
|
||||
(shift.orderType ?? '').toUpperCase() == 'PERMANENT';
|
||||
|
||||
DateTime? _parseShiftDate(String date) {
|
||||
if (date.isEmpty) return null;
|
||||
try {
|
||||
return DateTime.parse(date);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
List<Shift> _groupMultiDayShifts(List<Shift> shifts) {
|
||||
final Map<String, List<Shift>> grouped = <String, List<Shift>>{};
|
||||
for (final shift in shifts) {
|
||||
if (!_isRecurring(shift) && !_isPermanent(shift)) {
|
||||
continue;
|
||||
}
|
||||
final orderId = shift.orderId;
|
||||
final roleId = shift.roleId;
|
||||
if (orderId == null || roleId == null) {
|
||||
continue;
|
||||
}
|
||||
final key = '$orderId::$roleId';
|
||||
grouped.putIfAbsent(key, () => <Shift>[]).add(shift);
|
||||
}
|
||||
|
||||
final Set<String> addedGroups = <String>{};
|
||||
final List<Shift> result = <Shift>[];
|
||||
|
||||
for (final shift in shifts) {
|
||||
if (!_isRecurring(shift) && !_isPermanent(shift)) {
|
||||
result.add(shift);
|
||||
continue;
|
||||
}
|
||||
final orderId = shift.orderId;
|
||||
final roleId = shift.roleId;
|
||||
if (orderId == null || roleId == null) {
|
||||
result.add(shift);
|
||||
continue;
|
||||
}
|
||||
final key = '$orderId::$roleId';
|
||||
if (addedGroups.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
addedGroups.add(key);
|
||||
final List<Shift> group = grouped[key] ?? <Shift>[];
|
||||
if (group.isEmpty) {
|
||||
result.add(shift);
|
||||
continue;
|
||||
}
|
||||
group.sort((a, b) {
|
||||
final ad = _parseShiftDate(a.date);
|
||||
final bd = _parseShiftDate(b.date);
|
||||
if (ad == null && bd == null) return 0;
|
||||
if (ad == null) return 1;
|
||||
if (bd == null) return -1;
|
||||
return ad.compareTo(bd);
|
||||
});
|
||||
|
||||
final Shift first = group.first;
|
||||
final List<ShiftSchedule> schedules = group
|
||||
.map((s) => ShiftSchedule(
|
||||
date: s.date,
|
||||
startTime: s.startTime,
|
||||
endTime: s.endTime,
|
||||
))
|
||||
.toList();
|
||||
|
||||
result.add(
|
||||
Shift(
|
||||
id: first.id,
|
||||
roleId: first.roleId,
|
||||
title: first.title,
|
||||
clientName: first.clientName,
|
||||
logoUrl: first.logoUrl,
|
||||
hourlyRate: first.hourlyRate,
|
||||
location: first.location,
|
||||
locationAddress: first.locationAddress,
|
||||
date: first.date,
|
||||
startTime: first.startTime,
|
||||
endTime: first.endTime,
|
||||
createdDate: first.createdDate,
|
||||
tipsAvailable: first.tipsAvailable,
|
||||
travelTime: first.travelTime,
|
||||
mealProvided: first.mealProvided,
|
||||
parkingAvailable: first.parkingAvailable,
|
||||
gasCompensation: first.gasCompensation,
|
||||
description: first.description,
|
||||
instructions: first.instructions,
|
||||
managers: first.managers,
|
||||
latitude: first.latitude,
|
||||
longitude: first.longitude,
|
||||
status: first.status,
|
||||
durationDays: schedules.length,
|
||||
requiredSlots: first.requiredSlots,
|
||||
filledSlots: first.filledSlots,
|
||||
hasApplied: first.hasApplied,
|
||||
totalValue: first.totalValue,
|
||||
breakInfo: first.breakInfo,
|
||||
orderId: first.orderId,
|
||||
orderType: first.orderType,
|
||||
schedules: schedules,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Widget _buildFilterTab(String id, String label) {
|
||||
final isSelected = _jobType == id;
|
||||
return GestureDetector(
|
||||
@@ -49,8 +162,10 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final groupedJobs = _groupMultiDayShifts(widget.availableJobs);
|
||||
|
||||
// Filter logic
|
||||
final filteredJobs = widget.availableJobs.where((s) {
|
||||
final filteredJobs = groupedJobs.where((s) {
|
||||
final matchesSearch =
|
||||
s.title.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
s.location.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
@@ -60,10 +175,15 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
|
||||
if (_jobType == 'all') return true;
|
||||
if (_jobType == 'one-day') {
|
||||
if (_isRecurring(s) || _isPermanent(s)) return false;
|
||||
return s.durationDays == null || s.durationDays! <= 1;
|
||||
}
|
||||
if (_jobType == 'multi-day') {
|
||||
return s.durationDays != null && s.durationDays! > 1;
|
||||
return _isRecurring(s) ||
|
||||
(s.durationDays != null && s.durationDays! > 1);
|
||||
}
|
||||
if (_jobType == 'long-term') {
|
||||
return _isPermanent(s);
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
@@ -66,6 +66,7 @@ class StaffShiftsModule extends Module {
|
||||
return ShiftsPage(
|
||||
initialTab: queryParams['tab'] ?? args?['initialTab'],
|
||||
selectedDate: args?['selectedDate'],
|
||||
refreshAvailable: args?['refreshAvailable'] == true,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1530,4 +1530,4 @@ packages:
|
||||
version: "2.2.3"
|
||||
sdks:
|
||||
dart: ">=3.10.3 <4.0.0"
|
||||
flutter: ">=3.38.4"
|
||||
flutter: ">=3.38.4 <4.0.0"
|
||||
|
||||
@@ -3,6 +3,7 @@ publish_to: 'none'
|
||||
description: "A sample project using melos and modular scaffold."
|
||||
environment:
|
||||
sdk: '>=3.10.0 <4.0.0'
|
||||
flutter: '>=3.38.0 <4.0.0'
|
||||
workspace:
|
||||
- packages/design_system
|
||||
- packages/core
|
||||
|
||||
Reference in New Issue
Block a user