Fix build errors: localization syntax, key paths, and ViewOrderCard widget

This commit is contained in:
2026-02-14 16:26:10 +05:30
parent 097481d26a
commit 4e1a41ebff
25 changed files with 420 additions and 207 deletions

View File

@@ -148,9 +148,17 @@
"edit_mode_active": "Edit Mode Active", "edit_mode_active": "Edit Mode Active",
"drag_instruction": "Drag to reorder, toggle visibility", "drag_instruction": "Drag to reorder, toggle visibility",
"reset": "Reset", "reset": "Reset",
"todays_coverage": "TODAY'S COVERAGE",
"percent_covered": "$percent% Covered",
"metric_needed": "Needed", "metric_needed": "Needed",
"metric_filled": "Filled", "metric_filled": "Filled",
"metric_open": "Open", "metric_open": "Open",
"spending": {
"this_week": "This Week",
"next_7_days": "Next 7 Days",
"shifts_count": "$count shifts",
"scheduled_count": "$count scheduled"
},
"view_all": "View all", "view_all": "View all",
"insight_lightbulb": "Save $amount/month", "insight_lightbulb": "Save $amount/month",
"insight_tip": "Book 48hrs ahead for better rates" "insight_tip": "Book 48hrs ahead for better rates"
@@ -237,6 +245,14 @@
"scan_button": "Scan NFC Tag", "scan_button": "Scan NFC Tag",
"tag_identified": "Tag Identified", "tag_identified": "Tag Identified",
"assign_button": "Assign Tag" "assign_button": "Assign Tag"
},
"delete_dialog": {
"title": "Confirm Hub Deletion",
"message": "Are you sure you want to delete \"$hubName\"?",
"undo_warning": "This action cannot be undone.",
"dependency_warning": "Note that if there are any shifts/orders assigned to this hub we shouldn't be able to delete the hub.",
"cancel": "Cancel",
"delete": "Delete"
} }
}, },
"client_create_order": { "client_create_order": {
@@ -337,14 +353,26 @@
"cancelled": "CANCELLED", "cancelled": "CANCELLED",
"get_direction": "Get direction", "get_direction": "Get direction",
"total": "Total", "total": "Total",
"hrs": "HRS", "hrs": "Hrs",
"workers": "$count workers", "workers": "$count workers",
"clock_in": "CLOCK IN", "clock_in": "CLOCK IN",
"clock_out": "CLOCK OUT", "clock_out": "CLOCK OUT",
"coverage": "Coverage", "coverage": "Coverage",
"workers_label": "$filled/$needed Workers", "workers_label": "$filled/$needed Workers",
"confirmed_workers": "Workers Confirmed", "confirmed_workers": "Workers Confirmed",
"no_workers": "No workers confirmed yet." "no_workers": "No workers confirmed yet.",
"today": "Today",
"tomorrow": "Tomorrow",
"workers_needed": "$count Workers Needed",
"all_confirmed": "All Workers Confirmed",
"confirmed_workers_title": "CONFIRMED WORKERS",
"message_all": "Message All",
"show_more_workers": "Show $count more workers",
"checked_in": "Checked In",
"call_dialog": {
"title": "Call",
"message": "Do you want to call $phone?"
}
} }
}, },
"client_billing": { "client_billing": {
@@ -498,6 +526,10 @@
"menu_items": { "menu_items": {
"personal_info": "Personal Info", "personal_info": "Personal Info",
"emergency_contact": "Emergency Contact", "emergency_contact": "Emergency Contact",
"emergency_contact_page": {
"save_success": "Emergency contacts saved successfully",
"save_continue": "Save & Continue"
},
"experience": "Experience", "experience": "Experience",
"attire": "Attire", "attire": "Attire",
"documents": "Documents", "documents": "Documents",
@@ -853,6 +885,7 @@
}, },
"staff_certificates": { "staff_certificates": {
"title": "Certificates", "title": "Certificates",
"error_loading": "Error loading certificates",
"progress": { "progress": {
"title": "Your Progress", "title": "Your Progress",
"verified_count": "$completed of $total verified", "verified_count": "$completed of $total verified",
@@ -988,6 +1021,41 @@
"applying_dialog": { "applying_dialog": {
"title": "Applying" "title": "Applying"
} }
},
"card": {
"just_now": "Just now",
"assigned": "Assigned $time ago",
"accept_shift": "Accept shift",
"decline_shift": "Decline shift"
},
"my_shifts_tab": {
"confirm_dialog": {
"title": "Accept Shift",
"message": "Are you sure you want to accept this shift?",
"success": "Shift confirmed!"
},
"decline_dialog": {
"title": "Decline Shift",
"message": "Are you sure you want to decline this shift? This action cannot be undone.",
"success": "Shift declined."
},
"sections": {
"awaiting": "Awaiting Confirmation",
"cancelled": "Cancelled Shifts",
"confirmed": "Confirmed Shifts"
},
"empty": {
"title": "No shifts this week",
"subtitle": "Try finding new jobs in the Find tab"
},
"date": {
"today": "Today",
"tomorrow": "Tomorrow"
},
"card": {
"cancelled": "CANCELLED",
"compensation": "• 4hr compensation"
}
} }
}, },
"staff_time_card": { "staff_time_card": {

View File

@@ -148,9 +148,17 @@
"edit_mode_active": "Modo Edición Activo", "edit_mode_active": "Modo Edición Activo",
"drag_instruction": "Arrastra para reordenar, cambia la visibilidad", "drag_instruction": "Arrastra para reordenar, cambia la visibilidad",
"reset": "Restablecer", "reset": "Restablecer",
"todays_coverage": "COBERTURA DE HOY",
"percent_covered": "$percent% Cubierto",
"metric_needed": "Necesario", "metric_needed": "Necesario",
"metric_filled": "Lleno", "metric_filled": "Lleno",
"metric_open": "Abierto", "metric_open": "Abierto",
"spending": {
"this_week": "Esta Semana",
"next_7_days": "Próximos 7 Días",
"shifts_count": "$count turnos",
"scheduled_count": "$count programados"
},
"view_all": "Ver todo", "view_all": "Ver todo",
"insight_lightbulb": "Ahorra $amount/mes", "insight_lightbulb": "Ahorra $amount/mes",
"insight_tip": "Reserva con 48h de antelación para mejores tarifas" "insight_tip": "Reserva con 48h de antelación para mejores tarifas"
@@ -237,6 +245,14 @@
"scan_button": "Escanear Etiqueta NFC", "scan_button": "Escanear Etiqueta NFC",
"tag_identified": "Etiqueta Identificada", "tag_identified": "Etiqueta Identificada",
"assign_button": "Asignar Etiqueta" "assign_button": "Asignar Etiqueta"
},
"delete_dialog": {
"title": "Confirmar eliminación de Hub",
"message": "¿Estás seguro de que quieres eliminar \"$hubName\"?",
"undo_warning": "Esta acción no se puede deshacer.",
"dependency_warning": "Ten en cuenta que si hay turnos/órdenes asignados a este hub no deberíamos poder eliminarlo.",
"cancel": "Cancelar",
"delete": "Eliminar"
} }
}, },
"client_create_order": { "client_create_order": {
@@ -337,14 +353,26 @@
"cancelled": "CANCELADO", "cancelled": "CANCELADO",
"get_direction": "Obtener dirección", "get_direction": "Obtener dirección",
"total": "Total", "total": "Total",
"hrs": "HRS", "hrs": "Hrs",
"workers": "$count trabajadores", "workers": "$count trabajadores",
"clock_in": "ENTRADA", "clock_in": "ENTRADA",
"clock_out": "SALIDA", "clock_out": "SALIDA",
"coverage": "Cobertura", "coverage": "Cobertura",
"workers_label": "$filled/$needed Trabajadores", "workers_label": "$filled/$needed Trabajadores",
"confirmed_workers": "Trabajadores Confirmados", "confirmed_workers": "Trabajadores Confirmados",
"no_workers": "Ningún trabajador confirmado aún." "no_workers": "Ningún trabajador confirmado aún.",
"today": "Hoy",
"tomorrow": "Mañana",
"workers_needed": "$count Trabajadores Necesarios",
"all_confirmed": "Todos los trabajadores confirmados",
"confirmed_workers_title": "TRABAJADORES CONFIRMADOS",
"message_all": "Mensaje a todos",
"show_more_workers": "Mostrar $count trabajadores más",
"checked_in": "Registrado",
"call_dialog": {
"title": "Llamar",
"message": "¿Quieres llamar a $phone?"
}
} }
}, },
"client_billing": { "client_billing": {
@@ -498,6 +526,10 @@
"menu_items": { "menu_items": {
"personal_info": "Información Personal", "personal_info": "Información Personal",
"emergency_contact": "Contacto de Emergencia", "emergency_contact": "Contacto de Emergencia",
"emergency_contact_page": {
"save_success": "Contactos de emergencia guardados con éxito",
"save_continue": "Guardar y Continuar"
},
"experience": "Experiencia", "experience": "Experiencia",
"attire": "Vestimenta", "attire": "Vestimenta",
"documents": "Documentos", "documents": "Documentos",
@@ -853,6 +885,7 @@
}, },
"staff_certificates": { "staff_certificates": {
"title": "Certificados", "title": "Certificados",
"error_loading": "Error al cargar certificados",
"progress": { "progress": {
"title": "Tu Progreso", "title": "Tu Progreso",
"verified_count": "$completed de $total verificados", "verified_count": "$completed de $total verificados",
@@ -988,6 +1021,41 @@
"applying_dialog": { "applying_dialog": {
"title": "Solicitando" "title": "Solicitando"
} }
},
"card": {
"just_now": "Recién",
"assigned": "Asignado hace $time",
"accept_shift": "Aceptar turno",
"decline_shift": "Rechazar turno"
},
"my_shifts_tab": {
"confirm_dialog": {
"title": "Aceptar Turno",
"message": "¿Estás seguro de que quieres aceptar este turno?",
"success": "¡Turno confirmado!"
},
"decline_dialog": {
"title": "Rechazar Turno",
"message": "¿Estás seguro de que quieres rechazar este turno? Esta acción no se puede deshacer.",
"success": "Turno rechazado."
},
"sections": {
"awaiting": "Esperando Confirmación",
"cancelled": "Turnos Cancelados",
"confirmed": "Turnos Confirmados"
},
"empty": {
"title": "Sin turnos esta semana",
"subtitle": "Intenta buscar nuevos trabajos en la pestaña Buscar"
},
"date": {
"today": "Hoy",
"tomorrow": "Mañana"
},
"card": {
"cancelled": "CANCELADO",
"compensation": "• Compensación de 4h"
}
} }
}, },
"staff_time_card": { "staff_time_card": {

View File

@@ -238,7 +238,7 @@ class AuthRepositoryImpl
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> response = final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> response =
await executeProtected(() => _dataConnect.getUserById(id: firebaseUserId).execute()); await executeProtected(() => _dataConnect.getUserById(id: firebaseUserId).execute());
final dc.GetUserByIdUser? user = response.data.user; final dc.GetUserByIdUser? user = response.data.user;
return user != null && user.userRole == 'BUSINESS'; return user != null && (user.userRole == 'BUSINESS' || user.userRole == 'BOTH');
} }
/// Creates Business and User entities in PostgreSQL for a Firebase user. /// Creates Business and User entities in PostgreSQL for a Firebase user.
@@ -261,18 +261,28 @@ class AuthRepositoryImpl
final dc.CreateBusinessBusinessInsert businessData = createBusinessResponse.data.business_insert; final dc.CreateBusinessBusinessInsert businessData = createBusinessResponse.data.business_insert;
onBusinessCreated(businessData.id); onBusinessCreated(businessData.id);
// Create User entity in PostgreSQL // Check if User entity already exists in PostgreSQL
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> userResult =
await executeProtected(() => _dataConnect.getUserById(id: firebaseUser.uid).execute());
final dc.GetUserByIdUser? existingUser = userResult.data.user;
final OperationResult<dc.CreateUserData, dc.CreateUserVariables> createUserResponse = if (existingUser != null) {
await executeProtected(() => _dataConnect.createUser( // User exists (likely in another app like STAFF). Update role to BOTH.
id: firebaseUser.uid, await executeProtected(() => _dataConnect.updateUser(
role: dc.UserBaseRole.USER, id: firebaseUser.uid,
) )
.email(email) .userRole('BOTH')
.userRole('BUSINESS') .execute());
.execute()); } else {
// Create new User entity in PostgreSQL
final dc.CreateUserUserInsert newUserData = createUserResponse.data.user_insert; await executeProtected(() => _dataConnect.createUser(
id: firebaseUser.uid,
role: dc.UserBaseRole.USER,
)
.email(email)
.userRole('BUSINESS')
.execute());
}
return _getUserProfile( return _getUserProfile(
firebaseUserId: firebaseUser.uid, firebaseUserId: firebaseUser.uid,
@@ -331,11 +341,11 @@ class AuthRepositoryImpl
technicalMessage: 'Firebase UID $firebaseUserId not found in users table', technicalMessage: 'Firebase UID $firebaseUserId not found in users table',
); );
} }
if (requireBusinessRole && user.userRole != 'BUSINESS') { if (requireBusinessRole && user.userRole != 'BUSINESS' && user.userRole != 'BOTH') {
await _firebaseAuth.signOut(); await _firebaseAuth.signOut();
dc.ClientSessionStore.instance.clear(); dc.ClientSessionStore.instance.clear();
throw UnauthorizedAppException( throw UnauthorizedAppException(
technicalMessage: 'User role is ${user.userRole}, expected BUSINESS', technicalMessage: 'User role is ${user.userRole}, expected BUSINESS or BOTH',
); );
} }

View File

@@ -27,94 +27,108 @@ class ClientGetStartedPage extends StatelessWidget {
), ),
SafeArea( SafeArea(
child: Column( child: LayoutBuilder(
children: <Widget>[ builder: (BuildContext context, BoxConstraints constraints) {
const SizedBox(height: UiConstants.space10), return SingleChildScrollView(
// Logo child: ConstrainedBox(
Center( constraints: BoxConstraints(
child: Image.asset( minHeight: constraints.maxHeight,
UiImageAssets.logoBlue, ),
height: 40, child: IntrinsicHeight(
fit: BoxFit.contain, child: Column(
children: <Widget>[
const SizedBox(height: UiConstants.space10),
// Logo
Center(
child: Image.asset(
UiImageAssets.logoBlue,
height: 40,
fit: BoxFit.contain,
),
),
const Spacer(),
// Content Cards Area (Keeping prototype layout)
Container(
height: 300,
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space6,
),
child: Stack(
children: <Widget>[
// Representative cards from prototype
Positioned(
top: 20,
left: 0,
right: 20,
child: _ShiftOrderCard(),
),
Positioned(
bottom: 40,
right: 0,
left: 40,
child: _WorkerProfileCard(),
),
Positioned(
top: 60,
right: 10,
child: _CalendarCard(),
),
],
),
),
const Spacer(),
// Bottom Content
Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space6,
vertical: UiConstants.space10,
),
child: Column(
children: <Widget>[
Text(
t.client_authentication.get_started_page.title,
textAlign: TextAlign.center,
style: UiTypography.displayM,
),
const SizedBox(height: UiConstants.space3),
Text(
t.client_authentication.get_started_page
.subtitle,
textAlign: TextAlign.center,
style: UiTypography.body2r.textSecondary,
),
const SizedBox(height: UiConstants.space8),
// Sign In Button
UiButton.primary(
text: t.client_authentication.get_started_page
.sign_in_button,
onPressed: () => Modular.to.toClientSignIn(),
fullWidth: true,
),
const SizedBox(height: UiConstants.space3),
// Create Account Button
UiButton.secondary(
text: t.client_authentication.get_started_page
.create_account_button,
onPressed: () => Modular.to.toClientSignUp(),
fullWidth: true,
),
],
),
),
],
),
),
), ),
), );
},
const Spacer(),
// Content Cards Area (Keeping prototype layout)
Container(
height: 300,
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space6,
),
child: Stack(
children: <Widget>[
// Representative cards from prototype
Positioned(
top: 20,
left: 0,
right: 20,
child: _ShiftOrderCard(),
),
Positioned(
bottom: 40,
right: 0,
left: 40,
child: _WorkerProfileCard(),
),
Positioned(top: 60, right: 10, child: _CalendarCard()),
],
),
),
const Spacer(),
// Bottom Content
Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space6,
vertical: UiConstants.space10,
),
child: Column(
children: <Widget>[
Text(
t.client_authentication.get_started_page.title,
textAlign: TextAlign.center,
style: UiTypography.displayM,
),
const SizedBox(height: UiConstants.space3),
Text(
t.client_authentication.get_started_page.subtitle,
textAlign: TextAlign.center,
style: UiTypography.body2r.textSecondary,
),
const SizedBox(height: UiConstants.space8),
// Sign In Button
UiButton.primary(
text: t
.client_authentication
.get_started_page
.sign_in_button,
onPressed: () => Modular.to.toClientSignIn(),
fullWidth: true,
),
const SizedBox(height: UiConstants.space3),
// Create Account Button
UiButton.secondary(
text: t
.client_authentication
.get_started_page
.create_account_button,
onPressed: () => Modular.to.toClientSignUp(),
fullWidth: true,
),
],
),
),
],
), ),
), ),
], ],

View File

@@ -151,10 +151,12 @@ class CoverageRepositoryImpl implements CoverageRepository {
shiftId: app.shiftId, shiftId: app.shiftId,
roleId: app.roleId, roleId: app.roleId,
title: app.shiftRole.role.name, title: app.shiftRole.role.name,
location: app.shiftRole.shift.location ?? '', location: app.shiftRole.shift.location ??
startTime: '00:00', app.shiftRole.shift.locationAddress ??
workersNeeded: 0, '',
date: date, startTime: _formatTime(app.shiftRole.startTime) ?? '00:00',
workersNeeded: app.shiftRole.count,
date: app.shiftRole.shift.date?.toDateTime() ?? date,
workers: <CoverageWorker>[], workers: <CoverageWorker>[],
); );

View File

@@ -63,6 +63,8 @@ class OneTimeOrderSuccessView extends StatelessWidget {
color: UiColors.accent, color: UiColors.accent,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: const Center( child: const Center(
child: Icon( child: Icon(
UiIcons.check, UiIcons.check,

View File

@@ -72,6 +72,12 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
TextPosition(offset: _messageController.text.length), TextPosition(offset: _messageController.text.length),
); );
} }
} else if (state is RapidOrderFailure) {
UiSnackbar.show(
context,
message: translateErrorKey(state.error),
type: UiSnackbarType.error,
);
} }
}, },
child: Scaffold( child: Scaffold(

View File

@@ -1,3 +1,4 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -47,7 +48,7 @@ class CoverageWidget extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Text( Text(
"TODAY'S COVERAGE", t.client_home.dashboard.todays_coverage,
style: UiTypography.footnote1b.copyWith( style: UiTypography.footnote1b.copyWith(
color: UiColors.textPrimary, color: UiColors.textPrimary,
letterSpacing: 0.5, letterSpacing: 0.5,
@@ -64,8 +65,8 @@ class CoverageWidget extends StatelessWidget {
color: backgroundColor, color: backgroundColor,
borderRadius: UiConstants.radiusLg, borderRadius: UiConstants.radiusLg,
), ),
child: Text( child: Text(
'$coveragePercent% Covered', t.client_home.dashboard.percent_covered(percent: coveragePercent),
style: UiTypography.footnote2b.copyWith(color: textColor), style: UiTypography.footnote2b.copyWith(color: textColor),
), ),
), ),
@@ -81,7 +82,7 @@ class CoverageWidget extends StatelessWidget {
child: _MetricCard( child: _MetricCard(
icon: UiIcons.target, icon: UiIcons.target,
iconColor: UiColors.primary, iconColor: UiColors.primary,
label: 'Needed', label: t.client_home.dashboard.metric_needed,
value: '$totalNeeded', value: '$totalNeeded',
), ),
), ),
@@ -91,7 +92,7 @@ class CoverageWidget extends StatelessWidget {
child: _MetricCard( child: _MetricCard(
icon: UiIcons.success, icon: UiIcons.success,
iconColor: UiColors.iconSuccess, iconColor: UiColors.iconSuccess,
label: 'Filled', label: t.client_home.dashboard.metric_filled,
value: '$totalConfirmed', value: '$totalConfirmed',
valueColor: UiColors.textSuccess, valueColor: UiColors.textSuccess,
), ),
@@ -101,7 +102,7 @@ class CoverageWidget extends StatelessWidget {
child: _MetricCard( child: _MetricCard(
icon: UiIcons.error, icon: UiIcons.error,
iconColor: UiColors.iconError, iconColor: UiColors.iconError,
label: 'Open', label: t.client_home.dashboard.metric_open,
value: '${totalNeeded - totalConfirmed}', value: '${totalNeeded - totalConfirmed}',
valueColor: UiColors.textError, valueColor: UiColors.textError,
), ),

View File

@@ -65,6 +65,8 @@ class _ShiftOrderFormSheetState extends State<ShiftOrderFormSheet> {
dc.ListTeamHubsByOwnerIdTeamHubs? _selectedHub; dc.ListTeamHubsByOwnerIdTeamHubs? _selectedHub;
bool _showSuccess = false; bool _showSuccess = false;
Map<String, dynamic>? _submitData; Map<String, dynamic>? _submitData;
bool _isSubmitting = false;
String? _errorMessage;
@override @override
void initState() { void initState() {
@@ -190,7 +192,25 @@ class _ShiftOrderFormSheetState extends State<ShiftOrderFormSheet> {
} }
Future<void> _handleSubmit() async { Future<void> _handleSubmit() async {
await _submitNewOrder(); if (_isSubmitting) return;
setState(() {
_isSubmitting = true;
_errorMessage = null;
});
try {
await _submitNewOrder();
} catch (e) {
if (!mounted) return;
setState(() {
_isSubmitting = false;
_errorMessage = 'Failed to create order. Please try again.';
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(_errorMessage!)),
);
}
} }
Future<void> _submitNewOrder() async { Future<void> _submitNewOrder() async {
@@ -296,6 +316,7 @@ class _ShiftOrderFormSheetState extends State<ShiftOrderFormSheet> {
'date': _dateController.text, 'date': _dateController.text,
}; };
_showSuccess = true; _showSuccess = true;
_isSubmitting = false;
}); });
} }
@@ -770,7 +791,7 @@ class _ShiftOrderFormSheetState extends State<ShiftOrderFormSheet> {
UiButton.primary( UiButton.primary(
text: widget.initialData != null ? 'Update Order' : 'Post Order', text: widget.initialData != null ? 'Update Order' : 'Post Order',
onPressed: widget.isLoading ? null : _handleSubmit, onPressed: (widget.isLoading || _isSubmitting) ? null : _handleSubmit,
), ),
SizedBox(height: MediaQuery.of(context).padding.bottom + UiConstants.space5), SizedBox(height: MediaQuery.of(context).padding.bottom + UiConstants.space5),
], ],

View File

@@ -78,7 +78,7 @@ class SpendingWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( Text(
'This Week', t.client_home.dashboard.spending.this_week,
style: UiTypography.footnote2r.white.copyWith( style: UiTypography.footnote2r.white.copyWith(
color: UiColors.white.withValues(alpha: 0.7), color: UiColors.white.withValues(alpha: 0.7),
fontSize: 9, fontSize: 9,
@@ -93,7 +93,7 @@ class SpendingWidget extends StatelessWidget {
), ),
), ),
Text( Text(
'$weeklyShifts shifts', t.client_home.dashboard.spending.shifts_count(count: weeklyShifts),
style: UiTypography.footnote2r.white.copyWith( style: UiTypography.footnote2r.white.copyWith(
color: UiColors.white.withValues(alpha: 0.6), color: UiColors.white.withValues(alpha: 0.6),
fontSize: 9, fontSize: 9,
@@ -107,7 +107,7 @@ class SpendingWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[ children: <Widget>[
Text( Text(
'Next 7 Days', t.client_home.dashboard.spending.next_7_days,
style: UiTypography.footnote2r.white.copyWith( style: UiTypography.footnote2r.white.copyWith(
color: UiColors.white.withValues(alpha: 0.7), color: UiColors.white.withValues(alpha: 0.7),
fontSize: 9, fontSize: 9,
@@ -122,7 +122,7 @@ class SpendingWidget extends StatelessWidget {
), ),
), ),
Text( Text(
'$next7DaysScheduled scheduled', t.client_home.dashboard.spending.scheduled_count(count: next7DaysScheduled),
style: UiTypography.footnote2r.white.copyWith( style: UiTypography.footnote2r.white.copyWith(
color: UiColors.white.withValues(alpha: 0.6), color: UiColors.white.withValues(alpha: 0.6),
fontSize: 9, fontSize: 9,

View File

@@ -227,23 +227,23 @@ class ClientHubsPage extends StatelessWidget {
} }
Future<void> _confirmDeleteHub(BuildContext context, Hub hub) async { Future<void> _confirmDeleteHub(BuildContext context, Hub hub) async {
final String hubName = hub.name.isEmpty ? 'this hub' : hub.name; final String hubName = hub.name.isEmpty ? t.client_hubs.title : hub.name;
return showDialog<void>( return showDialog<void>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (BuildContext dialogContext) { builder: (BuildContext dialogContext) {
return AlertDialog( return AlertDialog(
title: const Text('Confirm Hub Deletion'), title: Text(t.client_hubs.delete_dialog.title),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text('Are you sure you want to delete "$hubName"?'), Text(t.client_hubs.delete_dialog.message(hubName: hubName)),
const SizedBox(height: UiConstants.space2), const SizedBox(height: UiConstants.space2),
const Text('This action cannot be undone.'), Text(t.client_hubs.delete_dialog.undo_warning),
const SizedBox(height: UiConstants.space2), const SizedBox(height: UiConstants.space2),
Text( Text(
'Note that if there are any shifts/orders assigned to this hub we shouldn\'t be able to delete the hub.', t.client_hubs.delete_dialog.dependency_warning,
style: UiTypography.footnote1r.copyWith( style: UiTypography.footnote1r.copyWith(
color: UiColors.textSecondary, color: UiColors.textSecondary,
), ),
@@ -253,7 +253,7 @@ class ClientHubsPage extends StatelessWidget {
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
onPressed: () => Modular.to.pop(), onPressed: () => Modular.to.pop(),
child: const Text('Cancel'), child: Text(t.client_hubs.delete_dialog.cancel),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
@@ -265,7 +265,7 @@ class ClientHubsPage extends StatelessWidget {
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: UiColors.destructive, foregroundColor: UiColors.destructive,
), ),
child: const Text('Delete'), child: Text(t.client_hubs.delete_dialog.delete),
), ),
], ],
); );

View File

@@ -89,8 +89,8 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
final DateTime tomorrow = today.add(const Duration(days: 1)); final DateTime tomorrow = today.add(const Duration(days: 1));
final DateTime checkDate = DateTime(date.year, date.month, date.day); final DateTime checkDate = DateTime(date.year, date.month, date.day);
if (checkDate == today) return 'Today'; if (checkDate == today) return t.client_view_orders.card.today;
if (checkDate == tomorrow) return 'Tomorrow'; if (checkDate == tomorrow) return t.client_view_orders.card.tomorrow;
return DateFormat('EEE, MMM d').format(date); return DateFormat('EEE, MMM d').format(date);
} catch (_) { } catch (_) {
return dateStr; return dateStr;
@@ -279,19 +279,19 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
_buildStatItem( _buildStatItem(
icon: UiIcons.dollar, icon: UiIcons.dollar,
value: '\$${cost.round()}', value: '\$${cost.round()}',
label: 'Total', label: t.client_view_orders.card.total,
), ),
_buildStatDivider(), _buildStatDivider(),
_buildStatItem( _buildStatItem(
icon: UiIcons.clock, icon: UiIcons.clock,
value: hours.toStringAsFixed(1), value: hours.toStringAsFixed(1),
label: 'Hrs', label: t.client_view_orders.card.hrs,
), ),
_buildStatDivider(), _buildStatDivider(),
_buildStatItem( _buildStatItem(
icon: UiIcons.users, icon: UiIcons.users,
value: '${order.workersNeeded}', value: '${order.workersNeeded}',
label: 'Workers', label: t.client_create_order.one_time.workers_label,
), ),
], ],
), ),
@@ -303,14 +303,14 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: _buildTimeDisplay( child: _buildTimeDisplay(
label: 'Clock In', label: t.client_view_orders.card.clock_in,
time: _formatTime(timeStr: order.startTime), time: _formatTime(timeStr: order.startTime),
), ),
), ),
const SizedBox(width: UiConstants.space3), const SizedBox(width: UiConstants.space3),
Expanded( Expanded(
child: _buildTimeDisplay( child: _buildTimeDisplay(
label: 'Clock Out', label: t.client_view_orders.card.clock_out,
time: _formatTime(timeStr: order.endTime), time: _formatTime(timeStr: order.endTime),
), ),
), ),
@@ -341,8 +341,8 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
const SizedBox(width: UiConstants.space2), const SizedBox(width: UiConstants.space2),
Text( Text(
coveragePercent == 100 coveragePercent == 100
? 'All Workers Confirmed' ? t.client_view_orders.card.all_confirmed
: '${order.workersNeeded} Workers Needed', : t.client_view_orders.card.workers_needed(count: order.workersNeeded),
style: UiTypography.body2m.textPrimary, style: UiTypography.body2m.textPrimary,
), ),
], ],
@@ -378,7 +378,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
Padding( Padding(
padding: const EdgeInsets.only(left: 12), padding: const EdgeInsets.only(left: 12),
child: Text( child: Text(
'+${order.confirmedApps.length - 3} more', t.client_view_orders.card.show_more_workers(count: order.confirmedApps.length - 3),
style: UiTypography.footnote2r.textSecondary, style: UiTypography.footnote2r.textSecondary,
), ),
), ),
@@ -408,13 +408,13 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Text( Text(
'CONFIRMED WORKERS', t.client_view_orders.card.confirmed_workers_title,
style: UiTypography.footnote2b.textSecondary, style: UiTypography.footnote2b.textSecondary,
), ),
GestureDetector( GestureDetector(
onTap: () {}, onTap: () {},
child: Text( child: Text(
'Message All', t.client_view_orders.card.message_all,
style: UiTypography.footnote2b.copyWith( style: UiTypography.footnote2b.copyWith(
color: UiColors.primary, color: UiColors.primary,
), ),
@@ -433,7 +433,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
child: TextButton( child: TextButton(
onPressed: () {}, onPressed: () {},
child: Text( child: Text(
'Show ${order.confirmedApps.length - 5} more workers', t.client_view_orders.card.show_more_workers(count: order.confirmedApps.length - 5),
style: UiTypography.body2m.copyWith( style: UiTypography.body2m.copyWith(
color: UiColors.primary, color: UiColors.primary,
), ),
@@ -569,7 +569,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
borderRadius: UiConstants.radiusSm, borderRadius: UiConstants.radiusSm,
), ),
child: Text( child: Text(
'Checked In', t.client_view_orders.card.checked_in,
style: UiTypography.titleUppercase4m.copyWith( style: UiTypography.titleUppercase4m.copyWith(
color: UiColors.textSuccess, color: UiColors.textSuccess,
), ),
@@ -615,16 +615,16 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: const Text('Call'), title: Text(t.client_view_orders.card.call_dialog.title),
content: Text('Do you want to call $phone?'), content: Text(t.client_view_orders.card.call_dialog.message(phone: phone)),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(false), onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'), child: Text(t.common.cancel),
), ),
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(context).pop(true),
child: const Text('Call'), child: Text(t.client_view_orders.card.call_dialog.title),
), ),
], ],
); );

View File

@@ -156,24 +156,30 @@ class AuthRepositoryImpl
.userRole('STAFF') .userRole('STAFF')
.execute()); .execute());
} else { } else {
if (user.userRole != 'STAFF') { // User exists in PostgreSQL. Check if they have a STAFF profile.
await firebaseAuth.signOut();
throw const domain.UnauthorizedAppException(
technicalMessage: 'User is not authorized for this app.',
);
}
final QueryResult<GetStaffByUserIdData, GetStaffByUserIdVariables> final QueryResult<GetStaffByUserIdData, GetStaffByUserIdVariables>
staffResponse = await executeProtected(() => dataConnect staffResponse = await executeProtected(() => dataConnect
.getStaffByUserId( .getStaffByUserId(
userId: firebaseUser.uid, userId: firebaseUser.uid,
) )
.execute()); .execute());
if (staffResponse.data.staffs.isNotEmpty) { if (staffResponse.data.staffs.isNotEmpty) {
// If profile exists, they should use Login mode.
await firebaseAuth.signOut(); await firebaseAuth.signOut();
throw const domain.AccountExistsException( throw const domain.AccountExistsException(
technicalMessage: 'This user already has a staff profile. Please log in.', technicalMessage:
'This user already has a staff profile. Please log in.',
); );
} }
// If they don't have a staff profile but they exist as BUSINESS,
// they are allowed to "Sign Up" for Staff.
// We update their userRole to 'BOTH'.
if (user.userRole == 'BUSINESS') {
await executeProtected(() =>
dataConnect.updateUser(id: firebaseUser.uid).userRole('BOTH').execute());
}
} }
} else { } else {
if (user == null) { if (user == null) {
@@ -182,7 +188,8 @@ class AuthRepositoryImpl
technicalMessage: 'Authenticated user profile not found in database.', technicalMessage: 'Authenticated user profile not found in database.',
); );
} }
if (user.userRole != 'STAFF') { // Allow STAFF or BOTH roles to log in to the Staff App
if (user.userRole != 'STAFF' && user.userRole != 'BOTH') {
await firebaseAuth.signOut(); await firebaseAuth.signOut();
throw const domain.UnauthorizedAppException( throw const domain.UnauthorizedAppException(
technicalMessage: 'User is not authorized for this app.', technicalMessage: 'User is not authorized for this app.',

View File

@@ -5,7 +5,7 @@ import 'package:staff_authentication/src/domain/ui_entities/auth_mode.dart';
abstract class AuthEvent extends Equatable { abstract class AuthEvent extends Equatable {
const AuthEvent(); const AuthEvent();
@override @override
List<Object> get props => <Object>[]; List<Object?> get props => <Object?>[];
} }
/// Event for requesting a sign-in with a phone number. /// Event for requesting a sign-in with a phone number.
@@ -19,7 +19,7 @@ class AuthSignInRequested extends AuthEvent {
const AuthSignInRequested({this.phoneNumber, required this.mode}); const AuthSignInRequested({this.phoneNumber, required this.mode});
@override @override
List<Object> get props => <Object>[mode]; List<Object?> get props => <Object?>[phoneNumber, mode];
} }
/// Event for submitting an OTP (One-Time Password) for verification. /// Event for submitting an OTP (One-Time Password) for verification.
@@ -43,7 +43,7 @@ class AuthOtpSubmitted extends AuthEvent {
}); });
@override @override
List<Object> get props => <Object>[verificationId, smsCode, mode]; List<Object?> get props => <Object?>[verificationId, smsCode, mode];
} }
/// Event for clearing any authentication error in the state. /// Event for clearing any authentication error in the state.
@@ -57,7 +57,7 @@ class AuthResetRequested extends AuthEvent {
const AuthResetRequested({required this.mode}); const AuthResetRequested({required this.mode});
@override @override
List<Object> get props => <Object>[mode]; List<Object?> get props => <Object?>[mode];
} }
/// Event for ticking down the resend cooldown. /// Event for ticking down the resend cooldown.
@@ -67,7 +67,7 @@ class AuthCooldownTicked extends AuthEvent {
const AuthCooldownTicked(this.secondsRemaining); const AuthCooldownTicked(this.secondsRemaining);
@override @override
List<Object> get props => <Object>[secondsRemaining]; List<Object?> get props => <Object?>[secondsRemaining];
} }
/// Event for updating the current draft OTP in the state. /// Event for updating the current draft OTP in the state.
@@ -78,7 +78,7 @@ class AuthOtpUpdated extends AuthEvent {
const AuthOtpUpdated(this.otp); const AuthOtpUpdated(this.otp);
@override @override
List<Object> get props => <Object>[otp]; List<Object?> get props => <Object?>[otp];
} }
/// Event for updating the current draft phone number in the state. /// Event for updating the current draft phone number in the state.
@@ -89,5 +89,5 @@ class AuthPhoneUpdated extends AuthEvent {
const AuthPhoneUpdated(this.phoneNumber); const AuthPhoneUpdated(this.phoneNumber);
@override @override
List<Object> get props => <Object>[phoneNumber]; List<Object?> get props => <Object?>[phoneNumber];
} }

View File

@@ -50,7 +50,13 @@ class _PhoneVerificationPageState extends State<PhoneVerificationPage> {
required BuildContext context, required BuildContext context,
required String phoneNumber, required String phoneNumber,
}) { }) {
final String normalized = phoneNumber.replaceAll(RegExp(r'\\D'), ''); String normalized = phoneNumber.replaceAll(RegExp(r'\D'), '');
// Handle US numbers entered with a leading 1
if (normalized.length == 11 && normalized.startsWith('1')) {
normalized = normalized.substring(1);
}
if (normalized.length == 10) { if (normalized.length == 10) {
BlocProvider.of<AuthBloc>( BlocProvider.of<AuthBloc>(
context, context,

View File

@@ -88,7 +88,7 @@ class _PhoneInputFormFieldState extends State<PhoneInputFormField> {
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
inputFormatters: <TextInputFormatter>[ inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly, FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(10), LengthLimitingTextInputFormatter(11),
], ],
decoration: InputDecoration( decoration: InputDecoration(
hintText: t.staff_authentication.phone_input.hint, hintText: t.staff_authentication.phone_input.hint,

View File

@@ -1,3 +1,4 @@
import 'package:core_localization/core_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
@@ -58,9 +59,9 @@ class _ShiftCardState extends State<ShiftCard> {
try { try {
final date = DateTime.parse(dateStr); final date = DateTime.parse(dateStr);
final diff = DateTime.now().difference(date); final diff = DateTime.now().difference(date);
if (diff.inHours < 1) return 'Just now'; if (diff.inHours < 1) return t.staff_shifts.card.just_now;
if (diff.inHours < 24) return 'Pending ${diff.inHours}h ago'; if (diff.inHours < 24) return t.staff_shifts.details.pending_time(time: '${diff.inHours}h');
return 'Pending ${diff.inDays}d ago'; return t.staff_shifts.details.pending_time(time: '${diff.inDays}d');
} catch (e) { } catch (e) {
return ''; return '';
} }
@@ -220,7 +221,11 @@ class _ShiftCardState extends State<ShiftCard> {
borderRadius: UiConstants.radiusFull, borderRadius: UiConstants.radiusFull,
), ),
child: Text( child: Text(
'Assigned ${_getTimeAgo(widget.shift.createdDate).replaceAll('Pending ', '')}', t.staff_shifts.card.assigned(
time: _getTimeAgo(widget.shift.createdDate)
.replaceAll('Pending ', '')
.replaceAll('Just now', 'just now'),
),
style: UiTypography.body3m.white, style: UiTypography.body3m.white,
), ),
), ),
@@ -302,13 +307,13 @@ class _ShiftCardState extends State<ShiftCard> {
children: [ children: [
_buildTag( _buildTag(
UiIcons.zap, UiIcons.zap,
'Immediate start', t.staff_shifts.tags.immediate_start,
UiColors.accent.withValues(alpha: 0.3), UiColors.accent.withValues(alpha: 0.3),
UiColors.foreground, UiColors.foreground,
), ),
_buildTag( _buildTag(
UiIcons.timer, UiIcons.timer,
'No experience', t.staff_shifts.tags.no_experience,
UiColors.tagError, UiColors.tagError,
UiColors.textError, UiColors.textError,
), ),
@@ -342,7 +347,7 @@ class _ShiftCardState extends State<ShiftCard> {
BorderRadius.circular(UiConstants.radiusBase), BorderRadius.circular(UiConstants.radiusBase),
), ),
), ),
child: const Text('Accept shift'), child: Text(t.staff_shifts.card.accept_shift),
), ),
), ),
const SizedBox(height: UiConstants.space2), const SizedBox(height: UiConstants.space2),
@@ -361,7 +366,7 @@ class _ShiftCardState extends State<ShiftCard> {
BorderRadius.circular(UiConstants.radiusBase), BorderRadius.circular(UiConstants.radiusBase),
), ),
), ),
child: const Text('Decline shift'), child: Text(t.staff_shifts.card.decline_shift),
), ),
), ),
const SizedBox(height: UiConstants.space5), const SizedBox(height: UiConstants.space5),

View File

@@ -35,14 +35,14 @@ class CertificatesPage extends StatelessWidget {
if (state.status == CertificatesStatus.failure) { if (state.status == CertificatesStatus.failure) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Certificates')), appBar: AppBar(title: Text(t.staff_certificates.title)),
body: Center( body: Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Text( child: Text(
state.errorMessage != null state.errorMessage != null
? translateErrorKey(state.errorMessage!) ? translateErrorKey(state.errorMessage!)
: 'Error loading certificates', : t.staff_certificates.error_loading,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: UiTypography.body2r.textSecondary, style: UiTypography.body2r.textSecondary,
), ),

View File

@@ -1,3 +1,4 @@
import 'package:core_localization/core_localization.dart';
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:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@@ -18,7 +19,7 @@ class EmergencyContactSaveButton extends StatelessWidget {
if (state.status == EmergencyContactStatus.saved) { if (state.status == EmergencyContactStatus.saved) {
UiSnackbar.show( UiSnackbar.show(
context, context,
message: 'Emergency contacts saved successfully', message: t.staff.profile.menu_items.emergency_contact_page.save_success,
type: UiSnackbarType.success, type: UiSnackbarType.success,
margin: const EdgeInsets.only(bottom: 150, left: 16, right: 16), margin: const EdgeInsets.only(bottom: 150, left: 16, right: 16),
); );
@@ -48,7 +49,7 @@ class EmergencyContactSaveButton extends StatelessWidget {
), ),
), ),
) )
: const Text('Save & Continue'), : Text(t.staff.profile.menu_items.emergency_contact_page.save_continue),
), ),
), ),
); );

View File

@@ -105,12 +105,12 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Accept Shift'), title: Text(t.staff_shifts.my_shifts_tab.confirm_dialog.title),
content: const Text('Are you sure you want to accept this shift?'), content: Text(t.staff_shifts.my_shifts_tab.confirm_dialog.message),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'), child: Text(t.common.cancel),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
@@ -118,14 +118,14 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
context.read<ShiftsBloc>().add(AcceptShiftEvent(id)); context.read<ShiftsBloc>().add(AcceptShiftEvent(id));
UiSnackbar.show( UiSnackbar.show(
context, context,
message: 'Shift confirmed!', message: t.staff_shifts.my_shifts_tab.confirm_dialog.success,
type: UiSnackbarType.success, type: UiSnackbarType.success,
); );
}, },
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: UiColors.success, foregroundColor: UiColors.success,
), ),
child: const Text('Accept'), child: Text(t.staff_shifts.shift_details.accept_shift),
), ),
], ],
), ),
@@ -136,14 +136,14 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Decline Shift'), title: Text(t.staff_shifts.my_shifts_tab.decline_dialog.title),
content: const Text( content: Text(
'Are you sure you want to decline this shift? This action cannot be undone.', t.staff_shifts.my_shifts_tab.decline_dialog.message,
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'), child: Text(t.common.cancel),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
@@ -151,14 +151,14 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
context.read<ShiftsBloc>().add(DeclineShiftEvent(id)); context.read<ShiftsBloc>().add(DeclineShiftEvent(id));
UiSnackbar.show( UiSnackbar.show(
context, context,
message: 'Shift declined.', message: t.staff_shifts.my_shifts_tab.decline_dialog.success,
type: UiSnackbarType.error, type: UiSnackbarType.error,
); );
}, },
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: UiColors.destructive, foregroundColor: UiColors.destructive,
), ),
child: const Text('Decline'), child: Text(t.staff_shifts.shift_details.decline),
), ),
], ],
), ),
@@ -169,9 +169,9 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
try { try {
final date = DateTime.parse(dateStr); final date = DateTime.parse(dateStr);
final now = DateTime.now(); final now = DateTime.now();
if (_isSameDay(date, now)) return "Today"; if (_isSameDay(date, now)) return t.staff_shifts.my_shifts_tab.date.today;
final tomorrow = now.add(const Duration(days: 1)); final tomorrow = now.add(const Duration(days: 1));
if (_isSameDay(date, tomorrow)) return "Tomorrow"; if (_isSameDay(date, tomorrow)) return t.staff_shifts.my_shifts_tab.date.tomorrow;
return DateFormat('EEE, MMM d').format(date); return DateFormat('EEE, MMM d').format(date);
} catch (_) { } catch (_) {
return dateStr; return dateStr;
@@ -338,7 +338,7 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
const SizedBox(height: UiConstants.space5), const SizedBox(height: UiConstants.space5),
if (widget.pendingAssignments.isNotEmpty) ...[ if (widget.pendingAssignments.isNotEmpty) ...[
_buildSectionHeader( _buildSectionHeader(
"Awaiting Confirmation", t.staff_shifts.my_shifts_tab.sections.awaiting,
UiColors.textWarning, UiColors.textWarning,
), ),
...widget.pendingAssignments.map( ...widget.pendingAssignments.map(
@@ -356,7 +356,7 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
], ],
if (visibleCancelledShifts.isNotEmpty) ...[ if (visibleCancelledShifts.isNotEmpty) ...[
_buildSectionHeader("Cancelled Shifts", UiColors.textSecondary), _buildSectionHeader(t.staff_shifts.my_shifts_tab.sections.cancelled, UiColors.textSecondary),
...visibleCancelledShifts.map( ...visibleCancelledShifts.map(
(shift) => Padding( (shift) => Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space4), padding: const EdgeInsets.only(bottom: UiConstants.space4),
@@ -378,7 +378,7 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
// Confirmed Shifts // Confirmed Shifts
if (visibleMyShifts.isNotEmpty) ...[ if (visibleMyShifts.isNotEmpty) ...[
_buildSectionHeader("Confirmed Shifts", UiColors.textSecondary), _buildSectionHeader(t.staff_shifts.my_shifts_tab.sections.confirmed, UiColors.textSecondary),
...visibleMyShifts.map( ...visibleMyShifts.map(
(shift) => Padding( (shift) => Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space3), padding: const EdgeInsets.only(bottom: UiConstants.space3),
@@ -390,10 +390,10 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
if (visibleMyShifts.isEmpty && if (visibleMyShifts.isEmpty &&
widget.pendingAssignments.isEmpty && widget.pendingAssignments.isEmpty &&
widget.cancelledShifts.isEmpty) widget.cancelledShifts.isEmpty)
const EmptyStateView( EmptyStateView(
icon: UiIcons.calendar, icon: UiIcons.calendar,
title: "No shifts this week", title: t.staff_shifts.my_shifts_tab.empty.title,
subtitle: "Try finding new jobs in the Find tab", subtitle: t.staff_shifts.my_shifts_tab.empty.subtitle,
), ),
const SizedBox(height: UiConstants.space32), const SizedBox(height: UiConstants.space32),
@@ -462,13 +462,13 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Text(
"CANCELLED", t.staff_shifts.my_shifts_tab.card.cancelled,
style: UiTypography.footnote2b.textError, style: UiTypography.footnote2b.textError,
), ),
if (isLastMinute) ...[ if (isLastMinute) ...[
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
"• 4hr compensation", t.staff_shifts.my_shifts_tab.card.compensation,
style: UiTypography.footnote2m.textSuccess, style: UiTypography.footnote2m.textSuccess,
), ),
], ],

View File

@@ -598,16 +598,19 @@ query listStaffsApplicationsByBusinessForDay(
appliedAt appliedAt
status status
shiftRole{ shiftRole {
shift{ startTime
shift {
date
location location
locationAddress
cost cost
} }
count count
assigned assigned
hours hours
role{ role {
name name
} }
} }

View File

@@ -46,7 +46,7 @@ mutation createOrder(
detectedConflicts: $detectedConflicts detectedConflicts: $detectedConflicts
poReference: $poReference poReference: $poReference
} }
) )
} }
mutation updateOrder( mutation updateOrder(
@@ -92,7 +92,7 @@ mutation updateOrder(
detectedConflicts: $detectedConflicts detectedConflicts: $detectedConflicts
poReference: $poReference poReference: $poReference
} }
) )
} }
mutation deleteOrder($id: UUID!) @auth(level: USER) { mutation deleteOrder($id: UUID!) @auth(level: USER) {

View File

@@ -321,7 +321,6 @@ query listShiftRolesByBusinessAndDateRange(
shift: { shift: {
date: { ge: $start, le: $end } date: { ge: $start, le: $end }
order: { businessId: { eq: $businessId } } order: { businessId: { eq: $businessId } }
status: { eq: $status }
} }
} }
offset: $offset offset: $offset

View File

@@ -22,7 +22,7 @@ enum OrderDuration {
} }
#events #events
type Order @table(name: "orders") { type Order @table(name: "orders", key: ["id"]) {
id: UUID! @default(expr: "uuidV4()") id: UUID! @default(expr: "uuidV4()")
eventName: String eventName: String

View File

@@ -10,7 +10,7 @@ enum ShiftStatus {
CANCELED CANCELED
} }
type Shift @table(name: "shifts") { type Shift @table(name: "shifts", key: ["id"]) {
id: UUID! @default(expr: "uuidV4()") id: UUID! @default(expr: "uuidV4()")
title: String! title: String!