Merge branch 'dev' into feature/session-persistence-424

This commit is contained in:
2026-02-20 20:56:18 +05:30
committed by GitHub
22 changed files with 1296 additions and 132 deletions

View File

@@ -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,

View File

@@ -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});

View File

@@ -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)

View File

@@ -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(),
},
),
);

View File

@@ -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(),
},
),
);

View File

@@ -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)

View File

@@ -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

View File

@@ -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 {

View File

@@ -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 {}

View File

@@ -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);

View File

@@ -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

View File

@@ -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 ...[

View File

@@ -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();

View File

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

View File

@@ -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"

View File

@@ -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