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": {
|
"client_authentication": {
|
||||||
"get_started_page": {
|
"get_started_page": {
|
||||||
"title": "Take Control of Your\nShifts and Events",
|
"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",
|
"sign_in_button": "Sign In",
|
||||||
"create_account_button": "Create Account"
|
"create_account_button": "Create Account"
|
||||||
},
|
},
|
||||||
@@ -452,7 +452,7 @@
|
|||||||
},
|
},
|
||||||
"empty_states": {
|
"empty_states": {
|
||||||
"no_shifts_today": "No shifts scheduled for today",
|
"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_shifts_tomorrow": "No shifts for tomorrow",
|
||||||
"no_recommended_shifts": "No recommended shifts"
|
"no_recommended_shifts": "No recommended shifts"
|
||||||
},
|
},
|
||||||
@@ -462,7 +462,7 @@
|
|||||||
"amount": "$amount"
|
"amount": "$amount"
|
||||||
},
|
},
|
||||||
"recommended_card": {
|
"recommended_card": {
|
||||||
"act_now": "• ACT NOW",
|
"act_now": "\u2022 ACT NOW",
|
||||||
"one_day": "One Day",
|
"one_day": "One Day",
|
||||||
"today": "Today",
|
"today": "Today",
|
||||||
"applied_for": "Applied for $title",
|
"applied_for": "Applied for $title",
|
||||||
@@ -695,7 +695,7 @@
|
|||||||
"eta_label": "$min min",
|
"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.",
|
"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",
|
"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?"
|
"arrived_desc": "You're at the shift location. Ready to clock in?"
|
||||||
},
|
},
|
||||||
"swipe": {
|
"swipe": {
|
||||||
@@ -967,16 +967,16 @@
|
|||||||
"required": "REQUIRED",
|
"required": "REQUIRED",
|
||||||
"add_photo": "Add Photo",
|
"add_photo": "Add Photo",
|
||||||
"added": "Added",
|
"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.",
|
"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": {
|
"actions": {
|
||||||
"save": "Save Attire"
|
"save": "Save Attire"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"select_required": "✓ Select all required items",
|
"select_required": "\u2713 Select all required items",
|
||||||
"upload_required": "✓ Upload photos of required items",
|
"upload_required": "\u2713 Upload photos of required items",
|
||||||
"accept_attestation": "✓ Accept attestation"
|
"accept_attestation": "\u2713 Accept attestation"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"staff_shifts": {
|
"staff_shifts": {
|
||||||
@@ -1095,8 +1095,18 @@
|
|||||||
},
|
},
|
||||||
"card": {
|
"card": {
|
||||||
"cancelled": "CANCELLED",
|
"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": {
|
"staff_time_card": {
|
||||||
@@ -1218,11 +1228,11 @@
|
|||||||
},
|
},
|
||||||
"total_spend": {
|
"total_spend": {
|
||||||
"label": "Total Spend",
|
"label": "Total Spend",
|
||||||
"badge": "↓ 8% vs last week"
|
"badge": "\u2193 8% vs last week"
|
||||||
},
|
},
|
||||||
"fill_rate": {
|
"fill_rate": {
|
||||||
"label": "Fill Rate",
|
"label": "Fill Rate",
|
||||||
"badge": "↑ 2% improvement"
|
"badge": "\u2191 2% improvement"
|
||||||
},
|
},
|
||||||
"avg_fill_time": {
|
"avg_fill_time": {
|
||||||
"label": "Avg Fill Time",
|
"label": "Avg Fill Time",
|
||||||
@@ -1364,9 +1374,9 @@
|
|||||||
"target_prefix": "Target: ",
|
"target_prefix": "Target: ",
|
||||||
"target_hours": "$hours hrs",
|
"target_hours": "$hours hrs",
|
||||||
"target_percent": "$percent%",
|
"target_percent": "$percent%",
|
||||||
"met": "✓ Met",
|
"met": "\u2713 Met",
|
||||||
"close": "→ Close",
|
"close": "\u2192 Close",
|
||||||
"miss": "✗ Miss"
|
"miss": "\u2717 Miss"
|
||||||
},
|
},
|
||||||
"additional_metrics_title": "ADDITIONAL METRICS",
|
"additional_metrics_title": "ADDITIONAL METRICS",
|
||||||
"additional_metrics": {
|
"additional_metrics": {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -314,6 +314,51 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
final String targetRoleId = roleId ?? '';
|
final String targetRoleId = roleId ?? '';
|
||||||
if (targetRoleId.isEmpty) throw Exception('Missing role id.');
|
if (targetRoleId.isEmpty) throw Exception('Missing role id.');
|
||||||
|
|
||||||
|
// 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? initialShift = shiftResult.data.shift;
|
||||||
|
if (initialShift == null) throw Exception('Shift not found');
|
||||||
|
|
||||||
|
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.ListShiftRolesByBusinessAndOrderData,
|
||||||
|
dc.ListShiftRolesByBusinessAndOrderVariables
|
||||||
|
>
|
||||||
|
allRolesRes = await _service.connector
|
||||||
|
.listShiftRolesByBusinessAndOrder(
|
||||||
|
businessId: initialShift.order.businessId,
|
||||||
|
orderId: initialShift.orderId,
|
||||||
|
)
|
||||||
|
.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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single shift application
|
||||||
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables>
|
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables>
|
||||||
roleResult = await _service.connector
|
roleResult = await _service.connector
|
||||||
.getShiftRoleById(shiftId: shiftId, roleId: targetRoleId)
|
.getShiftRoleById(shiftId: shiftId, roleId: targetRoleId)
|
||||||
@@ -321,20 +366,52 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
final dc.GetShiftRoleByIdShiftRole? role = roleResult.data.shiftRole;
|
final dc.GetShiftRoleByIdShiftRole? role = roleResult.data.shiftRole;
|
||||||
if (role == null) throw Exception('Shift role not found');
|
if (role == null) throw Exception('Shift role not found');
|
||||||
|
|
||||||
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables>
|
targets.add(
|
||||||
shiftResult = await _service.connector
|
_TargetShiftRole(
|
||||||
.getShiftById(id: shiftId)
|
shiftId: shiftId,
|
||||||
.execute();
|
roleId: targetRoleId,
|
||||||
final dc.GetShiftByIdShift? shift = shiftResult.data.shift;
|
count: role.count,
|
||||||
if (shift == null) throw Exception('Shift not found');
|
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
|
// Validate daily limit
|
||||||
final DateTime? shiftDate = _service.toDateTime(shift.date);
|
if (target.date != null) {
|
||||||
if (shiftDate != null) {
|
|
||||||
final DateTime dayStartUtc = DateTime.utc(
|
final DateTime dayStartUtc = DateTime.utc(
|
||||||
shiftDate.year,
|
target.date!.year,
|
||||||
shiftDate.month,
|
target.date!.month,
|
||||||
shiftDate.day,
|
target.date!.day,
|
||||||
);
|
);
|
||||||
final DateTime dayEndUtc = dayStartUtc
|
final DateTime dayEndUtc = dayStartUtc
|
||||||
.add(const Duration(days: 1))
|
.add(const Duration(days: 1))
|
||||||
@@ -363,21 +440,19 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
existingAppRes = await _service.connector
|
existingAppRes = await _service.connector
|
||||||
.getApplicationByStaffShiftAndRole(
|
.getApplicationByStaffShiftAndRole(
|
||||||
staffId: staffId,
|
staffId: staffId,
|
||||||
shiftId: shiftId,
|
shiftId: target.shiftId,
|
||||||
roleId: targetRoleId,
|
roleId: target.roleId,
|
||||||
)
|
)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
if (existingAppRes.data.applications.isNotEmpty) {
|
if (existingAppRes.data.applications.isNotEmpty) {
|
||||||
throw Exception('Application already exists.');
|
throw Exception('Application already exists.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((role.assigned ?? 0) >= role.count) {
|
if (target.assigned >= target.count) {
|
||||||
throw Exception('This shift is full.');
|
throw Exception('This shift is full.');
|
||||||
}
|
}
|
||||||
|
|
||||||
final int currentAssigned = role.assigned ?? 0;
|
|
||||||
final int currentFilled = shift.filled ?? 0;
|
|
||||||
|
|
||||||
String? createdAppId;
|
String? createdAppId;
|
||||||
try {
|
try {
|
||||||
final OperationResult<
|
final OperationResult<
|
||||||
@@ -386,10 +461,10 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
>
|
>
|
||||||
createRes = await _service.connector
|
createRes = await _service.connector
|
||||||
.createApplication(
|
.createApplication(
|
||||||
shiftId: shiftId,
|
shiftId: target.shiftId,
|
||||||
staffId: staffId,
|
staffId: staffId,
|
||||||
roleId: targetRoleId,
|
roleId: target.roleId,
|
||||||
status: dc.ApplicationStatus.CONFIRMED, // Matches existing logic
|
status: dc.ApplicationStatus.CONFIRMED,
|
||||||
origin: dc.ApplicationOrigin.STAFF,
|
origin: dc.ApplicationOrigin.STAFF,
|
||||||
)
|
)
|
||||||
.execute();
|
.execute();
|
||||||
@@ -397,24 +472,21 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
createdAppId = createRes.data.application_insert.id;
|
createdAppId = createRes.data.application_insert.id;
|
||||||
|
|
||||||
await _service.connector
|
await _service.connector
|
||||||
.updateShiftRole(shiftId: shiftId, roleId: targetRoleId)
|
.updateShiftRole(shiftId: target.shiftId, roleId: target.roleId)
|
||||||
.assigned(currentAssigned + 1)
|
.assigned(target.assigned + 1)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await _service.connector
|
await _service.connector
|
||||||
.updateShift(id: shiftId)
|
.updateShift(id: target.shiftId)
|
||||||
.filled(currentFilled + 1)
|
.filled(target.shiftFilled + 1)
|
||||||
.execute();
|
.execute();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Simple rollback attempt (not guaranteed)
|
// Simple rollback attempt (not guaranteed)
|
||||||
if (createdAppId != null) {
|
if (createdAppId != null) {
|
||||||
await _service.connector
|
await _service.connector.deleteApplication(id: createdAppId).execute();
|
||||||
.deleteApplication(id: createdAppId)
|
|
||||||
.execute();
|
|
||||||
}
|
}
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -704,3 +776,21 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
return schedules;
|
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:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import '../../blocs/shifts/shifts_bloc.dart';
|
import '../../blocs/shifts/shifts_bloc.dart';
|
||||||
@@ -233,7 +234,11 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
|||||||
setState(() => _searchQuery = v),
|
setState(() => _searchQuery = v),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
hintText: "Search jobs, location...",
|
hintText: context
|
||||||
|
.t
|
||||||
|
.staff_shifts
|
||||||
|
.find_shifts
|
||||||
|
.search_hint,
|
||||||
hintStyle: UiTypography.body2r.textPlaceholder,
|
hintStyle: UiTypography.body2r.textPlaceholder,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -267,13 +272,25 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
_buildFilterTab('all', 'All Jobs'),
|
_buildFilterTab(
|
||||||
|
'all',
|
||||||
|
context.t.staff_shifts.find_shifts.filter_all,
|
||||||
|
),
|
||||||
const SizedBox(width: UiConstants.space2),
|
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),
|
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),
|
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(
|
Expanded(
|
||||||
child: filteredJobs.isEmpty
|
child: filteredJobs.isEmpty
|
||||||
? const EmptyStateView(
|
? EmptyStateView(
|
||||||
icon: UiIcons.search,
|
icon: UiIcons.search,
|
||||||
title: "No jobs available",
|
title: context.t.staff_shifts.find_shifts.no_jobs_title,
|
||||||
subtitle: "Check back later",
|
subtitle: context.t.staff_shifts.find_shifts.no_jobs_subtitle,
|
||||||
)
|
)
|
||||||
: SingleChildScrollView(
|
: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
@@ -308,8 +325,11 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
|||||||
);
|
);
|
||||||
UiSnackbar.show(
|
UiSnackbar.show(
|
||||||
context,
|
context,
|
||||||
message:
|
message: context
|
||||||
"Shift application submitted!", // Todo: Localization
|
.t
|
||||||
|
.staff_shifts
|
||||||
|
.find_shifts
|
||||||
|
.application_submitted,
|
||||||
type: UiSnackbarType.success,
|
type: UiSnackbarType.success,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -401,6 +401,7 @@ query listShiftRolesByBusinessAndOrder(
|
|||||||
orderId
|
orderId
|
||||||
location
|
location
|
||||||
locationAddress
|
locationAddress
|
||||||
|
filled
|
||||||
|
|
||||||
order{
|
order{
|
||||||
vendorId
|
vendorId
|
||||||
@@ -425,6 +426,29 @@ query listShiftRolesByBusinessAndOrder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query listShiftRolesByOrderAndRole(
|
||||||
|
$orderId: UUID!
|
||||||
|
$roleId: UUID!
|
||||||
|
) @auth(level: USER) {
|
||||||
|
shiftRoles(
|
||||||
|
where: {
|
||||||
|
shift: { orderId: { eq: $orderId } }
|
||||||
|
roleId: { eq: $roleId }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
shiftId
|
||||||
|
roleId
|
||||||
|
count
|
||||||
|
assigned
|
||||||
|
shift {
|
||||||
|
id
|
||||||
|
filled
|
||||||
|
date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#reorder get list by businessId
|
#reorder get list by businessId
|
||||||
query listShiftRolesByBusinessDateRangeCompletedOrders(
|
query listShiftRolesByBusinessDateRangeCompletedOrders(
|
||||||
$businessId: UUID!
|
$businessId: UUID!
|
||||||
|
|||||||
Reference in New Issue
Block a user