feat: localization, file restriction banners, test credentials, edit icon fix

- #553: Audit and verify localizations (en/es), replace hardcoded strings
- #549: Incomplete profile banner in Find Shifts (staff app)
- #550: File restriction banner on document upload page
- #551: File restriction banner on certificate upload page
- #552: File restriction banner on attire upload page
- #492: Hide edit icon for past/completed orders (client app)
- #524: Display worker benefits in staff app
- Add test credentials to seed: testclient@gmail.com, staff +1-555-555-1234
- Fix document upload validation (context arg in _validatePdfFile on submit)
- Add PR_LOCALIZATION.md

Made-with: Cursor
This commit is contained in:
2026-02-27 13:48:04 +05:30
parent 66ffce413a
commit 34afe09963
26 changed files with 865 additions and 132 deletions

36
PR_LOCALIZATION.md Normal file
View File

@@ -0,0 +1,36 @@
# PR: Localize all user-facing strings (#553)
## Summary
All user-facing strings in the mobile apps have been localized. English and Spanish translations are provided via `core_localization` (Slang).
## Changes
### i18n (en.i18n.json & es.i18n.json)
- **common**: `file_not_found`, `error_occurred`, `gallery`, `camera`, `english`, `spanish`
- **staff_profile_attire.capture**: Attire capture flow (attest, validation, status, filters)
- **staff_certificates.upload_modal**: `name_hint`, `issuer_hint`
- **staff_documents**: `file_not_found`, `unknown`
- **staff_shifts.my_shift_card**: `checked_in`, `submit_for_approval`, `timesheet_submitted`, `submitted`, `ready_to_submit`
- **staff_shifts.shift_details**: `eligibility_requirements`
- **staff_shifts.shift_location**: `could_not_open_maps`
- **staff_shifts.my_shifts_tab**: `swap_coming_soon`
- **staff_clock_in**: `map_view_gps`
- **client_orders_common**: `select_vendor`, `hub`, `order_name`, `no_vendors`, `no_vendors_desc`
- **client_view_orders.order_edit_sheet**: Section headers and labels
### Staff app
- **Attire**: `attire_capture_page`, `image_preview_section`, `attire_page` all hardcoded strings replaced
- **Certificates**: `certificate_upload_page` file validation, hint text
- **Documents**: `document_upload_page`, `documents_page` file not found, unknown error
- **Shifts**: `my_shift_card` (status, timesheet), `my_shifts_tab` (swap message), `shift_location_section`, `shift_details_page`
- **Clock-in**: `location_map_placeholder` Map View (GPS)
### Client app
- **Orders**: `order_edit_sheet` ORDER NAME, HUB, SELECT VENDOR section headers
### Slang
- Ran `dart run slang` to regenerate i18n code
## Verification
- Slang generation succeeds
- All affected Dart files use `context.t` or `Translations.of(context)` for user-facing strings

View File

@@ -4,7 +4,13 @@
"cancel": "Cancel", "cancel": "Cancel",
"save": "Save", "save": "Save",
"delete": "Delete", "delete": "Delete",
"continue_text": "Continue" "continue_text": "Continue",
"error_occurred": "An error occurred",
"file_not_found": "File not found.",
"gallery": "Gallery",
"camera": "Camera",
"english": "English",
"spanish": "Español"
}, },
"settings": { "settings": {
"language": "Language", "language": "Language",
@@ -209,6 +215,7 @@
"hubs": "Hubs", "hubs": "Hubs",
"log_out": "Log Out", "log_out": "Log Out",
"log_out_confirmation": "Are you sure you want to log out?", "log_out_confirmation": "Are you sure you want to log out?",
"signed_out_successfully": "Signed out successfully",
"quick_links": "Quick Links", "quick_links": "Quick Links",
"clock_in_hubs": "Clock-In Hubs", "clock_in_hubs": "Clock-In Hubs",
"billing_payments": "Billing & Payments" "billing_payments": "Billing & Payments"
@@ -287,6 +294,7 @@
"edit_button": "Edit Hub", "edit_button": "Edit Hub",
"deleted_success": "Hub deleted successfully" "deleted_success": "Hub deleted successfully"
}, },
"nfc_assigned_success": "NFC tag assigned successfully",
"nfc_dialog": { "nfc_dialog": {
"title": "Identify NFC Tag", "title": "Identify NFC Tag",
"instruction": "Tap your phone to the NFC tag to identify it", "instruction": "Tap your phone to the NFC tag to identify it",
@@ -303,6 +311,17 @@
"delete": "Delete" "delete": "Delete"
} }
}, },
"client_orders_common": {
"select_vendor": "SELECT VENDOR",
"hub": "HUB",
"order_name": "ORDER NAME",
"permanent_days": "Permanent Days",
"recurring_days": "Recurring Days",
"start_date": "Start Date",
"end_date": "End Date",
"no_vendors": "No Vendors Available",
"no_vendors_desc": "There are no staffing vendors associated with your account."
},
"client_create_order": { "client_create_order": {
"title": "Create Order", "title": "Create Order",
"section_title": "ORDER TYPE", "section_title": "ORDER TYPE",
@@ -324,6 +343,7 @@
"need_staff": "Need staff urgently?", "need_staff": "Need staff urgently?",
"type_or_speak": "Type or speak what you need. I'll handle the rest", "type_or_speak": "Type or speak what you need. I'll handle the rest",
"example": "Example: ", "example": "Example: ",
"placeholder_message": "Need 2 servers for a banquet right now.",
"hint": "Type or speak... (e.g., \"Need 5 cooks ASAP until 5am\")", "hint": "Type or speak... (e.g., \"Need 5 cooks ASAP until 5am\")",
"speak": "Speak", "speak": "Speak",
"listening": "Listening...", "listening": "Listening...",
@@ -463,6 +483,11 @@
} }
} }
}, },
"client_billing_common": {
"invoices_ready": "Invoices Ready",
"total_amount": "TOTAL AMOUNT",
"no_invoices_ready": "No invoices ready yet"
},
"client_billing": { "client_billing": {
"title": "Billing", "title": "Billing",
"current_period": "Current Period", "current_period": "Current Period",
@@ -595,6 +620,11 @@
"overview": { "overview": {
"title": "Your Benefits Overview", "title": "Your Benefits Overview",
"subtitle": "Manage and track your earned benefits here", "subtitle": "Manage and track your earned benefits here",
"entitlement": "Entitlement",
"used": "Used",
"remaining": "Remaining",
"hours": "hours",
"empty_state": "No benefits available",
"request_payment": "Request Payment for $benefit", "request_payment": "Request Payment for $benefit",
"request_submitted": "Request submitted for $benefit", "request_submitted": "Request submitted for $benefit",
"sick_leave_subtitle": "You need at least 8 hours to request sick leave", "sick_leave_subtitle": "You need at least 8 hours to request sick leave",
@@ -844,6 +874,7 @@
"checkout_complete": "Check Out!", "checkout_complete": "Check Out!",
"checkin_complete": "Check In!" "checkin_complete": "Check In!"
}, },
"map_view_gps": "Map View (GPS)",
"lunch_break": { "lunch_break": {
"title": "Did You Take\na Lunch?", "title": "Did You Take\na Lunch?",
"no": "No", "no": "No",
@@ -1041,7 +1072,8 @@
}, },
"list": { "list": {
"empty": "No documents found", "empty": "No documents found",
"error": "Error: $message" "error": "Error: $message",
"unknown": "Unknown"
}, },
"card": { "card": {
"view": "View", "view": "View",
@@ -1053,6 +1085,8 @@
}, },
"upload": { "upload": {
"instructions": "Please select a valid PDF file to upload.", "instructions": "Please select a valid PDF file to upload.",
"pdf_banner": "Only PDF files are accepted. Maximum file size is 10MB.",
"file_not_found": "File not found.",
"submit": "Submit Document", "submit": "Submit Document",
"select_pdf": "Select PDF File", "select_pdf": "Select PDF File",
"attestation": "I certify that this document is genuine and valid.", "attestation": "I certify that this document is genuine and valid.",
@@ -1088,12 +1122,14 @@
"upload_modal": { "upload_modal": {
"title": "Upload Certificate", "title": "Upload Certificate",
"name_label": "Certificate Name", "name_label": "Certificate Name",
"name_hint": "e.g. Food Handler Permit",
"issuer_label": "Certificate Issuer", "issuer_label": "Certificate Issuer",
"issuer_hint": "e.g. Department of Health",
"expiry_label": "Expiration Date (Optional)", "expiry_label": "Expiration Date (Optional)",
"select_date": "Select date", "select_date": "Select date",
"upload_file": "Upload File", "upload_file": "Upload File",
"drag_drop": "Drag and drop or click to upload", "drag_drop": "Drag and drop or click to upload",
"supported_formats": "PDF, JPG, PNG up to 10MB", "supported_formats": "PDF up to 10MB",
"cancel": "Cancel", "cancel": "Cancel",
"save": "Save Certificate", "save": "Save Certificate",
"success_snackbar": "Certificate successfully uploaded and pending verification" "success_snackbar": "Certificate successfully uploaded and pending verification"
@@ -1125,6 +1161,22 @@
"select_required": "\u2713 Select all required items", "select_required": "\u2713 Select all required items",
"upload_required": "\u2713 Upload photos of required items", "upload_required": "\u2713 Upload photos of required items",
"accept_attestation": "\u2713 Accept attestation" "accept_attestation": "\u2713 Accept attestation"
},
"upload_file_types_banner": "Only JPEG, JPG, and PNG files are accepted. Maximum file size is 10MB.",
"capture": {
"attest_please": "Please attest that you own this item.",
"could_not_access_media": "Could not access camera or gallery. Please try again.",
"attire_submitted": "Attire image submitted for verification",
"file_size_exceeds": "File size exceeds 10MB. Maximum file size is 10MB.",
"pending_verification": "Pending Verification",
"not_uploaded": "Not Uploaded",
"your_uploaded_photo": "Your Uploaded Photo",
"reference_example": "Reference Example",
"review_attire_item": "Review the attire item",
"example_upload_hint": "Example of the item that you need to upload.",
"no_items_filter": "No items found for this filter.",
"approved": "Approved",
"rejected": "Rejected"
} }
}, },
"staff_shifts": { "staff_shifts": {
@@ -1209,7 +1261,21 @@
}, },
"applying_dialog": { "applying_dialog": {
"title": "Applying" "title": "Applying"
} },
"eligibility_requirements": "Eligibility Requirements"
},
"my_shift_card": {
"submit_for_approval": "Submit for Approval",
"timesheet_submitted": "Timesheet submitted for client approval",
"checked_in": "Checked in",
"submitted": "SUBMITTED",
"ready_to_submit": "READY TO SUBMIT"
},
"shift_location": {
"could_not_open_maps": "Could not open maps"
},
"history_tab": {
"subtitle": "Completed shifts appear here"
}, },
"card": { "card": {
"just_now": "Just now", "just_now": "Just now",
@@ -1218,6 +1284,7 @@
"decline_shift": "Decline shift" "decline_shift": "Decline shift"
}, },
"my_shifts_tab": { "my_shifts_tab": {
"swap_coming_soon": "Swap functionality coming soon!",
"confirm_dialog": { "confirm_dialog": {
"title": "Accept Shift", "title": "Accept Shift",
"message": "Are you sure you want to accept this shift?", "message": "Are you sure you want to accept this shift?",
@@ -1247,6 +1314,9 @@
} }
}, },
"find_shifts": { "find_shifts": {
"incomplete_profile_banner_title": "Your account isn't complete yet.",
"incomplete_profile_banner_message": "You won't be able to apply for shifts until your account is fully set up. Complete your account now to unlock shift applications and start getting matched with opportunities.",
"incomplete_profile_cta": "Complete your account now",
"search_hint": "Search jobs, location...", "search_hint": "Search jobs, location...",
"filter_all": "All Jobs", "filter_all": "All Jobs",
"filter_one_day": "One Day", "filter_one_day": "One Day",
@@ -1585,12 +1655,30 @@
} }
}, },
"client_coverage": { "client_coverage": {
"todays_status": "Today's Status",
"unfilled_today": "Unfilled Today",
"running_late": "Running Late",
"checked_in": "Checked In",
"todays_cost": "Today's Cost",
"no_shifts_day": "No shifts scheduled for this day",
"no_workers_assigned": "No workers assigned yet",
"worker_row": { "worker_row": {
"verify": "Verify", "verify": "Verify",
"verified_message": "Worker attire verified for $name" "verified_message": "Worker attire verified for $name"
} }
}, },
"client_reports_common": {
"export_coming_soon": "Export coming soon"
},
"client_authentication_demo": {
"shift_order_placeholder": "Shift Order #824",
"worker_name_placeholder": "Alex Thompson"
},
"staff_payments": { "staff_payments": {
"bank_placeholder": "Chase Bank",
"ending_in": "Ending in 4321",
"this_week": "This Week",
"this_month": "This Month",
"early_pay": { "early_pay": {
"title": "Early Pay", "title": "Early Pay",
"available_label": "Available for Cash Out", "available_label": "Available for Cash Out",

View File

@@ -4,7 +4,13 @@
"cancel": "Cancelar", "cancel": "Cancelar",
"save": "Guardar", "save": "Guardar",
"delete": "Eliminar", "delete": "Eliminar",
"continue_text": "Continuar" "continue_text": "Continuar",
"error_occurred": "Ocurrió un error",
"file_not_found": "Archivo no encontrado.",
"gallery": "Galería",
"camera": "Cámara",
"english": "English",
"spanish": "Español"
}, },
"settings": { "settings": {
"language": "Idioma", "language": "Idioma",
@@ -209,6 +215,7 @@
"hubs": "Hubs", "hubs": "Hubs",
"log_out": "Cerrar sesi\u00f3n", "log_out": "Cerrar sesi\u00f3n",
"log_out_confirmation": "\u00bfEst\u00e1 seguro de que desea cerrar sesi\u00f3n?", "log_out_confirmation": "\u00bfEst\u00e1 seguro de que desea cerrar sesi\u00f3n?",
"signed_out_successfully": "Sesi\u00f3n cerrada correctamente",
"quick_links": "Enlaces r\u00e1pidos", "quick_links": "Enlaces r\u00e1pidos",
"clock_in_hubs": "Hubs de Marcaje", "clock_in_hubs": "Hubs de Marcaje",
"billing_payments": "Facturaci\u00f3n y Pagos" "billing_payments": "Facturaci\u00f3n y Pagos"
@@ -301,7 +308,19 @@
"cost_center_label": "Centro de Costos", "cost_center_label": "Centro de Costos",
"cost_center_none": "No asignado", "cost_center_none": "No asignado",
"deleted_success": "Hub eliminado exitosamente" "deleted_success": "Hub eliminado exitosamente"
} },
"nfc_assigned_success": "Etiqueta NFC asignada exitosamente"
},
"client_orders_common": {
"select_vendor": "SELECCIONAR PROVEEDOR",
"hub": "HUB",
"order_name": "NOMBRE DE ORDEN",
"permanent_days": "Días Permanentes",
"recurring_days": "Días Recurrentes",
"start_date": "Fecha de Inicio",
"end_date": "Fecha de Fin",
"no_vendors": "No Hay Proveedores Disponibles",
"no_vendors_desc": "No hay proveedores de personal asociados a tu cuenta."
}, },
"client_create_order": { "client_create_order": {
"title": "Crear Orden", "title": "Crear Orden",
@@ -324,6 +343,7 @@
"need_staff": "\u00bfNecesitas personal urgentemente?", "need_staff": "\u00bfNecesitas personal urgentemente?",
"type_or_speak": "Escribe o habla lo que necesitas. Yo me encargo del resto", "type_or_speak": "Escribe o habla lo que necesitas. Yo me encargo del resto",
"example": "Ejemplo: ", "example": "Ejemplo: ",
"placeholder_message": "Necesito 2 meseros para un banquete ahora mismo.",
"hint": "Escribe o habla... (ej., \"Necesito 5 cocineros YA hasta las 5am\")", "hint": "Escribe o habla... (ej., \"Necesito 5 cocineros YA hasta las 5am\")",
"speak": "Hablar", "speak": "Hablar",
"listening": "Escuchando...", "listening": "Escuchando...",
@@ -595,6 +615,11 @@
"overview": { "overview": {
"title": "Resumen de tus Beneficios", "title": "Resumen de tus Beneficios",
"subtitle": "Gestiona y sigue tus beneficios ganados aqu\u00ed", "subtitle": "Gestiona y sigue tus beneficios ganados aqu\u00ed",
"entitlement": "Derecho",
"used": "Usado",
"remaining": "Disponible",
"hours": "horas",
"empty_state": "No hay beneficios disponibles",
"request_payment": "Solicitar pago por $benefit", "request_payment": "Solicitar pago por $benefit",
"request_submitted": "Solicitud enviada para $benefit", "request_submitted": "Solicitud enviada para $benefit",
"sick_leave_subtitle": "Necesitas al menos 8 horas para solicitar d\u00edas de enfermedad", "sick_leave_subtitle": "Necesitas al menos 8 horas para solicitar d\u00edas de enfermedad",
@@ -834,6 +859,7 @@
"arrived_title": "\u00a1Has llegado! \ud83c\udf89", "arrived_title": "\u00a1Has llegado! \ud83c\udf89",
"arrived_desc": "Est\u00e1s en el lugar del turno. \u00bfListo para registrar tu entrada?" "arrived_desc": "Est\u00e1s en el lugar del turno. \u00bfListo para registrar tu entrada?"
}, },
"map_view_gps": "Vista de Mapa (GPS)",
"swipe": { "swipe": {
"checking_out": "Registrando salida...", "checking_out": "Registrando salida...",
"checking_in": "Registrando entrada...", "checking_in": "Registrando entrada...",
@@ -1041,7 +1067,8 @@
}, },
"list": { "list": {
"empty": "No se encontraron documentos", "empty": "No se encontraron documentos",
"error": "Error: $message" "error": "Error: $message",
"unknown": "Desconocido"
}, },
"card": { "card": {
"view": "Ver", "view": "Ver",
@@ -1053,12 +1080,14 @@
}, },
"upload": { "upload": {
"instructions": "Por favor selecciona un archivo PDF válido para subir.", "instructions": "Por favor selecciona un archivo PDF válido para subir.",
"pdf_banner": "Solo se aceptan archivos PDF. Tamaño máximo del archivo: 10MB.",
"submit": "Enviar Documento", "submit": "Enviar Documento",
"select_pdf": "Seleccionar Archivo PDF", "select_pdf": "Seleccionar Archivo PDF",
"attestation": "Certifico que este documento es genuino y válido.", "attestation": "Certifico que este documento es genuino y válido.",
"success": "Documento subido exitosamente", "success": "Documento subido exitosamente",
"error": "Error al subir el documento", "error": "Error al subir el documento",
"replace": "Reemplazar" "replace": "Reemplazar",
"file_not_found": "Archivo no encontrado."
} }
}, },
"staff_certificates": { "staff_certificates": {
@@ -1093,7 +1122,9 @@
"select_date": "Seleccionar fecha", "select_date": "Seleccionar fecha",
"upload_file": "Subir Archivo", "upload_file": "Subir Archivo",
"drag_drop": "Arrastra y suelta o haz clic para subir", "drag_drop": "Arrastra y suelta o haz clic para subir",
"supported_formats": "PDF, JPG, PNG hasta 10MB", "supported_formats": "PDF hasta 10MB",
"name_hint": "ej. Permiso de Manipulación de Alimentos",
"issuer_hint": "ej. Departamento de Salud",
"cancel": "Cancelar", "cancel": "Cancelar",
"save": "Guardar Certificado", "save": "Guardar Certificado",
"success_snackbar": "Certificado subido exitosamente y pendiente de verificaci\u00f3n" "success_snackbar": "Certificado subido exitosamente y pendiente de verificaci\u00f3n"
@@ -1125,6 +1156,22 @@
"select_required": "\u2713 Seleccionar todos los art\u00edculos requeridos", "select_required": "\u2713 Seleccionar todos los art\u00edculos requeridos",
"upload_required": "\u2713 Subir fotos de art\u00edculos requeridos", "upload_required": "\u2713 Subir fotos de art\u00edculos requeridos",
"accept_attestation": "\u2713 Aceptar certificaci\u00f3n" "accept_attestation": "\u2713 Aceptar certificaci\u00f3n"
},
"upload_file_types_banner": "Solo se aceptan archivos JPEG, JPG y PNG. Tamaño máximo del archivo: 10MB.",
"capture": {
"attest_please": "Por favor, certifica que posees este artículo.",
"could_not_access_media": "No se pudo acceder a la cámara o galería. Por favor, inténtalo de nuevo.",
"attire_submitted": "Imagen de vestimenta enviada para verificación",
"file_size_exceeds": "El tamaño del archivo supera 10MB. Tamaño máximo: 10MB.",
"pending_verification": "Verificación Pendiente",
"not_uploaded": "No Subido",
"your_uploaded_photo": "Tu Foto Subida",
"reference_example": "Ejemplo de Referencia",
"review_attire_item": "Revisar el artículo de vestimenta",
"example_upload_hint": "Ejemplo del artículo que debes subir.",
"no_items_filter": "No se encontraron artículos para este filtro.",
"approved": "Aprobado",
"rejected": "Rechazado"
} }
}, },
"staff_shifts": { "staff_shifts": {
@@ -1209,7 +1256,21 @@
}, },
"applying_dialog": { "applying_dialog": {
"title": "Solicitando" "title": "Solicitando"
} },
"eligibility_requirements": "Requisitos de Elegibilidad"
},
"my_shift_card": {
"submit_for_approval": "Enviar para Aprobación",
"timesheet_submitted": "Hoja de tiempo enviada para aprobación del cliente",
"checked_in": "Registrado",
"submitted": "ENVIADO",
"ready_to_submit": "LISTO PARA ENVIAR"
},
"shift_location": {
"could_not_open_maps": "No se pudo abrir mapas"
},
"history_tab": {
"subtitle": "Los turnos completados aparecen aquí"
}, },
"card": { "card": {
"just_now": "Reci\u00e9n", "just_now": "Reci\u00e9n",
@@ -1218,6 +1279,7 @@
"decline_shift": "Rechazar turno" "decline_shift": "Rechazar turno"
}, },
"my_shifts_tab": { "my_shifts_tab": {
"swap_coming_soon": "¡Función de intercambio próximamente!",
"confirm_dialog": { "confirm_dialog": {
"title": "Aceptar Turno", "title": "Aceptar Turno",
"message": "\u00bfEst\u00e1s seguro de que quieres aceptar este turno?", "message": "\u00bfEst\u00e1s seguro de que quieres aceptar este turno?",
@@ -1247,6 +1309,9 @@
} }
}, },
"find_shifts": { "find_shifts": {
"incomplete_profile_banner_title": "Tu cuenta aún no está completa.",
"incomplete_profile_banner_message": "No podrás solicitar turnos hasta que tu cuenta esté completamente configurada. Completa tu cuenta ahora para desbloquear las solicitudes de turnos y empezar a recibir oportunidades.",
"incomplete_profile_cta": "Completa tu cuenta ahora",
"search_hint": "Buscar trabajos, ubicaci\u00f3n...", "search_hint": "Buscar trabajos, ubicaci\u00f3n...",
"filter_all": "Todos", "filter_all": "Todos",
"filter_one_day": "Un d\u00eda", "filter_one_day": "Un d\u00eda",
@@ -1584,13 +1649,36 @@
} }
} }
}, },
"client_billing_common": {
"invoices_ready": "Facturas Listas",
"total_amount": "MONTO TOTAL",
"no_invoices_ready": "Aún no hay facturas listas"
},
"client_coverage": { "client_coverage": {
"todays_status": "Estado de Hoy",
"unfilled_today": "Sin Cubrir Hoy",
"running_late": "Llegando Tarde",
"checked_in": "Registrado",
"todays_cost": "Costo de Hoy",
"no_shifts_day": "No hay turnos programados para este día",
"no_workers_assigned": "Aún no hay trabajadores asignados",
"worker_row": { "worker_row": {
"verify": "Verificar", "verify": "Verificar",
"verified_message": "Vestimenta del trabajador verificada para $name" "verified_message": "Vestimenta del trabajador verificada para $name"
} }
}, },
"client_reports_common": {
"export_coming_soon": "Exportar próximamente"
},
"client_authentication_demo": {
"shift_order_placeholder": "Orden de Turno #824",
"worker_name_placeholder": "Alex Thompson"
},
"staff_payments": { "staff_payments": {
"bank_placeholder": "Chase Bank",
"ending_in": "Terminando en 4321",
"this_week": "Esta Semana",
"this_month": "Este Mes",
"early_pay": { "early_pay": {
"title": "Pago Anticipado", "title": "Pago Anticipado",
"available_label": "Disponible para Retirar", "available_label": "Disponible para Retirar",

View File

@@ -1,4 +1,4 @@
import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart' as domain; import 'package:krow_domain/krow_domain.dart' as domain;
@@ -198,11 +198,16 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
return response.data.benefitsDatas return response.data.benefitsDatas
.map( .map(
(dc.ListBenefitsDataByStaffIdBenefitsDatas e) => domain.Benefit( (dc.ListBenefitsDataByStaffIdBenefitsDatas e) {
final total =
e.vendorBenefitPlan.total?.toDouble() ?? 0.0;
final remaining = e.current.toDouble();
return domain.Benefit(
title: e.vendorBenefitPlan.title, title: e.vendorBenefitPlan.title,
entitlementHours: e.vendorBenefitPlan.total?.toDouble() ?? 0, entitlementHours: total,
usedHours: e.current.toDouble(), usedHours: (total - remaining).clamp(0.0, total),
), );
},
) )
.toList(); .toList();
}); });

View File

@@ -73,11 +73,15 @@ class ViewOrdersRepositoryImpl implements IViewOrdersRepository {
final String eventName = final String eventName =
shiftRole.shift.order.eventName ?? shiftRole.shift.title; shiftRole.shift.order.eventName ?? shiftRole.shift.title;
final order = shiftRole.shift.order;
final String? hubManagerId = order.hubManagerId;
final String? hubManagerName = order.hubManager?.user?.fullName;
return domain.OrderItem( return domain.OrderItem(
id: _shiftRoleKey(shiftRole.shiftId, shiftRole.roleId), id: _shiftRoleKey(shiftRole.shiftId, shiftRole.roleId),
orderId: shiftRole.shift.order.id, orderId: order.id,
orderType: domain.OrderType.fromString( orderType: domain.OrderType.fromString(
shiftRole.shift.order.orderType.stringValue, order.orderType.stringValue,
), ),
title: shiftRole.role.name, title: shiftRole.role.name,
eventName: eventName, eventName: eventName,
@@ -94,6 +98,8 @@ class ViewOrdersRepositoryImpl implements IViewOrdersRepository {
hours: hours, hours: hours,
totalValue: totalValue, totalValue: totalValue,
confirmedApps: const <Map<String, dynamic>>[], confirmedApps: const <Map<String, dynamic>>[],
hubManagerId: hubManagerId,
hubManagerName: hubManagerName,
); );
}).toList(); }).toList();
}); });

View File

@@ -193,6 +193,8 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState>
hours: order.hours, hours: order.hours,
totalValue: order.totalValue, totalValue: order.totalValue,
confirmedApps: confirmed, confirmedApps: confirmed,
hubManagerId: order.hubManagerId,
hubManagerName: order.hubManagerName,
); );
}).toList(); }).toList();
} }

View File

@@ -692,7 +692,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
), ),
const SizedBox(height: UiConstants.space4), const SizedBox(height: UiConstants.space4),
_buildSectionHeader('VENDOR'), _buildSectionHeader(t.client_orders_common.select_vendor),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space3, horizontal: UiConstants.space3,
@@ -742,7 +742,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
), ),
const SizedBox(height: UiConstants.space4), const SizedBox(height: UiConstants.space4),
_buildSectionHeader('ORDER NAME'), _buildSectionHeader(t.client_orders_common.order_name),
UiTextField( UiTextField(
controller: _orderNameController, controller: _orderNameController,
hintText: t.client_view_orders.order_edit_sheet.order_name_hint, hintText: t.client_view_orders.order_edit_sheet.order_name_hint,
@@ -750,7 +750,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
), ),
const SizedBox(height: UiConstants.space4), const SizedBox(height: UiConstants.space4),
_buildSectionHeader('HUB'), _buildSectionHeader(t.client_orders_common.hub),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space3, horizontal: UiConstants.space3,

View File

@@ -108,6 +108,43 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
} }
} }
/// Returns true if the edit icon should be shown.
/// Hidden for completed orders and for past orders (shift has ended).
bool _canEditOrder(OrderItem order) {
if (order.status == 'COMPLETED') return false;
if (order.date.isEmpty) return true;
try {
final DateTime orderDate = DateTime.parse(order.date);
final String endTime = order.endTime.trim();
final DateTime endDateTime;
if (endTime.isEmpty) {
// No end time: use end of day so orders today remain editable
endDateTime = DateTime(
orderDate.year,
orderDate.month,
orderDate.day,
23,
59,
59,
);
} else {
final List<String> endParts = endTime.split(':');
final int hour = endParts.isNotEmpty ? int.parse(endParts[0]) : 0;
final int minute = endParts.length > 1 ? int.parse(endParts[1]) : 0;
endDateTime = DateTime(
orderDate.year,
orderDate.month,
orderDate.day,
hour,
minute,
);
}
return endDateTime.isAfter(DateTime.now());
} catch (_) {
return true;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final OrderItem order = widget.order; final OrderItem order = widget.order;
@@ -291,13 +328,14 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
// Actions // Actions
Row( Row(
children: <Widget>[ children: <Widget>[
if (_canEditOrder(order))
_buildHeaderIconButton( _buildHeaderIconButton(
icon: UiIcons.edit, icon: UiIcons.edit,
color: UiColors.primary, color: UiColors.primary,
bgColor: UiColors.primary.withValues(alpha: 0.08), bgColor: UiColors.primary.withValues(alpha: 0.08),
onTap: () => _openEditSheet(order: order), onTap: () => _openEditSheet(order: order),
), ),
const SizedBox(width: UiConstants.space2), if (_canEditOrder(order)) const SizedBox(width: UiConstants.space2),
if (order.confirmedApps.isNotEmpty) if (order.confirmedApps.isNotEmpty)
_buildHeaderIconButton( _buildHeaderIconButton(
icon: _expanded icon: _expanded

View File

@@ -1,6 +1,8 @@
class TestPhoneNumbers { class TestPhoneNumbers {
static const List<String> values = <String>[ static const List<String> values = <String>[
'+15145912311', // Test User 1 '+15145912311', // Test User 1
'+15557654321', // Demo / Mariana
'+15555551234', // Test User 2
]; ];
static bool isTestNumber(String phoneNumber) { static bool isTestNumber(String phoneNumber) {

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';
@@ -42,7 +43,7 @@ class LocationMapPlaceholder extends StatelessWidget {
color: UiColors.iconSecondary, color: UiColors.iconSecondary,
), ),
const SizedBox(height: UiConstants.space2), const SizedBox(height: UiConstants.space2),
Text('Map View (GPS)', style: UiTypography.body2r.textSecondary), Text(context.t.staff.clock_in.map_view_gps, style: UiTypography.body2r.textSecondary),
], ],
), ),
), ),

View File

@@ -123,10 +123,12 @@ class HomeRepositoryImpl
return response.data.benefitsDatas.map((data) { return response.data.benefitsDatas.map((data) {
final plan = data.vendorBenefitPlan; final plan = data.vendorBenefitPlan;
final total = plan.total?.toDouble() ?? 0.0;
final remaining = data.current.toDouble();
return Benefit( return Benefit(
title: plan.title, title: plan.title,
entitlementHours: plan.total?.toDouble() ?? 0.0, entitlementHours: total,
usedHours: data.current.toDouble(), usedHours: (total - remaining).clamp(0.0, total),
); );
}).toList(); }).toList();
}); });

View File

@@ -46,7 +46,7 @@ class BenefitsOverviewPage extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.all(UiConstants.space6), padding: const EdgeInsets.all(UiConstants.space6),
child: Text( child: Text(
t.staff.home.benefits.overview.subtitle, t.staff.home.benefits.overview.empty_state,
style: UiTypography.body1r.textSecondary, style: UiTypography.body1r.textSecondary,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -142,22 +142,18 @@ class BenefitsOverviewPage extends StatelessWidget {
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
benefit.title, benefit.title,
style: UiTypography.body1b.textPrimary, style: UiTypography.body1b.textPrimary,
), ),
const Icon(UiIcons.info, size: 18, color: Color(0xFFE2E8F0)),
],
),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
_getSubtitle(benefit.title), _getSubtitle(benefit.title),
style: UiTypography.footnote2r.textSecondary, style: UiTypography.footnote2r.textSecondary,
), ),
const SizedBox(height: UiConstants.space4),
_buildStatsRow(),
], ],
), ),
), ),
@@ -231,6 +227,56 @@ class BenefitsOverviewPage extends StatelessWidget {
); );
} }
Widget _buildStatsRow() {
final i18n = t.staff.home.benefits.overview;
return Row(
children: [
_buildStatChip(
i18n.entitlement,
'${benefit.entitlementHours.toInt()}',
),
const SizedBox(width: 8),
_buildStatChip(
i18n.used,
'${benefit.usedHours.toInt()}',
),
const SizedBox(width: 8),
_buildStatChip(
i18n.remaining,
'${benefit.remainingHours.toInt()}',
),
],
);
}
Widget _buildStatChip(String label, String value) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFF1F5F9),
borderRadius: BorderRadius.circular(6),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: UiTypography.footnote2r.textTertiary.copyWith(
fontSize: 10,
),
),
Text(
'$value ${t.staff.home.benefits.overview.hours}',
style: UiTypography.footnote2b.textPrimary.copyWith(
fontSize: 12,
),
),
],
),
);
}
String _getSubtitle(String title) { String _getSubtitle(String title) {
final i18n = t.staff.home.benefits.overview; final i18n = t.staff.home.benefits.overview;
if (title.toLowerCase().contains('sick')) { if (title.toLowerCase().contains('sick')) {

View File

@@ -1,3 +1,5 @@
import 'dart:io';
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';
@@ -55,18 +57,44 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
super.dispose(); super.dispose();
} }
static const int _kMaxFileSizeBytes = 10 * 1024 * 1024;
Future<void> _pickFile() async { Future<void> _pickFile() async {
final String? path = await _filePicker.pickFile( final String? path = await _filePicker.pickFile(
allowedExtensions: <String>['pdf', 'jpg', 'png'], allowedExtensions: <String>['pdf'],
); );
if (path != null) { if (path != null) {
final String? error = _validatePdfFile(context, path);
if (error != null && mounted) {
UiSnackbar.show(
context,
message: error,
type: UiSnackbarType.error,
margin: const EdgeInsets.all(UiConstants.space4),
);
return;
}
setState(() { setState(() {
_selectedFilePath = path; _selectedFilePath = path;
}); });
} }
} }
String? _validatePdfFile(BuildContext context, String path) {
final File file = File(path);
if (!file.existsSync()) return context.t.common.file_not_found;
final String ext = path.split('.').last.toLowerCase();
if (ext != 'pdf') {
return context.t.staff_documents.upload.pdf_banner;
}
final int size = file.lengthSync();
if (size > _kMaxFileSizeBytes) {
return context.t.staff_documents.upload.pdf_banner;
}
return null;
}
Future<void> _selectDate() async { Future<void> _selectDate() async {
final DateTime? picked = await showDatePicker( final DateTime? picked = await showDatePicker(
context: context, context: context,
@@ -117,12 +145,8 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( _PdfFileTypesBanner(
t message: t.staff_documents.upload.pdf_banner,
.staff_documents
.upload
.instructions, // Reusing instructions logic
style: UiTypography.body1m.textPrimary,
), ),
const SizedBox(height: UiConstants.space6), const SizedBox(height: UiConstants.space6),
@@ -135,7 +159,7 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
TextField( TextField(
controller: _nameController, controller: _nameController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: "e.g. Food Handler Permit", hintText: t.staff_certificates.upload_modal.name_hint,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: UiConstants.radiusLg, borderRadius: UiConstants.radiusLg,
), ),
@@ -193,7 +217,7 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
TextField( TextField(
controller: _issuerController, controller: _issuerController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: "e.g. Department of Health", hintText: t.staff_certificates.upload_modal.issuer_hint,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: UiConstants.radiusLg, borderRadius: UiConstants.radiusLg,
), ),
@@ -247,7 +271,20 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
(_selectedFilePath != null && (_selectedFilePath != null &&
state.isAttested && state.isAttested &&
_nameController.text.isNotEmpty) _nameController.text.isNotEmpty)
? () => ? () {
final String? err =
_validatePdfFile(context, _selectedFilePath!);
if (err != null) {
UiSnackbar.show(
context,
message: err,
type: UiSnackbarType.error,
margin: const EdgeInsets.all(
UiConstants.space4,
),
);
return;
}
BlocProvider.of<CertificateUploadCubit>( BlocProvider.of<CertificateUploadCubit>(
context, context,
).uploadCertificate( ).uploadCertificate(
@@ -259,7 +296,8 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
issuer: _issuerController.text, issuer: _issuerController.text,
certificateNumber: _numberController.text, certificateNumber: _numberController.text,
), ),
) );
}
: null, : null,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: UiColors.primary, backgroundColor: UiColors.primary,
@@ -291,6 +329,42 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
} }
} }
/// Banner displaying accepted file types and size limit for PDF upload.
class _PdfFileTypesBanner extends StatelessWidget {
const _PdfFileTypesBanner({required this.message});
final String message;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: UiConstants.space3,
),
decoration: BoxDecoration(
color: UiColors.tagActive,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(color: UiColors.primary.withValues(alpha: 0.3)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Icon(UiIcons.info, size: 20, color: UiColors.primary),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Text(
message,
style: UiTypography.body2r.textSecondary,
),
),
],
),
);
}
}
class _FileSelector extends StatelessWidget { class _FileSelector extends StatelessWidget {
const _FileSelector({this.selectedFilePath, required this.onTap}); const _FileSelector({this.selectedFilePath, required this.onTap});

View File

@@ -1,3 +1,5 @@
import 'dart:io';
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';
@@ -37,18 +39,44 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
String? _selectedFilePath; String? _selectedFilePath;
final FilePickerService _filePicker = Modular.get<FilePickerService>(); final FilePickerService _filePicker = Modular.get<FilePickerService>();
static const int _kMaxFileSizeBytes = 10 * 1024 * 1024;
Future<void> _pickFile() async { Future<void> _pickFile() async {
final String? path = await _filePicker.pickFile( final String? path = await _filePicker.pickFile(
allowedExtensions: <String>['pdf'], allowedExtensions: <String>['pdf'],
); );
if (path != null) { if (path != null) {
final String? error = _validatePdfFile(context, path);
if (error != null && mounted) {
UiSnackbar.show(
context,
message: error,
type: UiSnackbarType.error,
margin: const EdgeInsets.all(UiConstants.space4),
);
return;
}
setState(() { setState(() {
_selectedFilePath = path; _selectedFilePath = path;
}); });
} }
} }
String? _validatePdfFile(BuildContext context, String path) {
final File file = File(path);
if (!file.existsSync()) return context.t.common.file_not_found;
final String ext = path.split('.').last.toLowerCase();
if (ext != 'pdf') {
return context.t.staff_documents.upload.pdf_banner;
}
final int size = file.lengthSync();
if (size > _kMaxFileSizeBytes) {
return context.t.staff_documents.upload.pdf_banner;
}
return null;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<DocumentUploadCubit>( return BlocProvider<DocumentUploadCubit>(
@@ -82,9 +110,8 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( _PdfFileTypesBanner(
t.staff_documents.upload.instructions, message: t.staff_documents.upload.pdf_banner,
style: UiTypography.body1m.textPrimary,
), ),
const SizedBox(height: UiConstants.space6), const SizedBox(height: UiConstants.space6),
DocumentFileSelector( DocumentFileSelector(
@@ -116,9 +143,22 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
isUploading: isUploading:
state.status == DocumentUploadStatus.uploading, state.status == DocumentUploadStatus.uploading,
canSubmit: _selectedFilePath != null && state.isAttested, canSubmit: _selectedFilePath != null && state.isAttested,
onSubmit: () => BlocProvider.of<DocumentUploadCubit>( onSubmit: () {
final String? err =
_validatePdfFile(context, _selectedFilePath!);
if (err != null) {
UiSnackbar.show(
context, context,
).uploadDocument(widget.document.id, _selectedFilePath!), message: err,
type: UiSnackbarType.error,
margin: const EdgeInsets.all(UiConstants.space4),
);
return;
}
BlocProvider.of<DocumentUploadCubit>(
context,
).uploadDocument(widget.document.id, _selectedFilePath!);
},
), ),
], ],
), ),
@@ -130,3 +170,39 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
); );
} }
} }
/// Banner displaying accepted file types and size limit for PDF upload.
class _PdfFileTypesBanner extends StatelessWidget {
const _PdfFileTypesBanner({required this.message});
final String message;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: UiConstants.space3,
),
decoration: BoxDecoration(
color: UiColors.tagActive,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(color: UiColors.primary.withValues(alpha: 0.3)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Icon(UiIcons.info, size: 20, color: UiColors.primary),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Text(
message,
style: UiTypography.body2r.textSecondary,
),
),
],
),
);
}
}

View File

@@ -45,7 +45,7 @@ class DocumentsPage extends StatelessWidget {
: t.staff_documents.list.error( : t.staff_documents.list.error(
message: state.errorMessage!, message: state.errorMessage!,
)) ))
: t.staff_documents.list.error(message: 'Unknown'), : t.staff_documents.list.error(message: t.staff_documents.list.unknown),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: UiTypography.body1m.copyWith( style: UiTypography.body1m.copyWith(
color: UiColors.textSecondary, color: UiColors.textSecondary,

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:core_localization/core_localization.dart'; 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';
@@ -31,6 +33,12 @@ class AttireCapturePage extends StatefulWidget {
State<AttireCapturePage> createState() => _AttireCapturePageState(); State<AttireCapturePage> createState() => _AttireCapturePageState();
} }
/// Maximum file size for attire upload (10MB).
const int _kMaxFileSizeBytes = 10 * 1024 * 1024;
/// Allowed file extensions for attire upload.
const Set<String> _kAllowedExtensions = <String>{'jpeg', 'jpg', 'png'};
class _AttireCapturePageState extends State<AttireCapturePage> { class _AttireCapturePageState extends State<AttireCapturePage> {
String? _selectedLocalPath; String? _selectedLocalPath;
@@ -57,6 +65,16 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
final GalleryService service = Modular.get<GalleryService>(); final GalleryService service = Modular.get<GalleryService>();
final String? path = await service.pickImage(); final String? path = await service.pickImage();
if (path != null && context.mounted) { if (path != null && context.mounted) {
final String? error = _validateFile(path);
if (error != null) {
UiSnackbar.show(
context,
message: error,
type: UiSnackbarType.error,
margin: const EdgeInsets.all(UiConstants.space4),
);
return;
}
setState(() { setState(() {
_selectedLocalPath = path; _selectedLocalPath = path;
}); });
@@ -84,6 +102,16 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
final CameraService service = Modular.get<CameraService>(); final CameraService service = Modular.get<CameraService>();
final String? path = await service.takePhoto(); final String? path = await service.takePhoto();
if (path != null && context.mounted) { if (path != null && context.mounted) {
final String? error = _validateFile(path);
if (error != null) {
UiSnackbar.show(
context,
message: error,
type: UiSnackbarType.error,
margin: const EdgeInsets.all(UiConstants.space4),
);
return;
}
setState(() { setState(() {
_selectedLocalPath = path; _selectedLocalPath = path;
}); });
@@ -105,7 +133,7 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
leading: const Icon(Icons.photo_library), leading: const Icon(Icons.photo_library),
title: const Text('Gallery'), title: Text(t.common.gallery),
onTap: () { onTap: () {
Modular.to.pop(); Modular.to.pop();
_onGallery(context); _onGallery(context);
@@ -113,7 +141,7 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
), ),
ListTile( ListTile(
leading: const Icon(Icons.camera_alt), leading: const Icon(Icons.camera_alt),
title: const Text('Camera'), title: Text(t.common.camera),
onTap: () { onTap: () {
Modular.to.pop(); Modular.to.pop();
_onCamera(context); _onCamera(context);
@@ -128,17 +156,33 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
void _showAttestationWarning(BuildContext context) { void _showAttestationWarning(BuildContext context) {
UiSnackbar.show( UiSnackbar.show(
context, context,
message: 'Please attest that you own this item.', message: t.staff_profile_attire.capture.attest_please,
type: UiSnackbarType.error, type: UiSnackbarType.error,
margin: const EdgeInsets.all(UiConstants.space4), margin: const EdgeInsets.all(UiConstants.space4),
); );
} }
/// Validates file format (JPEG, JPG, PNG) and size (max 10MB).
/// Returns an error message if invalid, or null if valid.
String? _validateFile(String path) {
final File file = File(path);
if (!file.existsSync()) return t.common.file_not_found;
final String ext = path.split('.').last.toLowerCase();
if (!_kAllowedExtensions.contains(ext)) {
return t.staff_profile_attire.upload_file_types_banner;
}
final int size = file.lengthSync();
if (size > _kMaxFileSizeBytes) {
return t.staff_profile_attire.capture.file_size_exceeds;
}
return null;
}
void _showError(BuildContext context, String message) { void _showError(BuildContext context, String message) {
debugPrint(message); debugPrint(message);
UiSnackbar.show( UiSnackbar.show(
context, context,
message: 'Could not access camera or gallery. Please try again.', message: t.staff_profile_attire.capture.could_not_access_media,
type: UiSnackbarType.error, type: UiSnackbarType.error,
margin: const EdgeInsets.all(UiConstants.space4), margin: const EdgeInsets.all(UiConstants.space4),
); );
@@ -150,6 +194,17 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
); );
if (_selectedLocalPath == null) return; if (_selectedLocalPath == null) return;
final String? error = _validateFile(_selectedLocalPath!);
if (error != null) {
UiSnackbar.show(
context,
message: error,
type: UiSnackbarType.error,
margin: const EdgeInsets.all(UiConstants.space4),
);
return;
}
await cubit.uploadPhoto(widget.item.id, _selectedLocalPath!); await cubit.uploadPhoto(widget.item.id, _selectedLocalPath!);
if (context.mounted && cubit.state.status == AttireCaptureStatus.success) { if (context.mounted && cubit.state.status == AttireCaptureStatus.success) {
setState(() { setState(() {
@@ -160,10 +215,10 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
String _getStatusText(bool hasUploadedPhoto) { String _getStatusText(bool hasUploadedPhoto) {
return switch (widget.item.verificationStatus) { return switch (widget.item.verificationStatus) {
AttireVerificationStatus.approved => 'Approved', AttireVerificationStatus.approved => t.staff_profile_attire.capture.approved,
AttireVerificationStatus.rejected => 'Rejected', AttireVerificationStatus.rejected => t.staff_profile_attire.capture.rejected,
AttireVerificationStatus.pending => 'Pending Verification', AttireVerificationStatus.pending => t.staff_profile_attire.capture.pending_verification,
_ => hasUploadedPhoto ? 'Pending Verification' : 'Not Uploaded', _ => hasUploadedPhoto ? t.staff_profile_attire.capture.pending_verification : t.staff_profile_attire.capture.not_uploaded,
}; };
} }
@@ -207,7 +262,7 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
if (state.status == AttireCaptureStatus.success) { if (state.status == AttireCaptureStatus.success) {
UiSnackbar.show( UiSnackbar.show(
context, context,
message: 'Attire image submitted for verification', message: t.staff_profile_attire.capture.attire_submitted,
type: UiSnackbarType.success, type: UiSnackbarType.success,
); );
Modular.to.toAttire(); Modular.to.toAttire();
@@ -225,6 +280,10 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
padding: const EdgeInsets.all(UiConstants.space5), padding: const EdgeInsets.all(UiConstants.space5),
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
_FileTypesBanner(
message: t.staff_profile_attire.upload_file_types_banner,
),
const SizedBox(height: UiConstants.space4),
ImagePreviewSection( ImagePreviewSection(
selectedLocalPath: _selectedLocalPath, selectedLocalPath: _selectedLocalPath,
currentPhotoUrl: currentPhotoUrl, currentPhotoUrl: currentPhotoUrl,
@@ -268,3 +327,43 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
); );
} }
} }
/// Banner displaying accepted file types and size limit for attire upload.
class _FileTypesBanner extends StatelessWidget {
const _FileTypesBanner({required this.message});
final String message;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: UiConstants.space3,
),
decoration: BoxDecoration(
color: UiColors.tagActive,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(color: UiColors.primary.withValues(alpha: 0.3)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Icon(
UiIcons.info,
size: 20,
color: UiColors.primary,
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Text(
message,
style: UiTypography.body2r.textSecondary,
),
),
],
),
);
}
}

View File

@@ -78,7 +78,7 @@ class AttirePage extends StatelessWidget {
), ),
const SizedBox(height: UiConstants.space4), const SizedBox(height: UiConstants.space4),
Text( Text(
'No items found for this filter.', context.t.staff_profile_attire.capture.no_items_filter,
style: UiTypography.body1m.textSecondary, style: UiTypography.body1m.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';
@@ -28,7 +29,7 @@ class ImagePreviewSection extends StatelessWidget {
return Column( return Column(
children: <Widget>[ children: <Widget>[
Text( Text(
'Review the attire item', context.t.staff_profile_attire.capture.review_attire_item,
style: UiTypography.body1b.textPrimary, style: UiTypography.body1b.textPrimary,
), ),
const SizedBox(height: UiConstants.space2), const SizedBox(height: UiConstants.space2),
@@ -42,7 +43,7 @@ class ImagePreviewSection extends StatelessWidget {
if (currentPhotoUrl != null) { if (currentPhotoUrl != null) {
return Column( return Column(
children: <Widget>[ children: <Widget>[
Text('Your Uploaded Photo', style: UiTypography.body1b.textPrimary), Text(context.t.staff_profile_attire.capture.your_uploaded_photo, style: UiTypography.body1b.textPrimary),
const SizedBox(height: UiConstants.space2), const SizedBox(height: UiConstants.space2),
AttireImagePreview(imageUrl: currentPhotoUrl), AttireImagePreview(imageUrl: currentPhotoUrl),
const SizedBox(height: UiConstants.space4), const SizedBox(height: UiConstants.space4),
@@ -56,7 +57,7 @@ class ImagePreviewSection extends StatelessWidget {
AttireImagePreview(imageUrl: referenceImageUrl), AttireImagePreview(imageUrl: referenceImageUrl),
const SizedBox(height: UiConstants.space4), const SizedBox(height: UiConstants.space4),
Text( Text(
'Example of the item that you need to upload.', context.t.staff_profile_attire.capture.example_upload_hint,
style: UiTypography.body1b.textSecondary, style: UiTypography.body1b.textSecondary,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -77,7 +78,7 @@ class ReferenceExample extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: <Widget>[ children: <Widget>[
Text('Reference Example', style: UiTypography.body2b.textSecondary), Text(context.t.staff_profile_attire.capture.reference_example, style: UiTypography.body2b.textSecondary),
const SizedBox(height: UiConstants.space1), const SizedBox(height: UiConstants.space1),
Center( Center(
child: ClipRRect( child: ClipRRect(

View File

@@ -317,7 +317,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
children: [ children: [
const Icon(UiIcons.warning, color: UiColors.error), const Icon(UiIcons.warning, color: UiColors.error),
const SizedBox(width: UiConstants.space2), const SizedBox(width: UiConstants.space2),
Expanded(child: Text("Eligibility Requirements")), Expanded(child: Text(context.t.staff_shifts.shift_details.eligibility_requirements)),
], ],
), ),
content: Text( content: Text(

View File

@@ -252,7 +252,10 @@ class _ShiftsPageState extends State<ShiftsPage> {
if (availableLoading) { if (availableLoading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
return FindShiftsTab(availableJobs: availableJobs); return FindShiftsTab(
availableJobs: availableJobs,
profileComplete: state.profileComplete ?? true,
);
case ShiftTabType.history: case ShiftTabType.history:
if (historyLoading) { if (historyLoading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());

View File

@@ -117,7 +117,7 @@ class _MyShiftCardState extends State<MyShiftCard> {
statusColor = UiColors.textLink; statusColor = UiColors.textLink;
statusBg = UiColors.primary; statusBg = UiColors.primary;
} else if (status == 'checked_in') { } else if (status == 'checked_in') {
statusText = 'Checked in'; statusText = context.t.staff_shifts.my_shift_card.checked_in;
statusColor = UiColors.textSuccess; statusColor = UiColors.textSuccess;
statusBg = UiColors.iconSuccess; statusBg = UiColors.iconSuccess;
} else if (status == 'pending' || status == 'open') { } else if (status == 'pending' || status == 'open') {
@@ -487,20 +487,22 @@ class _MyShiftCardState extends State<MyShiftCard> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
_isSubmitted ? 'SUBMITTED' : 'READY TO SUBMIT', _isSubmitted
? context.t.staff_shifts.my_shift_card.submitted
: context.t.staff_shifts.my_shift_card.ready_to_submit,
style: UiTypography.footnote2b.copyWith( style: UiTypography.footnote2b.copyWith(
color: _isSubmitted ? UiColors.textSuccess : UiColors.textSecondary, color: _isSubmitted ? UiColors.textSuccess : UiColors.textSecondary,
), ),
), ),
if (!_isSubmitted) if (!_isSubmitted)
UiButton.secondary( UiButton.secondary(
text: 'Submit for Approval', text: context.t.staff_shifts.my_shift_card.submit_for_approval,
size: UiButtonSize.small, size: UiButtonSize.small,
onPressed: () { onPressed: () {
setState(() => _isSubmitted = true); setState(() => _isSubmitted = true);
UiSnackbar.show( UiSnackbar.show(
context, context,
message: 'Timesheet submitted for client approval', message: context.t.staff_shifts.my_shift_card.timesheet_submitted,
type: UiSnackbarType.success, type: UiSnackbarType.success,
); );
}, },

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:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
@@ -124,7 +125,7 @@ class ShiftLocationSection extends StatelessWidget {
if (context.mounted) { if (context.mounted) {
UiSnackbar.show( UiSnackbar.show(
context, context,
message: 'Could not open maps', message: context.t.staff_shifts.shift_location.could_not_open_maps,
type: UiSnackbarType.error, type: UiSnackbarType.error,
); );
} }

View File

@@ -1,5 +1,6 @@
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_modular/flutter_modular.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import 'package:core_localization/core_localization.dart'; import 'package:core_localization/core_localization.dart';
@@ -12,7 +13,15 @@ import 'package:geolocator/geolocator.dart';
class FindShiftsTab extends StatefulWidget { class FindShiftsTab extends StatefulWidget {
final List<Shift> availableJobs; final List<Shift> availableJobs;
const FindShiftsTab({super.key, required this.availableJobs}); /// Whether the worker's profile is complete. When false, shows incomplete
/// profile banner and disables apply actions.
final bool profileComplete;
const FindShiftsTab({
super.key,
required this.availableJobs,
this.profileComplete = true,
});
@override @override
State<FindShiftsTab> createState() => _FindShiftsTabState(); State<FindShiftsTab> createState() => _FindShiftsTabState();
@@ -305,6 +314,15 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
return Column( return Column(
children: [ children: [
// Incomplete profile banner
if (!widget.profileComplete) ...[
_IncompleteProfileBanner(
title: context.t.staff_shifts.find_shifts.incomplete_profile_banner_title,
message: context.t.staff_shifts.find_shifts.incomplete_profile_banner_message,
ctaText: context.t.staff_shifts.find_shifts.incomplete_profile_cta,
onCtaPressed: () => Modular.to.toProfile(),
),
],
// Search and Filters // Search and Filters
Container( Container(
color: UiColors.white, color: UiColors.white,
@@ -440,7 +458,8 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
), ),
child: MyShiftCard( child: MyShiftCard(
shift: shift, shift: shift,
onAccept: () { onAccept: widget.profileComplete
? () {
context.read<ShiftsBloc>().add( context.read<ShiftsBloc>().add(
AcceptShiftEvent(shift.id), AcceptShiftEvent(shift.id),
); );
@@ -453,7 +472,8 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
.application_submitted, .application_submitted,
type: UiSnackbarType.success, type: UiSnackbarType.success,
); );
}, }
: null,
), ),
), ),
), ),
@@ -466,3 +486,54 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
); );
} }
} }
/// Banner shown when the worker's profile is incomplete.
class _IncompleteProfileBanner extends StatelessWidget {
const _IncompleteProfileBanner({
required this.title,
required this.message,
required this.ctaText,
required this.onCtaPressed,
});
final String title;
final String message;
final String ctaText;
final VoidCallback onCtaPressed;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
margin: const EdgeInsets.all(UiConstants.space5),
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color: UiColors.tagPending,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(
color: UiColors.textWarning.withValues(alpha: 0.5),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
style: UiTypography.body2b.textPrimary,
),
const SizedBox(height: UiConstants.space2),
Text(
message,
style: UiTypography.body3r.textSecondary,
),
const SizedBox(height: UiConstants.space4),
UiButton.primary(
text: ctaText,
onPressed: onCtaPressed,
size: UiButtonSize.small,
),
],
),
);
}
}

View File

@@ -104,28 +104,28 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
void _confirmShift(String id) { void _confirmShift(String id) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (ctx) => AlertDialog(
title: Text(t.staff_shifts.my_shifts_tab.confirm_dialog.title), title: Text(context.t.staff_shifts.my_shifts_tab.confirm_dialog.title),
content: Text(t.staff_shifts.my_shifts_tab.confirm_dialog.message), content: Text(context.t.staff_shifts.my_shifts_tab.confirm_dialog.message),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(ctx).pop(),
child: Text(t.common.cancel), child: Text(context.t.common.cancel),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(ctx).pop();
context.read<ShiftsBloc>().add(AcceptShiftEvent(id)); context.read<ShiftsBloc>().add(AcceptShiftEvent(id));
UiSnackbar.show( UiSnackbar.show(
context, context,
message: t.staff_shifts.my_shifts_tab.confirm_dialog.success, message: context.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: Text(t.staff_shifts.shift_details.accept_shift), child: Text(context.t.staff_shifts.shift_details.accept_shift),
), ),
], ],
), ),
@@ -135,30 +135,30 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
void _declineShift(String id) { void _declineShift(String id) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (ctx) => AlertDialog(
title: Text(t.staff_shifts.my_shifts_tab.decline_dialog.title), title: Text(context.t.staff_shifts.my_shifts_tab.decline_dialog.title),
content: Text( content: Text(
t.staff_shifts.my_shifts_tab.decline_dialog.message, context.t.staff_shifts.my_shifts_tab.decline_dialog.message,
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(ctx).pop(),
child: Text(t.common.cancel), child: Text(context.t.common.cancel),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(ctx).pop();
context.read<ShiftsBloc>().add(DeclineShiftEvent(id)); context.read<ShiftsBloc>().add(DeclineShiftEvent(id));
UiSnackbar.show( UiSnackbar.show(
context, context,
message: t.staff_shifts.my_shifts_tab.decline_dialog.success, message: context.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: Text(t.staff_shifts.shift_details.decline), child: Text(context.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 t.staff_shifts.my_shifts_tab.date.today; if (_isSameDay(date, now)) return context.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 t.staff_shifts.my_shifts_tab.date.tomorrow; if (_isSameDay(date, tomorrow)) return context.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(
t.staff_shifts.my_shifts_tab.sections.awaiting, context.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(t.staff_shifts.my_shifts_tab.sections.cancelled, UiColors.textSecondary), _buildSectionHeader(context.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(t.staff_shifts.my_shifts_tab.sections.confirmed, UiColors.textSecondary), _buildSectionHeader(context.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),
@@ -388,7 +388,7 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
onRequestSwap: () { onRequestSwap: () {
UiSnackbar.show( UiSnackbar.show(
context, context,
message: "Swap functionality coming soon!", // Todo: Localization message: context.t.staff_shifts.my_shifts_tab.swap_coming_soon,
type: UiSnackbarType.message, type: UiSnackbarType.message,
); );
}, },
@@ -402,8 +402,8 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
widget.cancelledShifts.isEmpty) widget.cancelledShifts.isEmpty)
EmptyStateView( EmptyStateView(
icon: UiIcons.calendar, icon: UiIcons.calendar,
title: t.staff_shifts.my_shifts_tab.empty.title, title: context.t.staff_shifts.my_shifts_tab.empty.title,
subtitle: t.staff_shifts.my_shifts_tab.empty.subtitle, subtitle: context.t.staff_shifts.my_shifts_tab.empty.subtitle,
), ),
const SizedBox(height: UiConstants.space32), const SizedBox(height: UiConstants.space32),
@@ -472,13 +472,13 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Text(
t.staff_shifts.my_shifts_tab.card.cancelled, context.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(
t.staff_shifts.my_shifts_tab.card.compensation, context.t.staff_shifts.my_shifts_tab.card.compensation,
style: UiTypography.footnote2m.textSuccess, style: UiTypography.footnote2m.textSuccess,
), ),
], ],

View File

@@ -356,7 +356,13 @@ query listShiftRolesByBusinessAndDateRange(
locationAddress locationAddress
title title
status status
order { id eventName orderType } order {
id
eventName
orderType
hubManagerId
hubManager { id user { fullName } }
}
} }
} }
} }

View File

@@ -19,6 +19,25 @@ mutation seedAll @transaction {
userRole: "STAFF" userRole: "STAFF"
} }
) )
user_3: user_insert(
data: {
id: "367IkYcvnyfMWVvBp2EV2YHKqDR2"
email: "testclient@gmail.com"
fullName: "Test Client"
role: USER
userRole: "BUSINESS"
}
)
user_4: user_insert(
data: {
id: "testStaff12345678901234567890"
email: "teststaff@example.com"
phone: "+15555551234"
fullName: "Test Staff"
role: USER
userRole: "STAFF"
}
)
# Business # Business
business_1: business_insert( business_1: business_insert(
@@ -43,6 +62,28 @@ mutation seedAll @transaction {
status: ACTIVE status: ACTIVE
} }
) )
business_2: business_insert(
data: {
id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
businessName: "Test Client Business"
userId: "367IkYcvnyfMWVvBp2EV2YHKqDR2"
contactName: "Test Client"
email: "testclient@gmail.com"
phone: "+1-555-000-0000"
address: "123 Main Street, Los Angeles, CA, USA"
city: "Los Angeles"
state: "CA"
street: "Main Street"
country: "US"
placeId: "ChIJN1t_tDeuEmsRUsoyG83frY4"
latitude: 34.052235
longitude: -118.243683
area: SOUTHERN_CALIFORNIA
sector: OTHER
rateGroup: PREMIUM
status: ACTIVE
}
)
# Team # Team
team_1: team_insert( team_1: team_insert(
@@ -55,6 +96,16 @@ mutation seedAll @transaction {
totalHubs: 3 totalHubs: 3
} }
) )
team_2: team_insert(
data: {
id: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
teamName: "Test Client Team"
ownerId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
ownerName: "Test Client"
ownerRole: "ADMIN"
totalHubs: 1
}
)
# Team Hubs # Team Hubs
team_hub_1: teamHub_insert( team_hub_1: teamHub_insert(
@@ -105,6 +156,22 @@ mutation seedAll @transaction {
isActive: true isActive: true
} }
) )
team_hub_4: teamHub_insert(
data: {
id: "d4e5f6a7-b8c9-0123-def0-234567890123"
teamId: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
hubName: "Test Hub"
address: "123 Main Street, Los Angeles, CA, USA"
city: "Los Angeles"
state: "CA"
street: "Main Street"
country: "US"
placeId: "ChIJN1t_tDeuEmsRUsoyG83frY4"
latitude: 34.052235
longitude: -118.243683
isActive: true
}
)
# Vendor # Vendor
vendor_1: vendor_insert( vendor_1: vendor_insert(
@@ -326,6 +393,25 @@ mutation seedAll @transaction {
englishRequired: true englishRequired: true
} }
) )
staff_7: staff_insert(
data: {
id: "e7f8a9b0-c1d2-4e5f-a6b7-c8d9e0f1a2b3"
userId: "testStaff12345678901234567890"
fullName: "Test Staff"
email: "teststaff@example.com"
phone: "+1-555-555-1234"
ownerId: "c3b25c47-0ebd-4402-a9b1-b8a875a7f71a"
addres: "123 Main Street, Los Angeles, CA 90001, USA"
city: "Los Angeles"
state: "CA"
street: "Main Street"
country: "US"
placeId: "ChIJN1t_tDeuEmsRUsoyG83frY4"
latitude: 34.052235
longitude: -118.243683
englishRequired: true
}
)
# Orders (20 total) # Orders (20 total)
order_01: order_insert( order_01: order_insert(