feat: localize "Find Shifts" tab strings and add filled status to shift role queries.
This commit is contained in:
@@ -104,7 +104,7 @@
|
||||
"client_authentication": {
|
||||
"get_started_page": {
|
||||
"title": "Take Control of Your\nShifts and Events",
|
||||
"subtitle": "Streamline your operations with powerful tools to manage schedules, track performance, and keep your team on the same page—all in one place",
|
||||
"subtitle": "Streamline your operations with powerful tools to manage schedules, track performance, and keep your team on the same page\u2014all in one place",
|
||||
"sign_in_button": "Sign In",
|
||||
"create_account_button": "Create Account"
|
||||
},
|
||||
@@ -452,7 +452,7 @@
|
||||
},
|
||||
"empty_states": {
|
||||
"no_shifts_today": "No shifts scheduled for today",
|
||||
"find_shifts_cta": "Find shifts →",
|
||||
"find_shifts_cta": "Find shifts \u2192",
|
||||
"no_shifts_tomorrow": "No shifts for tomorrow",
|
||||
"no_recommended_shifts": "No recommended shifts"
|
||||
},
|
||||
@@ -462,7 +462,7 @@
|
||||
"amount": "$amount"
|
||||
},
|
||||
"recommended_card": {
|
||||
"act_now": "• ACT NOW",
|
||||
"act_now": "\u2022 ACT NOW",
|
||||
"one_day": "One Day",
|
||||
"today": "Today",
|
||||
"applied_for": "Applied for $title",
|
||||
@@ -695,7 +695,7 @@
|
||||
"eta_label": "$min min",
|
||||
"locked_desc": "Most app features are locked while commute mode is on. You'll be able to clock in once you arrive.",
|
||||
"turn_off": "Turn Off Commute Mode",
|
||||
"arrived_title": "You've Arrived! 🎉",
|
||||
"arrived_title": "You've Arrived! \ud83c\udf89",
|
||||
"arrived_desc": "You're at the shift location. Ready to clock in?"
|
||||
},
|
||||
"swipe": {
|
||||
@@ -967,16 +967,16 @@
|
||||
"required": "REQUIRED",
|
||||
"add_photo": "Add Photo",
|
||||
"added": "Added",
|
||||
"pending": "⏳ Pending verification"
|
||||
"pending": "\u23f3 Pending verification"
|
||||
},
|
||||
"attestation": "I certify that I own these items and will wear them to my shifts. I understand that items are pending manager verification at my first shift.",
|
||||
"actions": {
|
||||
"save": "Save Attire"
|
||||
},
|
||||
"validation": {
|
||||
"select_required": "✓ Select all required items",
|
||||
"upload_required": "✓ Upload photos of required items",
|
||||
"accept_attestation": "✓ Accept attestation"
|
||||
"select_required": "\u2713 Select all required items",
|
||||
"upload_required": "\u2713 Upload photos of required items",
|
||||
"accept_attestation": "\u2713 Accept attestation"
|
||||
}
|
||||
},
|
||||
"staff_shifts": {
|
||||
@@ -1095,8 +1095,18 @@
|
||||
},
|
||||
"card": {
|
||||
"cancelled": "CANCELLED",
|
||||
"compensation": "• 4hr compensation"
|
||||
"compensation": "\u2022 4hr compensation"
|
||||
}
|
||||
},
|
||||
"find_shifts": {
|
||||
"search_hint": "Search jobs, location...",
|
||||
"filter_all": "All Jobs",
|
||||
"filter_one_day": "One Day",
|
||||
"filter_multi_day": "Multi-Day",
|
||||
"filter_long_term": "Long Term",
|
||||
"no_jobs_title": "No jobs available",
|
||||
"no_jobs_subtitle": "Check back later",
|
||||
"application_submitted": "Shift application submitted!"
|
||||
}
|
||||
},
|
||||
"staff_time_card": {
|
||||
@@ -1218,11 +1228,11 @@
|
||||
},
|
||||
"total_spend": {
|
||||
"label": "Total Spend",
|
||||
"badge": "↓ 8% vs last week"
|
||||
"badge": "\u2193 8% vs last week"
|
||||
},
|
||||
"fill_rate": {
|
||||
"label": "Fill Rate",
|
||||
"badge": "↑ 2% improvement"
|
||||
"badge": "\u2191 2% improvement"
|
||||
},
|
||||
"avg_fill_time": {
|
||||
"label": "Avg Fill Time",
|
||||
@@ -1364,9 +1374,9 @@
|
||||
"target_prefix": "Target: ",
|
||||
"target_hours": "$hours hrs",
|
||||
"target_percent": "$percent%",
|
||||
"met": "✓ Met",
|
||||
"close": "→ Close",
|
||||
"miss": "✗ Miss"
|
||||
"met": "\u2713 Met",
|
||||
"close": "\u2192 Close",
|
||||
"miss": "\u2717 Miss"
|
||||
},
|
||||
"additional_metrics_title": "ADDITIONAL METRICS",
|
||||
"additional_metrics": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -314,109 +314,181 @@ 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
|
||||
.getShiftRoleById(shiftId: shiftId, roleId: targetRoleId)
|
||||
.execute();
|
||||
final dc.GetShiftRoleByIdShiftRole? role = roleResult.data.shiftRole;
|
||||
if (role == null) throw Exception('Shift role not found');
|
||||
|
||||
// 1. Fetch the initial shift to determine order type
|
||||
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');
|
||||
final dc.GetShiftByIdShift? initialShift = shiftResult.data.shift;
|
||||
if (initialShift == 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 dc.EnumValue<dc.OrderType> orderTypeEnum =
|
||||
initialShift.order.orderType;
|
||||
final bool isMultiDay =
|
||||
orderTypeEnum is dc.Known<dc.OrderType> &&
|
||||
(orderTypeEnum.value == dc.OrderType.RECURRING ||
|
||||
orderTypeEnum.value == dc.OrderType.PERMANENT);
|
||||
final List<_TargetShiftRole> targets = [];
|
||||
|
||||
if (isMultiDay) {
|
||||
// 2. Fetch all shifts for this order to apply to all of them for the same role
|
||||
final QueryResult<
|
||||
dc.VaidateDayStaffApplicationData,
|
||||
dc.VaidateDayStaffApplicationVariables
|
||||
dc.ListShiftRolesByBusinessAndOrderData,
|
||||
dc.ListShiftRolesByBusinessAndOrderVariables
|
||||
>
|
||||
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
|
||||
.getApplicationByStaffShiftAndRole(
|
||||
staffId: staffId,
|
||||
shiftId: shiftId,
|
||||
roleId: targetRoleId,
|
||||
)
|
||||
.execute();
|
||||
if (existingAppRes.data.applications.isNotEmpty) {
|
||||
throw Exception('Application already exists.');
|
||||
}
|
||||
|
||||
if ((role.assigned ?? 0) >= role.count) {
|
||||
throw Exception('This shift is full.');
|
||||
}
|
||||
|
||||
final int currentAssigned = role.assigned ?? 0;
|
||||
final int currentFilled = shift.filled ?? 0;
|
||||
|
||||
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,
|
||||
allRolesRes = await _service.connector
|
||||
.listShiftRolesByBusinessAndOrder(
|
||||
businessId: initialShift.order.businessId,
|
||||
orderId: initialShift.orderId,
|
||||
)
|
||||
.execute();
|
||||
|
||||
createdAppId = createRes.data.application_insert.id;
|
||||
|
||||
await _service.connector
|
||||
.updateShiftRole(shiftId: shiftId, roleId: targetRoleId)
|
||||
.assigned(currentAssigned + 1)
|
||||
.execute();
|
||||
|
||||
await _service.connector
|
||||
.updateShift(id: shiftId)
|
||||
.filled(currentFilled + 1)
|
||||
.execute();
|
||||
} catch (e) {
|
||||
// Simple rollback attempt (not guaranteed)
|
||||
if (createdAppId != null) {
|
||||
await _service.connector
|
||||
.deleteApplication(id: createdAppId)
|
||||
.execute();
|
||||
for (final role in allRolesRes.data.shiftRoles) {
|
||||
if (role.roleId == targetRoleId) {
|
||||
targets.add(
|
||||
_TargetShiftRole(
|
||||
shiftId: role.shiftId,
|
||||
roleId: role.roleId,
|
||||
count: role.count,
|
||||
assigned: role.assigned ?? 0,
|
||||
shiftFilled: role.shift.filled ?? 0,
|
||||
date: _service.toDateTime(role.shift.date),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
rethrow;
|
||||
} else {
|
||||
// Single shift application
|
||||
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');
|
||||
|
||||
targets.add(
|
||||
_TargetShiftRole(
|
||||
shiftId: shiftId,
|
||||
roleId: targetRoleId,
|
||||
count: role.count,
|
||||
assigned: role.assigned ?? 0,
|
||||
shiftFilled: initialShift.filled ?? 0,
|
||||
date: _service.toDateTime(initialShift.date),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (targets.isEmpty) {
|
||||
throw Exception('No valid shifts found to apply for.');
|
||||
}
|
||||
|
||||
int appliedCount = 0;
|
||||
final List<String> errors = [];
|
||||
|
||||
for (final target in targets) {
|
||||
try {
|
||||
await _applyToSingleShiftRole(target: target, staffId: staffId);
|
||||
appliedCount++;
|
||||
} catch (e) {
|
||||
// For multi-shift apply, we might want to continue even if some fail due to conflicts
|
||||
if (targets.length == 1) rethrow;
|
||||
errors.add('Shift on ${target.date}: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
if (appliedCount == 0 && targets.length > 1) {
|
||||
throw Exception('Failed to apply for any shifts: ${errors.join(", ")}');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _applyToSingleShiftRole({
|
||||
required _TargetShiftRole target,
|
||||
required String staffId,
|
||||
}) async {
|
||||
// Validate daily limit
|
||||
if (target.date != null) {
|
||||
final DateTime dayStartUtc = DateTime.utc(
|
||||
target.date!.year,
|
||||
target.date!.month,
|
||||
target.date!.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
|
||||
.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
|
||||
.getApplicationByStaffShiftAndRole(
|
||||
staffId: staffId,
|
||||
shiftId: target.shiftId,
|
||||
roleId: target.roleId,
|
||||
)
|
||||
.execute();
|
||||
|
||||
if (existingAppRes.data.applications.isNotEmpty) {
|
||||
throw Exception('Application already exists.');
|
||||
}
|
||||
|
||||
if (target.assigned >= target.count) {
|
||||
throw Exception('This shift is full.');
|
||||
}
|
||||
|
||||
String? createdAppId;
|
||||
try {
|
||||
final OperationResult<
|
||||
dc.CreateApplicationData,
|
||||
dc.CreateApplicationVariables
|
||||
>
|
||||
createRes = await _service.connector
|
||||
.createApplication(
|
||||
shiftId: target.shiftId,
|
||||
staffId: staffId,
|
||||
roleId: target.roleId,
|
||||
status: dc.ApplicationStatus.CONFIRMED,
|
||||
origin: dc.ApplicationOrigin.STAFF,
|
||||
)
|
||||
.execute();
|
||||
|
||||
createdAppId = createRes.data.application_insert.id;
|
||||
|
||||
await _service.connector
|
||||
.updateShiftRole(shiftId: target.shiftId, roleId: target.roleId)
|
||||
.assigned(target.assigned + 1)
|
||||
.execute();
|
||||
|
||||
await _service.connector
|
||||
.updateShift(id: target.shiftId)
|
||||
.filled(target.shiftFilled + 1)
|
||||
.execute();
|
||||
} catch (e) {
|
||||
// Simple rollback attempt (not guaranteed)
|
||||
if (createdAppId != null) {
|
||||
await _service.connector.deleteApplication(id: createdAppId).execute();
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> acceptShift({required String shiftId, required String staffId}) {
|
||||
return _updateApplicationStatus(
|
||||
@@ -704,3 +776,21 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
return schedules;
|
||||
}
|
||||
}
|
||||
|
||||
class _TargetShiftRole {
|
||||
final String shiftId;
|
||||
final String roleId;
|
||||
final int count;
|
||||
final int assigned;
|
||||
final int shiftFilled;
|
||||
final DateTime? date;
|
||||
|
||||
_TargetShiftRole({
|
||||
required this.shiftId,
|
||||
required this.roleId,
|
||||
required this.count,
|
||||
required this.assigned,
|
||||
required this.shiftFilled,
|
||||
this.date,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../blocs/shifts/shifts_bloc.dart';
|
||||
@@ -233,7 +234,11 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
setState(() => _searchQuery = v),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: "Search jobs, location...",
|
||||
hintText: context
|
||||
.t
|
||||
.staff_shifts
|
||||
.find_shifts
|
||||
.search_hint,
|
||||
hintStyle: UiTypography.body2r.textPlaceholder,
|
||||
),
|
||||
),
|
||||
@@ -267,13 +272,25 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
_buildFilterTab('all', 'All Jobs'),
|
||||
_buildFilterTab(
|
||||
'all',
|
||||
context.t.staff_shifts.find_shifts.filter_all,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
_buildFilterTab('one-day', 'One Day'),
|
||||
_buildFilterTab(
|
||||
'one-day',
|
||||
context.t.staff_shifts.find_shifts.filter_one_day,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
_buildFilterTab('multi-day', 'Multi-Day'),
|
||||
_buildFilterTab(
|
||||
'multi-day',
|
||||
context.t.staff_shifts.find_shifts.filter_multi_day,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
_buildFilterTab('long-term', 'Long Term'),
|
||||
_buildFilterTab(
|
||||
'long-term',
|
||||
context.t.staff_shifts.find_shifts.filter_long_term,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -283,10 +300,10 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
|
||||
Expanded(
|
||||
child: filteredJobs.isEmpty
|
||||
? const EmptyStateView(
|
||||
? EmptyStateView(
|
||||
icon: UiIcons.search,
|
||||
title: "No jobs available",
|
||||
subtitle: "Check back later",
|
||||
title: context.t.staff_shifts.find_shifts.no_jobs_title,
|
||||
subtitle: context.t.staff_shifts.find_shifts.no_jobs_subtitle,
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -308,8 +325,11 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
);
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message:
|
||||
"Shift application submitted!", // Todo: Localization
|
||||
message: context
|
||||
.t
|
||||
.staff_shifts
|
||||
.find_shifts
|
||||
.application_submitted,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user