Merge pull request #444 from Oloodi/staff_recurring_permanent_order
Staff recurring permanent order
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,
|
||||
|
||||
@@ -31,6 +31,9 @@ class Shift extends Equatable {
|
||||
final bool? hasApplied;
|
||||
final double? totalValue;
|
||||
final Break? breakInfo;
|
||||
final String? orderId;
|
||||
final String? orderType;
|
||||
final List<ShiftSchedule>? schedules;
|
||||
|
||||
const Shift({
|
||||
required this.id,
|
||||
@@ -62,6 +65,9 @@ class Shift extends Equatable {
|
||||
this.hasApplied,
|
||||
this.totalValue,
|
||||
this.breakInfo,
|
||||
this.orderId,
|
||||
this.orderType,
|
||||
this.schedules,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -95,9 +101,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)
|
||||
|
||||
@@ -95,11 +95,12 @@ class ShiftsRepositoryImpl
|
||||
DateTime? end,
|
||||
}) async {
|
||||
final staffId = await _service.getStaffId();
|
||||
var query = _service.connector.getApplicationsByStaffId(staffId: staffId);
|
||||
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.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 List<Shift> shifts = [];
|
||||
@@ -229,6 +230,8 @@ class ShiftsRepositoryImpl
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
$staffId: UUID!
|
||||
$offset: Int
|
||||
@@ -695,10 +784,14 @@ query listCompletedApplicationsByStaffId(
|
||||
durationDays
|
||||
latitude
|
||||
longitude
|
||||
orderId
|
||||
|
||||
order {
|
||||
id
|
||||
eventName
|
||||
orderType
|
||||
startDate
|
||||
endDate
|
||||
|
||||
teamHub {
|
||||
address
|
||||
|
||||
@@ -254,7 +254,7 @@ query listShiftRolesByVendorId(
|
||||
shiftRoles(
|
||||
where: {
|
||||
shift: {
|
||||
status: {in: [IN_PROGRESS, CONFIRMED, ASSIGNED, OPEN, PENDING]} #IN_PROGRESS? PENDING?
|
||||
status: {in: [IN_PROGRESS, ASSIGNED, OPEN]} #IN_PROGRESS?
|
||||
order: {
|
||||
vendorId: { eq: $vendorId }
|
||||
}
|
||||
@@ -511,7 +511,7 @@ query getCompletedShiftsByBusinessId(
|
||||
shifts(
|
||||
where: {
|
||||
order: { businessId: { eq: $businessId } }
|
||||
status: {in: [IN_PROGRESS, CONFIRMED, COMPLETED, OPEN]}
|
||||
status: {in: [IN_PROGRESS, COMPLETED, OPEN]}
|
||||
date: { ge: $dateFrom, le: $dateTo }
|
||||
}
|
||||
offset: $offset
|
||||
|
||||
@@ -927,7 +927,7 @@ mutation seedAll @transaction {
|
||||
placeId: "Eiw0MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw"
|
||||
latitude: 34.2611486
|
||||
longitude: -118.5010287
|
||||
status: ASSIGNED
|
||||
status: OPEN
|
||||
workersNeeded: 2
|
||||
filled: 1
|
||||
}
|
||||
@@ -950,7 +950,7 @@ mutation seedAll @transaction {
|
||||
placeId: "Eiw2ODAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw"
|
||||
latitude: 34.2611486
|
||||
longitude: -118.5010287
|
||||
status: ASSIGNED
|
||||
status: OPEN
|
||||
workersNeeded: 2
|
||||
filled: 1
|
||||
}
|
||||
@@ -996,7 +996,7 @@ mutation seedAll @transaction {
|
||||
placeId: "Eiw0MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw"
|
||||
latitude: 34.2611486
|
||||
longitude: -118.5010287
|
||||
status: ASSIGNED
|
||||
status: OPEN
|
||||
workersNeeded: 2
|
||||
filled: 1
|
||||
}
|
||||
@@ -1042,7 +1042,7 @@ mutation seedAll @transaction {
|
||||
placeId: "Eiw1MDAwIFNhbiBKb3NlIFN0cmVldCwgR3JhbmFkYSBIaWxscywgQ0EsIFVTQSIuKiwKFAoSCYNJZBTdmsKAEddGOfBj8LvTEhQKEglnNXI0zZrCgBEjR6om62lcVw"
|
||||
latitude: 34.2611486
|
||||
longitude: -118.5010287
|
||||
status: ASSIGNED
|
||||
status: OPEN
|
||||
workersNeeded: 2
|
||||
filled: 1
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
enum ShiftStatus {
|
||||
DRAFT
|
||||
FILLED
|
||||
PENDING
|
||||
ASSIGNED
|
||||
CONFIRMED
|
||||
OPEN
|
||||
IN_PROGRESS
|
||||
COMPLETED
|
||||
|
||||
Reference in New Issue
Block a user