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,109 +314,181 @@ 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.');
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables> // 1. Fetch the initial shift to determine order type
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> final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables>
shiftResult = await _service.connector shiftResult = await _service.connector
.getShiftById(id: shiftId) .getShiftById(id: shiftId)
.execute(); .execute();
final dc.GetShiftByIdShift? shift = shiftResult.data.shift; final dc.GetShiftByIdShift? initialShift = shiftResult.data.shift;
if (shift == null) throw Exception('Shift not found'); if (initialShift == null) throw Exception('Shift not found');
// Validate daily limit final dc.EnumValue<dc.OrderType> orderTypeEnum =
final DateTime? shiftDate = _service.toDateTime(shift.date); initialShift.order.orderType;
if (shiftDate != null) { final bool isMultiDay =
final DateTime dayStartUtc = DateTime.utc( orderTypeEnum is dc.Known<dc.OrderType> &&
shiftDate.year, (orderTypeEnum.value == dc.OrderType.RECURRING ||
shiftDate.month, orderTypeEnum.value == dc.OrderType.PERMANENT);
shiftDate.day, final List<_TargetShiftRole> targets = [];
);
final DateTime dayEndUtc = dayStartUtc
.add(const Duration(days: 1))
.subtract(const Duration(microseconds: 1));
if (isMultiDay) {
// 2. Fetch all shifts for this order to apply to all of them for the same role
final QueryResult< final QueryResult<
dc.VaidateDayStaffApplicationData, dc.ListShiftRolesByBusinessAndOrderData,
dc.VaidateDayStaffApplicationVariables dc.ListShiftRolesByBusinessAndOrderVariables
> >
validationResponse = await _service.connector allRolesRes = await _service.connector
.vaidateDayStaffApplication(staffId: staffId) .listShiftRolesByBusinessAndOrder(
.dayStart(_service.toTimestamp(dayStartUtc)) businessId: initialShift.order.businessId,
.dayEnd(_service.toTimestamp(dayEndUtc)) orderId: initialShift.orderId,
.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,
) )
.execute(); .execute();
createdAppId = createRes.data.application_insert.id; for (final role in allRolesRes.data.shiftRoles) {
if (role.roleId == targetRoleId) {
await _service.connector targets.add(
.updateShiftRole(shiftId: shiftId, roleId: targetRoleId) _TargetShiftRole(
.assigned(currentAssigned + 1) shiftId: role.shiftId,
.execute(); roleId: role.roleId,
count: role.count,
await _service.connector assigned: role.assigned ?? 0,
.updateShift(id: shiftId) shiftFilled: role.shift.filled ?? 0,
.filled(currentFilled + 1) date: _service.toDateTime(role.shift.date),
.execute(); ),
} catch (e) { );
// Simple rollback attempt (not guaranteed) }
if (createdAppId != null) {
await _service.connector
.deleteApplication(id: createdAppId)
.execute();
} }
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 @override
Future<void> acceptShift({required String shiftId, required String staffId}) { Future<void> acceptShift({required String shiftId, required String staffId}) {
return _updateApplicationStatus( return _updateApplicationStatus(
@@ -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!