feat: localize "Find Shifts" tab strings and add filled status to shift role queries.

This commit is contained in:
Achintha Isuru
2026-02-22 20:27:01 -05:00
parent d1a0c74b95
commit 0980c6584b
5 changed files with 620 additions and 466 deletions

View File

@@ -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 pageall 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": {

View File

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

View File

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

View File

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