feat: Add orderId and normalized orderType to the Shift model to enable UI grouping and type-badging in shift displays.

This commit is contained in:
Achintha Isuru
2026-02-22 11:46:38 -05:00
parent 6e43888187
commit b519c49406
4 changed files with 210 additions and 107 deletions

View File

@@ -10,9 +10,8 @@ import '../../domain/repositories/shifts_connector_repository.dart';
/// Handles shift-related data operations by interacting with Data Connect.
class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
/// Creates a new [ShiftsConnectorRepositoryImpl].
ShiftsConnectorRepositoryImpl({
dc.DataConnectService? service,
}) : _service = service ?? dc.DataConnectService.instance;
ShiftsConnectorRepositoryImpl({dc.DataConnectService? service})
: _service = service ?? dc.DataConnectService.instance;
final dc.DataConnectService _service;
@@ -23,12 +22,17 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
required DateTime end,
}) async {
return _service.run(() async {
final dc.GetApplicationsByStaffIdVariablesBuilder query = _service.connector
final dc.GetApplicationsByStaffIdVariablesBuilder query = _service
.connector
.getApplicationsByStaffId(staffId: staffId)
.dayStart(_service.toTimestamp(start))
.dayEnd(_service.toTimestamp(end));
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> response = await query.execute();
final QueryResult<
dc.GetApplicationsByStaffIdData,
dc.GetApplicationsByStaffIdVariables
>
response = await query.execute();
return _mapApplicationsToShifts(response.data.applications);
});
}
@@ -45,18 +49,28 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
final String? vendorId = dc.StaffSessionStore.instance.session?.ownerId;
if (vendorId == null || vendorId.isEmpty) return <Shift>[];
final QueryResult<dc.ListShiftRolesByVendorIdData, dc.ListShiftRolesByVendorIdVariables> response = await _service.connector
final QueryResult<
dc.ListShiftRolesByVendorIdData,
dc.ListShiftRolesByVendorIdVariables
>
response = await _service.connector
.listShiftRolesByVendorId(vendorId: vendorId)
.execute();
final List<dc.ListShiftRolesByVendorIdShiftRoles> allShiftRoles = response.data.shiftRoles;
final List<dc.ListShiftRolesByVendorIdShiftRoles> allShiftRoles =
response.data.shiftRoles;
// Fetch current applications to filter out already booked shifts
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> myAppsResponse = await _service.connector
final QueryResult<
dc.GetApplicationsByStaffIdData,
dc.GetApplicationsByStaffIdVariables
>
myAppsResponse = await _service.connector
.getApplicationsByStaffId(staffId: staffId)
.execute();
final Set<String> appliedShiftIds =
myAppsResponse.data.applications.map((dc.GetApplicationsByStaffIdApplications a) => a.shiftId).toSet();
final Set<String> appliedShiftIds = myAppsResponse.data.applications
.map((dc.GetApplicationsByStaffIdApplications a) => a.shiftId)
.toSet();
final List<Shift> mappedShifts = <Shift>[];
for (final dc.ListShiftRolesByVendorIdShiftRoles sr in allShiftRoles) {
@@ -67,6 +81,12 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
final DateTime? endDt = _service.toDateTime(sr.endTime);
final DateTime? createdDt = _service.toDateTime(sr.createdAt);
// Normalise orderType to uppercase for consistent checks in the UI.
// RECURRING → groups shifts into Multi-Day cards.
// PERMANENT → groups shifts into Long Term cards.
final String orderTypeStr = sr.shift.order.orderType.stringValue
.toUpperCase();
mappedShifts.add(
Shift(
id: sr.shiftId,
@@ -78,7 +98,9 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
location: sr.shift.location ?? '',
locationAddress: sr.shift.locationAddress ?? '',
date: shiftDate?.toIso8601String() ?? '',
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
startTime: startDt != null
? DateFormat('HH:mm').format(startDt)
: '',
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
createdDate: createdDt?.toIso8601String() ?? '',
status: sr.shift.status?.stringValue.toLowerCase() ?? 'open',
@@ -88,6 +110,10 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
filledSlots: sr.assigned ?? 0,
latitude: sr.shift.latitude,
longitude: sr.shift.longitude,
// orderId + orderType power the grouping and type-badge logic in
// FindShiftsTab._groupMultiDayShifts and MyShiftCard._getShiftType.
orderId: sr.shift.orderId,
orderType: orderTypeStr,
breakInfo: BreakAdapter.fromData(
isPaid: sr.isBreakPaid ?? false,
breakTime: sr.breakType?.stringValue,
@@ -125,7 +151,8 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
}) async {
return _service.run(() async {
if (roleId != null && roleId.isNotEmpty) {
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables> roleResult = await _service.connector
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables>
roleResult = await _service.connector
.getShiftRoleById(shiftId: shiftId, roleId: roleId)
.execute();
final dc.GetShiftRoleByIdShiftRole? sr = roleResult.data.shiftRole;
@@ -137,13 +164,22 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
bool hasApplied = false;
String status = 'open';
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> appsResponse = await _service.connector
final QueryResult<
dc.GetApplicationsByStaffIdData,
dc.GetApplicationsByStaffIdVariables
>
appsResponse = await _service.connector
.getApplicationsByStaffId(staffId: staffId)
.execute();
final dc.GetApplicationsByStaffIdApplications? app = appsResponse.data.applications
.where((dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId && a.shiftRole.roleId == roleId)
final dc.GetApplicationsByStaffIdApplications? app = appsResponse
.data
.applications
.where(
(dc.GetApplicationsByStaffIdApplications a) =>
a.shiftId == shiftId && a.shiftRole.roleId == roleId,
)
.firstOrNull;
if (app != null) {
@@ -181,7 +217,8 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
);
}
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> result = await _service.connector.getShiftById(id: shiftId).execute();
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> result =
await _service.connector.getShiftById(id: shiftId).execute();
final dc.GetShiftByIdShift? s = result.data.shift;
if (s == null) return null;
@@ -190,17 +227,23 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
Break? breakInfo;
try {
final QueryResult<dc.ListShiftRolesByShiftIdData, dc.ListShiftRolesByShiftIdVariables> rolesRes = await _service.connector
final QueryResult<
dc.ListShiftRolesByShiftIdData,
dc.ListShiftRolesByShiftIdVariables
>
rolesRes = await _service.connector
.listShiftRolesByShiftId(shiftId: shiftId)
.execute();
if (rolesRes.data.shiftRoles.isNotEmpty) {
required = 0;
filled = 0;
for (dc.ListShiftRolesByShiftIdShiftRoles r in rolesRes.data.shiftRoles) {
for (dc.ListShiftRolesByShiftIdShiftRoles r
in rolesRes.data.shiftRoles) {
required = (required ?? 0) + r.count;
filled = (filled ?? 0) + (r.assigned ?? 0);
}
final dc.ListShiftRolesByShiftIdShiftRoles firstRole = rolesRes.data.shiftRoles.first;
final dc.ListShiftRolesByShiftIdShiftRoles firstRole =
rolesRes.data.shiftRoles.first;
breakInfo = BreakAdapter.fromData(
isPaid: firstRole.isBreakPaid ?? false,
breakTime: firstRole.breakType?.stringValue,
@@ -247,35 +290,53 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
final String targetRoleId = roleId ?? '';
if (targetRoleId.isEmpty) throw Exception('Missing role id.');
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables> roleResult = await _service.connector
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables>
roleResult = await _service.connector
.getShiftRoleById(shiftId: shiftId, roleId: targetRoleId)
.execute();
final dc.GetShiftRoleByIdShiftRole? role = roleResult.data.shiftRole;
if (role == null) throw Exception('Shift role not found');
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> shiftResult = await _service.connector.getShiftById(id: shiftId).execute();
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables>
shiftResult = await _service.connector
.getShiftById(id: shiftId)
.execute();
final dc.GetShiftByIdShift? shift = shiftResult.data.shift;
if (shift == null) throw Exception('Shift not found');
// Validate daily limit
final DateTime? shiftDate = _service.toDateTime(shift.date);
if (shiftDate != null) {
final DateTime dayStartUtc = DateTime.utc(shiftDate.year, shiftDate.month, shiftDate.day);
final DateTime dayEndUtc = dayStartUtc.add(const Duration(days: 1)).subtract(const Duration(microseconds: 1));
final DateTime dayStartUtc = DateTime.utc(
shiftDate.year,
shiftDate.month,
shiftDate.day,
);
final DateTime dayEndUtc = dayStartUtc
.add(const Duration(days: 1))
.subtract(const Duration(microseconds: 1));
final QueryResult<dc.VaidateDayStaffApplicationData, dc.VaidateDayStaffApplicationVariables> validationResponse = await _service.connector
final QueryResult<
dc.VaidateDayStaffApplicationData,
dc.VaidateDayStaffApplicationVariables
>
validationResponse = await _service.connector
.vaidateDayStaffApplication(staffId: staffId)
.dayStart(_service.toTimestamp(dayStartUtc))
.dayEnd(_service.toTimestamp(dayEndUtc))
.execute();
if (validationResponse.data.applications.isNotEmpty) {
throw Exception('The user already has a shift that day.');
}
}
// Check for existing application
final QueryResult<dc.GetApplicationByStaffShiftAndRoleData, dc.GetApplicationByStaffShiftAndRoleVariables> existingAppRes = await _service.connector
final QueryResult<
dc.GetApplicationByStaffShiftAndRoleData,
dc.GetApplicationByStaffShiftAndRoleVariables
>
existingAppRes = await _service.connector
.getApplicationByStaffShiftAndRole(
staffId: staffId,
shiftId: shiftId,
@@ -295,14 +356,20 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
String? createdAppId;
try {
final OperationResult<dc.CreateApplicationData, dc.CreateApplicationVariables> createRes = await _service.connector.createApplication(
shiftId: shiftId,
staffId: staffId,
roleId: targetRoleId,
status: dc.ApplicationStatus.CONFIRMED, // Matches existing logic
origin: dc.ApplicationOrigin.STAFF,
).execute();
final OperationResult<
dc.CreateApplicationData,
dc.CreateApplicationVariables
>
createRes = await _service.connector
.createApplication(
shiftId: shiftId,
staffId: staffId,
roleId: targetRoleId,
status: dc.ApplicationStatus.CONFIRMED, // Matches existing logic
origin: dc.ApplicationOrigin.STAFF,
)
.execute();
createdAppId = createRes.data.application_insert.id;
await _service.connector
@@ -317,7 +384,9 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
} catch (e) {
// Simple rollback attempt (not guaranteed)
if (createdAppId != null) {
await _service.connector.deleteApplication(id: createdAppId).execute();
await _service.connector
.deleteApplication(id: createdAppId)
.execute();
}
rethrow;
}
@@ -325,11 +394,12 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
}
@override
Future<void> acceptShift({
required String shiftId,
required String staffId,
}) {
return _updateApplicationStatus(shiftId, staffId, dc.ApplicationStatus.CONFIRMED);
Future<void> acceptShift({required String shiftId, required String staffId}) {
return _updateApplicationStatus(
shiftId,
staffId,
dc.ApplicationStatus.CONFIRMED,
);
}
@override
@@ -337,7 +407,11 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
required String shiftId,
required String staffId,
}) {
return _updateApplicationStatus(shiftId, staffId, dc.ApplicationStatus.REJECTED);
return _updateApplicationStatus(
shiftId,
staffId,
dc.ApplicationStatus.REJECTED,
);
}
@override
@@ -351,18 +425,24 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
@override
Future<List<Shift>> getHistoryShifts({required String staffId}) async {
return _service.run(() async {
final QueryResult<dc.ListCompletedApplicationsByStaffIdData, dc.ListCompletedApplicationsByStaffIdVariables> response = await _service.connector
final QueryResult<
dc.ListCompletedApplicationsByStaffIdData,
dc.ListCompletedApplicationsByStaffIdVariables
>
response = await _service.connector
.listCompletedApplicationsByStaffId(staffId: staffId)
.execute();
final List<Shift> shifts = <Shift>[];
for (final dc.ListCompletedApplicationsByStaffIdApplications app in response.data.applications) {
for (final dc.ListCompletedApplicationsByStaffIdApplications app
in response.data.applications) {
final String roleName = app.shiftRole.role.name;
final String orderName = (app.shift.order.eventName ?? '').trim().isNotEmpty
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);
@@ -379,7 +459,9 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
location: app.shift.location ?? '',
locationAddress: app.shift.order.teamHub.hubName,
date: shiftDate?.toIso8601String() ?? '',
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
startTime: startDt != null
? DateFormat('HH:mm').format(startDt)
: '',
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
createdDate: createdDt?.toIso8601String() ?? '',
status: 'completed', // Hardcoded as checked out implies completion
@@ -406,7 +488,8 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
List<Shift> _mapApplicationsToShifts(List<dynamic> apps) {
return apps.map((app) {
final String roleName = app.shiftRole.role.name;
final String orderName = (app.shift.order.eventName ?? '').trim().isNotEmpty
final String orderName =
(app.shift.order.eventName ?? '').trim().isNotEmpty
? app.shift.order.eventName!
: app.shift.order.business.businessName;
final String title = '$roleName - $orderName';
@@ -418,7 +501,7 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
final bool hasCheckIn = app.checkInTime != null;
final bool hasCheckOut = app.checkOutTime != null;
String status;
if (hasCheckOut) {
status = 'completed';
@@ -479,12 +562,20 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
) async {
return _service.run(() async {
// First try to find the application
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> appsResponse = await _service.connector
final QueryResult<
dc.GetApplicationsByStaffIdData,
dc.GetApplicationsByStaffIdVariables
>
appsResponse = await _service.connector
.getApplicationsByStaffId(staffId: staffId)
.execute();
final dc.GetApplicationsByStaffIdApplications? app = appsResponse.data.applications
.where((dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId)
final dc.GetApplicationsByStaffIdApplications? app = appsResponse
.data
.applications
.where(
(dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId,
)
.firstOrNull;
if (app != null) {
@@ -494,19 +585,26 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
.execute();
} else if (newStatus == dc.ApplicationStatus.REJECTED) {
// If declining but no app found, create a rejected application
final QueryResult<dc.ListShiftRolesByShiftIdData, dc.ListShiftRolesByShiftIdVariables> rolesRes = await _service.connector
final QueryResult<
dc.ListShiftRolesByShiftIdData,
dc.ListShiftRolesByShiftIdVariables
>
rolesRes = await _service.connector
.listShiftRolesByShiftId(shiftId: shiftId)
.execute();
if (rolesRes.data.shiftRoles.isNotEmpty) {
final dc.ListShiftRolesByShiftIdShiftRoles firstRole = rolesRes.data.shiftRoles.first;
await _service.connector.createApplication(
shiftId: shiftId,
staffId: staffId,
roleId: firstRole.id,
status: dc.ApplicationStatus.REJECTED,
origin: dc.ApplicationOrigin.STAFF,
).execute();
final dc.ListShiftRolesByShiftIdShiftRoles firstRole =
rolesRes.data.shiftRoles.first;
await _service.connector
.createApplication(
shiftId: shiftId,
staffId: staffId,
roleId: firstRole.id,
status: dc.ApplicationStatus.REJECTED,
origin: dc.ApplicationOrigin.STAFF,
)
.execute();
}
} else {
throw Exception("Application not found for shift $shiftId");
@@ -514,4 +612,3 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
});
}
}

View File

@@ -1,12 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:design_system/design_system.dart';
import 'package:core_localization/core_localization.dart';
import 'package:krow_core/core.dart';
/// Card widget for displaying pending payment information, using design system tokens.
class PendingPaymentCard extends StatelessWidget {
/// Creates a [PendingPaymentCard].
@@ -21,7 +19,10 @@ class PendingPaymentCard extends StatelessWidget {
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [UiColors.primary.withOpacity(0.08), UiColors.primary.withOpacity(0.04)],
colors: [
UiColors.primary.withOpacity(0.08),
UiColors.primary.withOpacity(0.04),
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
@@ -59,7 +60,9 @@ class PendingPaymentCard extends StatelessWidget {
),
Text(
pendingI18n.subtitle,
style: UiTypography.body3r.copyWith(color: UiColors.mutedForeground),
style: UiTypography.body3r.copyWith(
color: UiColors.mutedForeground,
),
overflow: TextOverflow.ellipsis,
),
],
@@ -70,10 +73,7 @@ class PendingPaymentCard extends StatelessWidget {
),
Row(
children: [
Text(
'\$285.00',
style: UiTypography.headline4m,
),
Text('\$285.00', style: UiTypography.headline4m),
SizedBox(width: UiConstants.space2),
Icon(
UiIcons.chevronRight,

View File

@@ -185,12 +185,15 @@ class _MyShiftCardState extends State<MyShiftCard> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Status Badge
if (statusText.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space2),
child: Row(
children: [
// Badge row: shows the status label and the shift-type chip.
// The type chip (One Day / Multi-Day / Long Term) is always
// rendered when orderType is present — even for "open" find-shifts
// cards that may have no meaningful status text.
Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space2),
child: Row(
children: [
if (statusText.isNotEmpty) ...[
if (statusIcon != null)
Padding(
padding: const EdgeInsets.only(
@@ -221,30 +224,31 @@ class _MyShiftCardState extends State<MyShiftCard> {
letterSpacing: 0.5,
),
),
// Shift Type Badge (Order type)
if ((widget.shift.orderType ?? '').isNotEmpty) ...[
const SizedBox(width: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space2,
vertical: 2,
),
decoration: BoxDecoration(
color: UiColors.background,
borderRadius: UiConstants.radiusSm,
border: Border.all(color: UiColors.border),
),
child: Text(
_getShiftType(),
style: UiTypography.footnote2m.copyWith(
color: UiColors.textSecondary,
),
),
),
],
const SizedBox(width: UiConstants.space2),
],
),
// Type badge — driven by RECURRING / PERMANENT / one-day
// order data and always visible so users can filter
// Find Shifts cards at a glance.
Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space2,
vertical: 2,
),
decoration: BoxDecoration(
color: UiColors.background,
borderRadius: UiConstants.radiusSm,
border: Border.all(color: UiColors.border),
),
child: Text(
_getShiftType(),
style: UiTypography.footnote2m.copyWith(
color: UiColors.textSecondary,
),
),
),
],
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,

View File

@@ -85,11 +85,13 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
final Shift first = group.first;
final List<ShiftSchedule> schedules = group
.map((s) => ShiftSchedule(
date: s.date,
startTime: s.startTime,
endTime: s.endTime,
))
.map(
(s) => ShiftSchedule(
date: s.date,
startTime: s.startTime,
endTime: s.endTime,
),
)
.toList();
result.add(