Merge pull request #558 from Oloodi/feature/session-persistence-new
feat: localization, file restriction banners, test credentials, edit …
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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')) {
|
||||||
|
|||||||
@@ -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});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: _buildTabContent(
|
: _buildTabContent(
|
||||||
|
state,
|
||||||
myShifts,
|
myShifts,
|
||||||
pendingAssignments,
|
pendingAssignments,
|
||||||
cancelledShifts,
|
cancelledShifts,
|
||||||
@@ -232,6 +233,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTabContent(
|
Widget _buildTabContent(
|
||||||
|
ShiftsState state,
|
||||||
List<Shift> myShifts,
|
List<Shift> myShifts,
|
||||||
List<Shift> pendingAssignments,
|
List<Shift> pendingAssignments,
|
||||||
List<Shift> cancelledShifts,
|
List<Shift> cancelledShifts,
|
||||||
@@ -252,7 +254,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());
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
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_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.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';
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import '../../blocs/shifts/shifts_bloc.dart';
|
import '../../blocs/shifts/shifts_bloc.dart';
|
||||||
import '../my_shift_card.dart';
|
import '../my_shift_card.dart';
|
||||||
import '../shared/empty_state_view.dart';
|
import '../shared/empty_state_view.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,8 +458,11 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
|||||||
),
|
),
|
||||||
child: MyShiftCard(
|
child: MyShiftCard(
|
||||||
shift: shift,
|
shift: shift,
|
||||||
onAccept: () {
|
onAccept: widget.profileComplete
|
||||||
context.read<ShiftsBloc>().add(
|
? () {
|
||||||
|
BlocProvider.of<ShiftsBloc>(
|
||||||
|
context,
|
||||||
|
).add(
|
||||||
AcceptShiftEvent(shift.id),
|
AcceptShiftEvent(shift.id),
|
||||||
);
|
);
|
||||||
UiSnackbar.show(
|
UiSnackbar.show(
|
||||||
@@ -453,7 +474,8 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
|||||||
.application_submitted,
|
.application_submitted,
|
||||||
type: UiSnackbarType.success,
|
type: UiSnackbarType.success,
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -466,3 +488,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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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 } }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user