Merge pull request #444 from Oloodi/staff_recurring_permanent_order

Staff recurring permanent order
This commit is contained in:
Achintha Isuru
2026-02-20 10:18:03 -05:00
committed by GitHub
18 changed files with 487 additions and 39 deletions

View File

@@ -98,7 +98,11 @@ extension StaffNavigator on IModularNavigator {
/// Parameters: /// Parameters:
/// * [selectedDate] - Optional date to pre-select in the shifts view /// * [selectedDate] - Optional date to pre-select in the shifts view
/// * [initialTab] - Optional initial tab (via query parameter) /// * [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>{}; final Map<String, dynamic> args = <String, dynamic>{};
if (selectedDate != null) { if (selectedDate != null) {
args['selectedDate'] = selectedDate; args['selectedDate'] = selectedDate;
@@ -106,6 +110,9 @@ extension StaffNavigator on IModularNavigator {
if (initialTab != null) { if (initialTab != null) {
args['initialTab'] = initialTab; args['initialTab'] = initialTab;
} }
if (refreshAvailable == true) {
args['refreshAvailable'] = true;
}
navigate( navigate(
StaffPaths.shifts, StaffPaths.shifts,
arguments: args.isEmpty ? null : args, arguments: args.isEmpty ? null : args,

View File

@@ -31,6 +31,9 @@ class Shift extends Equatable {
final bool? hasApplied; final bool? hasApplied;
final double? totalValue; final double? totalValue;
final Break? breakInfo; final Break? breakInfo;
final String? orderId;
final String? orderType;
final List<ShiftSchedule>? schedules;
const Shift({ const Shift({
required this.id, required this.id,
@@ -62,6 +65,9 @@ class Shift extends Equatable {
this.hasApplied, this.hasApplied,
this.totalValue, this.totalValue,
this.breakInfo, this.breakInfo,
this.orderId,
this.orderType,
this.schedules,
}); });
@override @override
@@ -95,9 +101,27 @@ class Shift extends Equatable {
hasApplied, hasApplied,
totalValue, totalValue,
breakInfo, 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 { class ShiftManager extends Equatable {
const ShiftManager({required this.name, required this.phone, this.avatar}); const ShiftManager({required this.name, required this.phone, this.avatar});

View File

@@ -105,7 +105,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
.state(hub.state) .state(hub.state)
.street(hub.street) .street(hub.street)
.country(hub.country) .country(hub.country)
.status(dc.ShiftStatus.CONFIRMED) .status(dc.ShiftStatus.OPEN)
.workersNeeded(workersNeeded) .workersNeeded(workersNeeded)
.filled(0) .filled(0)
.durationDays(1) .durationDays(1)
@@ -224,7 +224,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
.state(hub.state) .state(hub.state)
.street(hub.street) .street(hub.street)
.country(hub.country) .country(hub.country)
.status(dc.ShiftStatus.CONFIRMED) .status(dc.ShiftStatus.OPEN)
.workersNeeded(workersNeeded) .workersNeeded(workersNeeded)
.filled(0) .filled(0)
.durationDays(1) .durationDays(1)
@@ -342,7 +342,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
.state(hub.state) .state(hub.state)
.street(hub.street) .street(hub.street)
.country(hub.country) .country(hub.country)
.status(dc.ShiftStatus.CONFIRMED) .status(dc.ShiftStatus.OPEN)
.workersNeeded(workersNeeded) .workersNeeded(workersNeeded)
.filled(0) .filled(0)
.durationDays(1) .durationDays(1)

View File

@@ -20,6 +20,42 @@ class PermanentOrderView extends StatelessWidget {
/// Creates a [PermanentOrderView]. /// Creates a [PermanentOrderView].
const PermanentOrderView({super.key}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final TranslationsClientCreateOrderPermanentEn labels = final TranslationsClientCreateOrderPermanentEn labels =
@@ -42,6 +78,10 @@ class PermanentOrderView extends StatelessWidget {
}, },
builder: (BuildContext context, PermanentOrderState state) { builder: (BuildContext context, PermanentOrderState state) {
if (state.status == PermanentOrderStatus.success) { if (state.status == PermanentOrderStatus.success) {
final DateTime initialDate = _firstPermanentShiftDate(
state.startDate,
state.permanentDays,
);
return PermanentOrderSuccessView( return PermanentOrderSuccessView(
title: labels.title, title: labels.title,
message: labels.subtitle, message: labels.subtitle,
@@ -50,7 +90,7 @@ class PermanentOrderView extends StatelessWidget {
ClientPaths.orders, ClientPaths.orders,
(_) => false, (_) => false,
arguments: <String, dynamic>{ arguments: <String, dynamic>{
'initialDate': state.startDate.toIso8601String(), 'initialDate': initialDate.toIso8601String(),
}, },
), ),
); );

View File

@@ -20,6 +20,43 @@ class RecurringOrderView extends StatelessWidget {
/// Creates a [RecurringOrderView]. /// Creates a [RecurringOrderView].
const RecurringOrderView({super.key}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final TranslationsClientCreateOrderRecurringEn labels = final TranslationsClientCreateOrderRecurringEn labels =
@@ -44,6 +81,15 @@ class RecurringOrderView extends StatelessWidget {
}, },
builder: (BuildContext context, RecurringOrderState state) { builder: (BuildContext context, RecurringOrderState state) {
if (state.status == RecurringOrderStatus.success) { 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( return RecurringOrderSuccessView(
title: labels.title, title: labels.title,
message: labels.subtitle, message: labels.subtitle,
@@ -52,7 +98,7 @@ class RecurringOrderView extends StatelessWidget {
ClientPaths.orders, ClientPaths.orders,
(_) => false, (_) => false,
arguments: <String, dynamic>{ arguments: <String, dynamic>{
'initialDate': state.startDate.toIso8601String(), 'initialDate': initialDate.toIso8601String(),
}, },
), ),
); );

View File

@@ -265,7 +265,7 @@ class _ShiftOrderFormSheetState extends State<ShiftOrderFormSheet> {
.state(selectedHub.state) .state(selectedHub.state)
.street(selectedHub.street) .street(selectedHub.street)
.country(selectedHub.country) .country(selectedHub.country)
.status(dc.ShiftStatus.PENDING) .status(dc.ShiftStatus.OPEN)
.workersNeeded(workersNeeded) .workersNeeded(workersNeeded)
.filled(0) .filled(0)
.durationDays(1) .durationDays(1)

View File

@@ -95,11 +95,12 @@ class ShiftsRepositoryImpl
DateTime? end, DateTime? end,
}) async { }) async {
final staffId = await _service.getStaffId(); final staffId = await _service.getStaffId();
var query = _service.connector.getApplicationsByStaffId(staffId: staffId); var query = _service.connector.getMyApplicationsByStaffId(staffId: staffId);
if (start != null && end != null) { if (start != null && end != null) {
query = query.dayStart(_service.toTimestamp(start)).dayEnd(_service.toTimestamp(end)); query = query.dayStart(_service.toTimestamp(start)).dayEnd(_service.toTimestamp(end));
} }
final fdc.QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> response = await _service.executeProtected(() => query.execute()); final fdc.QueryResult<dc.GetMyApplicationsByStaffIdData, dc.GetMyApplicationsByStaffIdVariables> response =
await _service.executeProtected(() => query.execute());
final apps = response.data.applications; final apps = response.data.applications;
final List<Shift> shifts = []; final List<Shift> shifts = [];
@@ -229,6 +230,8 @@ class ShiftsRepositoryImpl
filledSlots: sr.assigned ?? 0, filledSlots: sr.assigned ?? 0,
latitude: sr.shift.latitude, latitude: sr.shift.latitude,
longitude: sr.shift.longitude, longitude: sr.shift.longitude,
orderId: sr.shift.order.id,
orderType: sr.shift.order.orderType?.stringValue,
breakInfo: BreakAdapter.fromData( breakInfo: BreakAdapter.fromData(
isPaid: sr.isBreakPaid ?? false, isPaid: sr.isBreakPaid ?? false,
breakTime: sr.breakType?.stringValue, breakTime: sr.breakType?.stringValue,

View File

@@ -112,9 +112,15 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
) async { ) async {
final currentState = state; final currentState = state;
if (currentState is! ShiftsLoaded) return; 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( await handleError(
emit: emit, emit: emit,
action: () async { action: () async {

View File

@@ -12,7 +12,13 @@ class LoadShiftsEvent extends ShiftsEvent {}
class LoadHistoryShiftsEvent 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 {} class LoadFindFirstEvent extends ShiftsEvent {}

View File

@@ -93,7 +93,11 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
message: state.message, message: state.message,
type: UiSnackbarType.success, type: UiSnackbarType.success,
); );
Modular.to.toShifts(selectedDate: state.shiftDate); Modular.to.toShifts(
selectedDate: state.shiftDate,
initialTab: 'find',
refreshAvailable: true,
);
} else if (state is ShiftDetailsError) { } else if (state is ShiftDetailsError) {
if (_isApplying) { if (_isApplying) {
UiSnackbar.show( 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 i18n = Translations.of(context).staff_shifts.shift_details;
final duration = _calculateDuration(displayShift); final duration = _calculateDuration(displayShift);

View File

@@ -12,7 +12,13 @@ import '../widgets/tabs/history_shifts_tab.dart';
class ShiftsPage extends StatefulWidget { class ShiftsPage extends StatefulWidget {
final String? initialTab; final String? initialTab;
final DateTime? selectedDate; 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 @override
State<ShiftsPage> createState() => _ShiftsPageState(); State<ShiftsPage> createState() => _ShiftsPageState();
@@ -22,6 +28,8 @@ class _ShiftsPageState extends State<ShiftsPage> {
late String _activeTab; late String _activeTab;
DateTime? _selectedDate; DateTime? _selectedDate;
bool _prioritizeFind = false; bool _prioritizeFind = false;
bool _refreshAvailable = false;
bool _pendingAvailableRefresh = false;
final ShiftsBloc _bloc = Modular.get<ShiftsBloc>(); final ShiftsBloc _bloc = Modular.get<ShiftsBloc>();
@override @override
@@ -30,6 +38,8 @@ class _ShiftsPageState extends State<ShiftsPage> {
_activeTab = widget.initialTab ?? 'myshifts'; _activeTab = widget.initialTab ?? 'myshifts';
_selectedDate = widget.selectedDate; _selectedDate = widget.selectedDate;
_prioritizeFind = widget.initialTab == 'find'; _prioritizeFind = widget.initialTab == 'find';
_refreshAvailable = widget.refreshAvailable;
_pendingAvailableRefresh = widget.refreshAvailable;
if (_prioritizeFind) { if (_prioritizeFind) {
_bloc.add(LoadFindFirstEvent()); _bloc.add(LoadFindFirstEvent());
} else { } else {
@@ -40,7 +50,9 @@ class _ShiftsPageState extends State<ShiftsPage> {
} }
if (_activeTab == 'find') { if (_activeTab == 'find') {
if (!_prioritizeFind) { if (!_prioritizeFind) {
_bloc.add(LoadAvailableShiftsEvent()); _bloc.add(
LoadAvailableShiftsEvent(force: _refreshAvailable),
);
} }
} }
// Check profile completion // Check profile completion
@@ -61,6 +73,10 @@ class _ShiftsPageState extends State<ShiftsPage> {
_selectedDate = widget.selectedDate; _selectedDate = widget.selectedDate;
}); });
} }
if (widget.refreshAvailable) {
_refreshAvailable = true;
_pendingAvailableRefresh = true;
}
} }
@override @override
@@ -79,6 +95,10 @@ class _ShiftsPageState extends State<ShiftsPage> {
} }
}, },
builder: (context, state) { builder: (context, state) {
if (_pendingAvailableRefresh && state is ShiftsLoaded) {
_pendingAvailableRefresh = false;
_bloc.add(const LoadAvailableShiftsEvent(force: true));
}
final bool baseLoaded = state is ShiftsLoaded; final bool baseLoaded = state is ShiftsLoaded;
final List<Shift> myShifts = (state is ShiftsLoaded) final List<Shift> myShifts = (state is ShiftsLoaded)
? state.myShifts ? state.myShifts

View File

@@ -77,6 +77,13 @@ class _MyShiftCardState extends State<MyShiftCard> {
String _getShiftType() { String _getShiftType() {
// Handling potential localization key availability // Handling potential localization key availability
try { 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) { if (widget.shift.durationDays != null && widget.shift.durationDays! > 30) {
return t.staff_shifts.filter.long_term; return t.staff_shifts.filter.long_term;
} }
@@ -133,6 +140,24 @@ class _MyShiftCardState extends State<MyShiftCard> {
statusText = status?.toUpperCase() ?? ""; 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( return GestureDetector(
onTap: () { onTap: () {
Modular.to.pushNamed( Modular.to.pushNamed(
@@ -191,8 +216,8 @@ class _MyShiftCardState extends State<MyShiftCard> {
letterSpacing: 0.5, letterSpacing: 0.5,
), ),
), ),
// Shift Type Badge // Shift Type Badge (Order type)
if (status == 'open' || status == 'pending') ...[ if ((widget.shift.orderType ?? '').isNotEmpty) ...[
const SizedBox(width: UiConstants.space2), const SizedBox(width: UiConstants.space2),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -200,13 +225,14 @@ class _MyShiftCardState extends State<MyShiftCard> {
vertical: 2, vertical: 2,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: UiColors.primary.withValues(alpha: 0.1), color: UiColors.background,
borderRadius: UiConstants.radiusSm, borderRadius: UiConstants.radiusSm,
border: Border.all(color: UiColors.border),
), ),
child: Text( child: Text(
_getShiftType(), _getShiftType(),
style: UiTypography.footnote2m.copyWith( style: UiTypography.footnote2m.copyWith(
color: UiColors.primary, color: UiColors.textSecondary,
), ),
), ),
), ),
@@ -299,7 +325,55 @@ class _MyShiftCardState extends State<MyShiftCard> {
const SizedBox(height: UiConstants.space2), const SizedBox(height: UiConstants.space2),
// Date & Time // 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) ...[ widget.shift.durationDays! > 1) ...[
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -327,13 +401,18 @@ class _MyShiftCardState extends State<MyShiftCard> {
padding: const EdgeInsets.only(bottom: 2), padding: const EdgeInsets.only(bottom: 2),
child: Text( child: Text(
'${_formatDate(widget.shift.date)}, ${_formatTime(widget.shift.startTime)} ${_formatTime(widget.shift.endTime)}', '${_formatDate(widget.shift.date)}, ${_formatTime(widget.shift.startTime)} ${_formatTime(widget.shift.endTime)}',
style: UiTypography.footnote2r.copyWith(color: UiColors.primary), style: UiTypography.footnote2r.copyWith(
color: UiColors.primary,
),
), ),
), ),
if (widget.shift.durationDays! > 1) if (widget.shift.durationDays! > 1)
Text( Text(
'... +${widget.shift.durationDays! - 1} more days', '... +${widget.shift.durationDays! - 1} more days',
style: UiTypography.footnote2r.copyWith(color: UiColors.primary.withOpacity(0.7)), style: UiTypography.footnote2r.copyWith(
color:
UiColors.primary.withOpacity(0.7),
),
) )
], ],
), ),

View File

@@ -20,6 +20,119 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
String _searchQuery = ''; String _searchQuery = '';
String _jobType = 'all'; 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) { Widget _buildFilterTab(String id, String label) {
final isSelected = _jobType == id; final isSelected = _jobType == id;
return GestureDetector( return GestureDetector(
@@ -49,8 +162,10 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final groupedJobs = _groupMultiDayShifts(widget.availableJobs);
// Filter logic // Filter logic
final filteredJobs = widget.availableJobs.where((s) { final filteredJobs = groupedJobs.where((s) {
final matchesSearch = final matchesSearch =
s.title.toLowerCase().contains(_searchQuery.toLowerCase()) || s.title.toLowerCase().contains(_searchQuery.toLowerCase()) ||
s.location.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 == 'all') return true;
if (_jobType == 'one-day') { if (_jobType == 'one-day') {
if (_isRecurring(s) || _isPermanent(s)) return false;
return s.durationDays == null || s.durationDays! <= 1; return s.durationDays == null || s.durationDays! <= 1;
} }
if (_jobType == 'multi-day') { 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; return true;
}).toList(); }).toList();

View File

@@ -66,6 +66,7 @@ class StaffShiftsModule extends Module {
return ShiftsPage( return ShiftsPage(
initialTab: queryParams['tab'] ?? args?['initialTab'], initialTab: queryParams['tab'] ?? args?['initialTab'],
selectedDate: args?['selectedDate'], selectedDate: args?['selectedDate'],
refreshAvailable: args?['refreshAvailable'] == true,
); );
}, },
); );

View File

@@ -356,6 +356,95 @@ query getApplicationsByStaffId(
} }
} }
query getMyApplicationsByStaffId(
$staffId: UUID!
$offset: Int
$limit: Int
$dayStart: Timestamp
$dayEnd: Timestamp
) @auth(level: USER) {
applications(
where: {
staffId: { eq: $staffId }
status: { in: [ CONFIRMED, CHECKED_IN, CHECKED_OUT, LATE, PENDING] }
shift: {
date: { ge: $dayStart, le: $dayEnd }
}
}
offset: $offset
limit: $limit
) {
id
shiftId
staffId
status
appliedAt
checkInTime
checkOutTime
origin
createdAt
shift {
id
title
date
startTime
endTime
location
status
durationDays
description
latitude
longitude
order {
id
eventName
#location
teamHub {
address
placeId
hubName
}
business {
id
businessName
email
contactName
companyLogoUrl
}
vendor {
id
companyName
}
}
}
shiftRole {
id
roleId
count
assigned
startTime
endTime
hours
breakType
isBreakPaid
totalValue
role {
id
name
costPerHour
}
}
}
}
query vaidateDayStaffApplication( query vaidateDayStaffApplication(
$staffId: UUID! $staffId: UUID!
$offset: Int $offset: Int
@@ -695,10 +784,14 @@ query listCompletedApplicationsByStaffId(
durationDays durationDays
latitude latitude
longitude longitude
orderId
order { order {
id id
eventName eventName
orderType
startDate
endDate
teamHub { teamHub {
address address

View File

@@ -254,7 +254,7 @@ query listShiftRolesByVendorId(
shiftRoles( shiftRoles(
where: { where: {
shift: { shift: {
status: {in: [IN_PROGRESS, CONFIRMED, ASSIGNED, OPEN, PENDING]} #IN_PROGRESS? PENDING? status: {in: [IN_PROGRESS, ASSIGNED, OPEN]} #IN_PROGRESS?
order: { order: {
vendorId: { eq: $vendorId } vendorId: { eq: $vendorId }
} }
@@ -511,7 +511,7 @@ query getCompletedShiftsByBusinessId(
shifts( shifts(
where: { where: {
order: { businessId: { eq: $businessId } } order: { businessId: { eq: $businessId } }
status: {in: [IN_PROGRESS, CONFIRMED, COMPLETED, OPEN]} status: {in: [IN_PROGRESS, COMPLETED, OPEN]}
date: { ge: $dateFrom, le: $dateTo } date: { ge: $dateFrom, le: $dateTo }
} }
offset: $offset offset: $offset

View File

@@ -927,7 +927,7 @@ mutation seedAll @transaction {
placeId: "Eiw0MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" placeId: "Eiw0MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw"
latitude: 34.2611486 latitude: 34.2611486
longitude: -118.5010287 longitude: -118.5010287
status: ASSIGNED status: OPEN
workersNeeded: 2 workersNeeded: 2
filled: 1 filled: 1
} }
@@ -950,7 +950,7 @@ mutation seedAll @transaction {
placeId: "Eiw2ODAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" placeId: "Eiw2ODAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw"
latitude: 34.2611486 latitude: 34.2611486
longitude: -118.5010287 longitude: -118.5010287
status: ASSIGNED status: OPEN
workersNeeded: 2 workersNeeded: 2
filled: 1 filled: 1
} }
@@ -996,7 +996,7 @@ mutation seedAll @transaction {
placeId: "Eiw0MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" placeId: "Eiw0MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw"
latitude: 34.2611486 latitude: 34.2611486
longitude: -118.5010287 longitude: -118.5010287
status: ASSIGNED status: OPEN
workersNeeded: 2 workersNeeded: 2
filled: 1 filled: 1
} }
@@ -1042,7 +1042,7 @@ mutation seedAll @transaction {
placeId: "Eiw1MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw" placeId: "Eiw1MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw"
latitude: 34.2611486 latitude: 34.2611486
longitude: -118.5010287 longitude: -118.5010287
status: ASSIGNED status: OPEN
workersNeeded: 2 workersNeeded: 2
filled: 1 filled: 1
} }

View File

@@ -1,9 +1,7 @@
enum ShiftStatus { enum ShiftStatus {
DRAFT DRAFT
FILLED FILLED
PENDING
ASSIGNED ASSIGNED
CONFIRMED
OPEN OPEN
IN_PROGRESS IN_PROGRESS
COMPLETED COMPLETED