#537 (Cost Center)#539 (Hub Manager)

This commit is contained in:
2026-02-25 21:18:51 +05:30
parent af09cd40e7
commit b85a83b446
18 changed files with 285 additions and 79 deletions

View File

@@ -255,6 +255,7 @@
"address_hint": "Full address", "address_hint": "Full address",
"cost_center_label": "Cost Center", "cost_center_label": "Cost Center",
"cost_center_hint": "eg: 1001, 1002", "cost_center_hint": "eg: 1001, 1002",
"cost_centers_empty": "No cost centers available",
"name_required": "Name is required", "name_required": "Name is required",
"address_required": "Address is required", "address_required": "Address is required",
"create_button": "Create Hub" "create_button": "Create Hub"
@@ -268,8 +269,12 @@
"address_hint": "Full address", "address_hint": "Full address",
"cost_center_label": "Cost Center", "cost_center_label": "Cost Center",
"cost_center_hint": "eg: 1001, 1002", "cost_center_hint": "eg: 1001, 1002",
"cost_centers_empty": "No cost centers available",
"name_required": "Name is required",
"save_button": "Save Changes", "save_button": "Save Changes",
"success": "Hub updated successfully!" "success": "Hub updated successfully!",
"created_success": "Hub created successfully",
"updated_success": "Hub updated successfully"
}, },
"hub_details": { "hub_details": {
"title": "Hub Details", "title": "Hub Details",
@@ -279,7 +284,8 @@
"nfc_not_assigned": "Not Assigned", "nfc_not_assigned": "Not Assigned",
"cost_center_label": "Cost Center", "cost_center_label": "Cost Center",
"cost_center_none": "Not Assigned", "cost_center_none": "Not Assigned",
"edit_button": "Edit Hub" "edit_button": "Edit Hub",
"deleted_success": "Hub deleted successfully"
}, },
"nfc_dialog": { "nfc_dialog": {
"title": "Identify NFC Tag", "title": "Identify NFC Tag",
@@ -338,6 +344,8 @@
"hub_manager_label": "Shift Contact", "hub_manager_label": "Shift Contact",
"hub_manager_desc": "On-site manager or supervisor for this shift", "hub_manager_desc": "On-site manager or supervisor for this shift",
"hub_manager_hint": "Select Contact", "hub_manager_hint": "Select Contact",
"hub_manager_empty": "No hub managers available",
"hub_manager_none": "None",
"positions_title": "Positions", "positions_title": "Positions",
"add_position": "Add Position", "add_position": "Add Position",
"position_number": "Position $number", "position_number": "Position $number",
@@ -389,6 +397,41 @@
"active": "Active", "active": "Active",
"completed": "Completed" "completed": "Completed"
}, },
"order_edit_sheet": {
"title": "Edit Your Order",
"vendor_section": "VENDOR",
"location_section": "LOCATION",
"shift_contact_section": "SHIFT CONTACT",
"shift_contact_desc": "On-site manager or supervisor for this shift",
"select_contact": "Select Contact",
"no_hub_managers": "No hub managers available",
"none": "None",
"positions_section": "POSITIONS",
"add_position": "Add Position",
"review_positions": "Review $count Positions",
"order_name_hint": "Order name",
"remove": "Remove",
"select_role_hint": "Select role",
"start_label": "Start",
"end_label": "End",
"workers_label": "Workers",
"different_location": "Use different location for this position",
"different_location_title": "Different Location",
"enter_address_hint": "Enter different address",
"no_break": "No Break",
"positions": "Positions",
"workers": "Workers",
"est_cost": "Est. Cost",
"positions_breakdown": "Positions Breakdown",
"edit_button": "Edit",
"confirm_save": "Confirm & Save",
"position_singular": "Position",
"order_updated_title": "Order Updated!",
"order_updated_message": "Your shift has been updated successfully.",
"back_to_orders": "Back to Orders",
"one_time_order_title": "One-Time Order",
"refine_subtitle": "Refine your staffing needs"
},
"card": { "card": {
"open": "OPEN", "open": "OPEN",
"filled": "FILLED", "filled": "FILLED",

View File

@@ -255,6 +255,7 @@
"address_hint": "Direcci\u00f3n completa", "address_hint": "Direcci\u00f3n completa",
"cost_center_label": "Centro de Costos", "cost_center_label": "Centro de Costos",
"cost_center_hint": "ej: 1001, 1002", "cost_center_hint": "ej: 1001, 1002",
"cost_centers_empty": "No hay centros de costos disponibles",
"name_required": "Nombre es obligatorio", "name_required": "Nombre es obligatorio",
"address_required": "La direcci\u00f3n es obligatoria", "address_required": "La direcci\u00f3n es obligatoria",
"create_button": "Crear Hub" "create_button": "Crear Hub"
@@ -283,8 +284,12 @@
"address_hint": "Ingresar direcci\u00f3n", "address_hint": "Ingresar direcci\u00f3n",
"cost_center_label": "Centro de Costos", "cost_center_label": "Centro de Costos",
"cost_center_hint": "ej: 1001, 1002", "cost_center_hint": "ej: 1001, 1002",
"cost_centers_empty": "No hay centros de costos disponibles",
"name_required": "El nombre es obligatorio",
"save_button": "Guardar Cambios", "save_button": "Guardar Cambios",
"success": "\u00a1Hub actualizado exitosamente!" "success": "\u00a1Hub actualizado exitosamente!",
"created_success": "Hub creado exitosamente",
"updated_success": "Hub actualizado exitosamente"
}, },
"hub_details": { "hub_details": {
"title": "Detalles del Hub", "title": "Detalles del Hub",
@@ -294,7 +299,8 @@
"nfc_label": "Etiqueta NFC", "nfc_label": "Etiqueta NFC",
"nfc_not_assigned": "No asignada", "nfc_not_assigned": "No asignada",
"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"
} }
}, },
"client_create_order": { "client_create_order": {
@@ -338,6 +344,8 @@
"hub_manager_label": "Contacto del Turno", "hub_manager_label": "Contacto del Turno",
"hub_manager_desc": "Gerente o supervisor en el sitio para este turno", "hub_manager_desc": "Gerente o supervisor en el sitio para este turno",
"hub_manager_hint": "Seleccionar Contacto", "hub_manager_hint": "Seleccionar Contacto",
"hub_manager_empty": "No hay contactos de turno disponibles",
"hub_manager_none": "Ninguno",
"positions_title": "Posiciones", "positions_title": "Posiciones",
"add_position": "A\u00f1adir Posici\u00f3n", "add_position": "A\u00f1adir Posici\u00f3n",
"position_number": "Posici\u00f3n $number", "position_number": "Posici\u00f3n $number",
@@ -389,6 +397,41 @@
"active": "Activos", "active": "Activos",
"completed": "Completados" "completed": "Completados"
}, },
"order_edit_sheet": {
"title": "Editar Tu Orden",
"vendor_section": "PROVEEDOR",
"location_section": "UBICACI\u00d3N",
"shift_contact_section": "CONTACTO DEL TURNO",
"shift_contact_desc": "Gerente o supervisor en el sitio para este turno",
"select_contact": "Seleccionar Contacto",
"no_hub_managers": "No hay contactos de turno disponibles",
"none": "Ninguno",
"positions_section": "POSICIONES",
"add_position": "A\u00f1adir Posici\u00f3n",
"review_positions": "Revisar $count Posiciones",
"order_name_hint": "Nombre de la orden",
"remove": "Eliminar",
"select_role_hint": "Seleccionar rol",
"start_label": "Inicio",
"end_label": "Fin",
"workers_label": "Trabajadores",
"different_location": "Usar ubicaci\u00f3n diferente para esta posici\u00f3n",
"different_location_title": "Ubicaci\u00f3n Diferente",
"enter_address_hint": "Ingresar direcci\u00f3n diferente",
"no_break": "Sin Descanso",
"positions": "Posiciones",
"workers": "Trabajadores",
"est_cost": "Costo Est.",
"positions_breakdown": "Desglose de Posiciones",
"edit_button": "Editar",
"confirm_save": "Confirmar y Guardar",
"position_singular": "Posici\u00f3n",
"order_updated_title": "\u00a1Orden Actualizada!",
"order_updated_message": "Tu turno ha sido actualizado exitosamente.",
"back_to_orders": "Volver a \u00d3rdenes",
"one_time_order_title": "Orden \u00danica Vez",
"refine_subtitle": "Ajusta tus necesidades de personal"
},
"card": { "card": {
"open": "ABIERTO", "open": "ABIERTO",
"filled": "LLENO", "filled": "LLENO",

View File

@@ -1,4 +1,4 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs // ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'dart:convert'; import 'dart:convert';
import 'package:firebase_data_connect/src/core/ref.dart'; import 'package:firebase_data_connect/src/core/ref.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@@ -23,7 +23,25 @@ class HubsConnectorRepositoryImpl implements HubsConnectorRepository {
.getTeamHubsByTeamId(teamId: teamId) .getTeamHubsByTeamId(teamId: teamId)
.execute(); .execute();
final QueryResult<
dc.ListTeamHudDepartmentsData,
dc.ListTeamHudDepartmentsVariables
>
deptsResult = await _service.connector.listTeamHudDepartments().execute();
final Map<String, dc.ListTeamHudDepartmentsTeamHudDepartments> hubToDept =
<String, dc.ListTeamHudDepartmentsTeamHudDepartments>{};
for (final dc.ListTeamHudDepartmentsTeamHudDepartments dep
in deptsResult.data.teamHudDepartments) {
if (dep.costCenter != null &&
dep.costCenter!.isNotEmpty &&
!hubToDept.containsKey(dep.teamHubId)) {
hubToDept[dep.teamHubId] = dep;
}
}
return response.data.teamHubs.map((dc.GetTeamHubsByTeamIdTeamHubs h) { return response.data.teamHubs.map((dc.GetTeamHubsByTeamIdTeamHubs h) {
final dc.ListTeamHudDepartmentsTeamHudDepartments? dept =
hubToDept[h.id];
return Hub( return Hub(
id: h.id, id: h.id,
businessId: businessId, businessId: businessId,
@@ -31,7 +49,13 @@ class HubsConnectorRepositoryImpl implements HubsConnectorRepository {
address: h.address, address: h.address,
nfcTagId: null, nfcTagId: null,
status: h.isActive ? HubStatus.active : HubStatus.inactive, status: h.isActive ? HubStatus.active : HubStatus.inactive,
costCenter: null, costCenter: dept != null
? CostCenter(
id: dept.id,
name: dept.name,
code: dept.costCenter ?? dept.name,
)
: null,
); );
}).toList(); }).toList();
}); });
@@ -50,6 +74,7 @@ class HubsConnectorRepositoryImpl implements HubsConnectorRepository {
String? street, String? street,
String? country, String? country,
String? zipCode, String? zipCode,
String? costCenterId,
}) async { }) async {
return _service.run(() async { return _service.run(() async {
final String teamId = await _getOrCreateTeamId(businessId); final String teamId = await _getOrCreateTeamId(businessId);
@@ -73,14 +98,27 @@ class HubsConnectorRepositoryImpl implements HubsConnectorRepository {
.zipCode(zipCode ?? placeAddress?.zipCode) .zipCode(zipCode ?? placeAddress?.zipCode)
.execute(); .execute();
final String hubId = result.data.teamHub_insert.id;
CostCenter? costCenter;
if (costCenterId != null && costCenterId.isNotEmpty) {
await _service.connector
.createTeamHudDepartment(
name: costCenterId,
teamHubId: hubId,
)
.costCenter(costCenterId)
.execute();
costCenter = CostCenter(id: costCenterId, name: costCenterId, code: costCenterId);
}
return Hub( return Hub(
id: result.data.teamHub_insert.id, id: hubId,
businessId: businessId, businessId: businessId,
name: name, name: name,
address: address, address: address,
nfcTagId: null, nfcTagId: null,
status: HubStatus.active, status: HubStatus.active,
costCenter: null, costCenter: costCenter,
); );
}); });
} }
@@ -99,6 +137,7 @@ class HubsConnectorRepositoryImpl implements HubsConnectorRepository {
String? street, String? street,
String? country, String? country,
String? zipCode, String? zipCode,
String? costCenterId,
}) async { }) async {
return _service.run(() async { return _service.run(() async {
final _PlaceAddress? placeAddress = (placeId != null && placeId.isNotEmpty) final _PlaceAddress? placeAddress = (placeId != null && placeId.isNotEmpty)
@@ -130,7 +169,43 @@ class HubsConnectorRepositoryImpl implements HubsConnectorRepository {
await builder.execute(); await builder.execute();
// Return a basic hub object reflecting changes (or we could re-fetch) CostCenter? costCenter;
final QueryResult<
dc.ListTeamHudDepartmentsByTeamHubIdData,
dc.ListTeamHudDepartmentsByTeamHubIdVariables
>
deptsResult = await _service.connector
.listTeamHudDepartmentsByTeamHubId(teamHubId: id)
.execute();
final List<dc.ListTeamHudDepartmentsByTeamHubIdTeamHudDepartments> depts =
deptsResult.data.teamHudDepartments;
if (costCenterId == null || costCenterId.isEmpty) {
if (depts.isNotEmpty) {
await _service.connector
.updateTeamHudDepartment(id: depts.first.id)
.costCenter(null)
.execute();
}
} else {
if (depts.isNotEmpty) {
await _service.connector
.updateTeamHudDepartment(id: depts.first.id)
.costCenter(costCenterId)
.execute();
costCenter = CostCenter(id: costCenterId, name: costCenterId, code: costCenterId);
} else {
await _service.connector
.createTeamHudDepartment(
name: costCenterId,
teamHubId: id,
)
.costCenter(costCenterId)
.execute();
costCenter = CostCenter(id: costCenterId, name: costCenterId, code: costCenterId);
}
}
return Hub( return Hub(
id: id, id: id,
businessId: businessId, businessId: businessId,
@@ -138,7 +213,7 @@ class HubsConnectorRepositoryImpl implements HubsConnectorRepository {
address: address ?? '', address: address ?? '',
nfcTagId: null, nfcTagId: null,
status: HubStatus.active, status: HubStatus.active,
costCenter: null, costCenter: costCenter,
); );
}); });
} }

View File

@@ -20,6 +20,7 @@ abstract interface class HubsConnectorRepository {
String? street, String? street,
String? country, String? country,
String? zipCode, String? zipCode,
String? costCenterId,
}); });
/// Updates an existing hub. /// Updates an existing hub.
@@ -36,6 +37,7 @@ abstract interface class HubsConnectorRepository {
String? street, String? street,
String? country, String? country,
String? zipCode, String? zipCode,
String? costCenterId,
}); });
/// Deletes a hub. /// Deletes a hub.

View File

@@ -1,4 +1,4 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs // ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
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'; import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/hub_repository_interface.dart'; import '../../domain/repositories/hub_repository_interface.dart';
@@ -26,13 +26,20 @@ class HubRepositoryImpl implements HubRepositoryInterface {
@override @override
Future<List<CostCenter>> getCostCenters() async { Future<List<CostCenter>> getCostCenters() async {
// Mocking cost centers for now since the backend is not yet ready. return _service.run(() async {
return <CostCenter>[ final result = await _service.connector.listTeamHudDepartments().execute();
const CostCenter(id: 'cc-001', name: 'Kitchen', code: '1001'), final Set<String> seen = <String>{};
const CostCenter(id: 'cc-002', name: 'Front Desk', code: '1002'), final List<CostCenter> costCenters = <CostCenter>[];
const CostCenter(id: 'cc-003', name: 'Waitstaff', code: '1003'), for (final dc.ListTeamHudDepartmentsTeamHudDepartments dep
const CostCenter(id: 'cc-004', name: 'Management', code: '1004'), in result.data.teamHudDepartments) {
]; final String? cc = dep.costCenter;
if (cc != null && cc.isNotEmpty && !seen.contains(cc)) {
seen.add(cc);
costCenters.add(CostCenter(id: cc, name: dep.name, code: cc));
}
}
return costCenters;
});
} }
@override @override
@@ -62,6 +69,7 @@ class HubRepositoryImpl implements HubRepositoryInterface {
street: street, street: street,
country: country, country: country,
zipCode: zipCode, zipCode: zipCode,
costCenterId: costCenterId,
); );
} }
@@ -107,6 +115,7 @@ class HubRepositoryImpl implements HubRepositoryInterface {
street: street, street: street,
country: country, country: country,
zipCode: zipCode, zipCode: zipCode,
costCenterId: costCenterId,
); );
} }
} }

View File

@@ -72,7 +72,7 @@ class EditHubBloc extends Bloc<EditHubEvent, EditHubState>
emit( emit(
state.copyWith( state.copyWith(
status: EditHubStatus.success, status: EditHubStatus.success,
successMessage: 'Hub created successfully', successKey: 'created',
), ),
); );
}, },
@@ -109,7 +109,7 @@ class EditHubBloc extends Bloc<EditHubEvent, EditHubState>
emit( emit(
state.copyWith( state.copyWith(
status: EditHubStatus.success, status: EditHubStatus.success,
successMessage: 'Hub updated successfully', successKey: 'updated',
), ),
); );
}, },

View File

@@ -22,6 +22,7 @@ class EditHubState extends Equatable {
this.status = EditHubStatus.initial, this.status = EditHubStatus.initial,
this.errorMessage, this.errorMessage,
this.successMessage, this.successMessage,
this.successKey,
this.costCenters = const <CostCenter>[], this.costCenters = const <CostCenter>[],
}); });
@@ -34,6 +35,9 @@ class EditHubState extends Equatable {
/// The success message if the operation succeeded. /// The success message if the operation succeeded.
final String? successMessage; final String? successMessage;
/// Localization key for success message: 'created' | 'updated'.
final String? successKey;
/// Available cost centers for selection. /// Available cost centers for selection.
final List<CostCenter> costCenters; final List<CostCenter> costCenters;
@@ -42,12 +46,14 @@ class EditHubState extends Equatable {
EditHubStatus? status, EditHubStatus? status,
String? errorMessage, String? errorMessage,
String? successMessage, String? successMessage,
String? successKey,
List<CostCenter>? costCenters, List<CostCenter>? costCenters,
}) { }) {
return EditHubState( return EditHubState(
status: status ?? this.status, status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage, errorMessage: errorMessage ?? this.errorMessage,
successMessage: successMessage ?? this.successMessage, successMessage: successMessage ?? this.successMessage,
successKey: successKey ?? this.successKey,
costCenters: costCenters ?? this.costCenters, costCenters: costCenters ?? this.costCenters,
); );
} }
@@ -57,6 +63,7 @@ class EditHubState extends Equatable {
status, status,
errorMessage, errorMessage,
successMessage, successMessage,
successKey,
costCenters, costCenters,
]; ];
} }

View File

@@ -36,7 +36,7 @@ class HubDetailsBloc extends Bloc<HubDetailsEvent, HubDetailsState>
emit( emit(
state.copyWith( state.copyWith(
status: HubDetailsStatus.deleted, status: HubDetailsStatus.deleted,
successMessage: 'Hub deleted successfully', successKey: 'deleted',
), ),
); );
}, },

View File

@@ -24,6 +24,7 @@ class HubDetailsState extends Equatable {
this.status = HubDetailsStatus.initial, this.status = HubDetailsStatus.initial,
this.errorMessage, this.errorMessage,
this.successMessage, this.successMessage,
this.successKey,
}); });
/// The status of the operation. /// The status of the operation.
@@ -35,19 +36,24 @@ class HubDetailsState extends Equatable {
/// The success message if the operation succeeded. /// The success message if the operation succeeded.
final String? successMessage; final String? successMessage;
/// Localization key for success message: 'deleted'.
final String? successKey;
/// Create a copy of this state with the given fields replaced. /// Create a copy of this state with the given fields replaced.
HubDetailsState copyWith({ HubDetailsState copyWith({
HubDetailsStatus? status, HubDetailsStatus? status,
String? errorMessage, String? errorMessage,
String? successMessage, String? successMessage,
String? successKey,
}) { }) {
return HubDetailsState( return HubDetailsState(
status: status ?? this.status, status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage, errorMessage: errorMessage ?? this.errorMessage,
successMessage: successMessage ?? this.successMessage, successMessage: successMessage ?? this.successMessage,
successKey: successKey ?? this.successKey,
); );
} }
@override @override
List<Object?> get props => <Object?>[status, errorMessage, successMessage]; List<Object?> get props => <Object?>[status, errorMessage, successMessage, successKey];
} }

View File

@@ -1,3 +1,4 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@@ -34,14 +35,16 @@ class _EditHubPageState extends State<EditHubPage> {
value: widget.bloc, value: widget.bloc,
child: BlocListener<EditHubBloc, EditHubState>( child: BlocListener<EditHubBloc, EditHubState>(
listenWhen: (EditHubState prev, EditHubState curr) => listenWhen: (EditHubState prev, EditHubState curr) =>
prev.status != curr.status || prev.status != curr.status || prev.successKey != curr.successKey,
prev.successMessage != curr.successMessage,
listener: (BuildContext context, EditHubState state) { listener: (BuildContext context, EditHubState state) {
if (state.status == EditHubStatus.success && if (state.status == EditHubStatus.success &&
state.successMessage != null) { state.successKey != null) {
final String message = state.successKey == 'created'
? t.client_hubs.edit_hub.created_success
: t.client_hubs.edit_hub.updated_success;
UiSnackbar.show( UiSnackbar.show(
context, context,
message: state.successMessage!, message: message,
type: UiSnackbarType.success, type: UiSnackbarType.success,
); );
Modular.to.pop(true); Modular.to.pop(true);

View File

@@ -29,9 +29,12 @@ class HubDetailsPage extends StatelessWidget {
child: BlocListener<HubDetailsBloc, HubDetailsState>( child: BlocListener<HubDetailsBloc, HubDetailsState>(
listener: (BuildContext context, HubDetailsState state) { listener: (BuildContext context, HubDetailsState state) {
if (state.status == HubDetailsStatus.deleted) { if (state.status == HubDetailsStatus.deleted) {
final String message = state.successKey == 'deleted'
? t.client_hubs.hub_details.deleted_success
: (state.successMessage ?? t.client_hubs.hub_details.deleted_success);
UiSnackbar.show( UiSnackbar.show(
context, context,
message: state.successMessage ?? 'Hub deleted successfully', message: message,
type: UiSnackbarType.success, type: UiSnackbarType.success,
); );
Modular.to.pop(true); // Return true to indicate change Modular.to.pop(true); // Return true to indicate change

View File

@@ -51,7 +51,7 @@ class EditHubFormSection extends StatelessWidget {
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
validator: (String? value) { validator: (String? value) {
if (value == null || value.trim().isEmpty) { if (value == null || value.trim().isEmpty) {
return 'Name is required'; return t.client_hubs.edit_hub.name_required;
} }
return null; return null;
}, },
@@ -181,11 +181,11 @@ class EditHubFormSection extends StatelessWidget {
width: double.maxFinite, width: double.maxFinite,
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 400), constraints: const BoxConstraints(maxHeight: 400),
child: costCenters.isEmpty child : costCenters.isEmpty
? const Padding( ? Padding(
padding: EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text('No cost centers available'), child: Text(t.client_hubs.edit_hub.cost_centers_empty),
) )
: ListView.builder( : ListView.builder(
shrinkWrap: true, shrinkWrap: true,
itemCount: costCenters.length, itemCount: costCenters.length,

View File

@@ -318,9 +318,9 @@ class _HubFormDialogState extends State<HubFormDialog> {
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 400), constraints: const BoxConstraints(maxHeight: 400),
child: widget.costCenters.isEmpty child: widget.costCenters.isEmpty
? const Padding( ? Padding(
padding: EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text('No cost centers available'), child: Text(t.client_hubs.add_hub_dialog.cost_centers_empty),
) )
: ListView.builder( : ListView.builder(
shrinkWrap: true, shrinkWrap: true,

View File

@@ -11,6 +11,8 @@ class HubManagerSelector extends StatelessWidget {
required this.hintText, required this.hintText,
required this.label, required this.label,
this.description, this.description,
this.noManagersText,
this.noneText,
super.key, super.key,
}); });
@@ -20,6 +22,8 @@ class HubManagerSelector extends StatelessWidget {
final String hintText; final String hintText;
final String label; final String label;
final String? description; final String? description;
final String? noManagersText;
final String? noneText;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -107,18 +111,20 @@ class HubManagerSelector extends StatelessWidget {
shrinkWrap: true, shrinkWrap: true,
itemCount: managers.isEmpty ? 2 : managers.length + 1, itemCount: managers.isEmpty ? 2 : managers.length + 1,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final String emptyText = noManagersText ?? 'No hub managers available';
final String noneLabel = noneText ?? 'None';
if (managers.isEmpty) { if (managers.isEmpty) {
if (index == 0) { if (index == 0) {
return const Padding( return Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: Text('No hub managers available'), child: Text(emptyText),
); );
} }
return ListTile( return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text('None', style: UiTypography.body1m.textSecondary), title: Text(noneLabel, style: UiTypography.body1m.textSecondary),
onTap: () => Navigator.of(context).pop( onTap: () => Navigator.of(context).pop(
const OrderManagerUiModel(id: 'NONE', name: 'None'), OrderManagerUiModel(id: 'NONE', name: noneLabel),
), ),
); );
} }
@@ -126,9 +132,9 @@ class HubManagerSelector extends StatelessWidget {
if (index == managers.length) { if (index == managers.length) {
return ListTile( return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text('None', style: UiTypography.body1m.textSecondary), title: Text(noneLabel, style: UiTypography.body1m.textSecondary),
onTap: () => Navigator.of(context).pop( onTap: () => Navigator.of(context).pop(
const OrderManagerUiModel(id: 'NONE', name: 'None'), OrderManagerUiModel(id: 'NONE', name: noneLabel),
), ),
); );
} }

View File

@@ -332,6 +332,8 @@ class _OneTimeOrderForm extends StatelessWidget {
label: labels.hub_manager_label, label: labels.hub_manager_label,
description: labels.hub_manager_desc, description: labels.hub_manager_desc,
hintText: labels.hub_manager_hint, hintText: labels.hub_manager_hint,
noManagersText: labels.hub_manager_empty,
noneText: labels.hub_manager_none,
managers: hubManagers, managers: hubManagers,
selectedManager: selectedHubManager, selectedManager: selectedHubManager,
onChanged: onHubManagerChanged, onChanged: onHubManagerChanged,

View File

@@ -354,6 +354,8 @@ class _PermanentOrderForm extends StatelessWidget {
label: oneTimeLabels.hub_manager_label, label: oneTimeLabels.hub_manager_label,
description: oneTimeLabels.hub_manager_desc, description: oneTimeLabels.hub_manager_desc,
hintText: oneTimeLabels.hub_manager_hint, hintText: oneTimeLabels.hub_manager_hint,
noManagersText: oneTimeLabels.hub_manager_empty,
noneText: oneTimeLabels.hub_manager_none,
managers: hubManagers, managers: hubManagers,
selectedManager: selectedHubManager, selectedManager: selectedHubManager,
onChanged: onHubManagerChanged, onChanged: onHubManagerChanged,

View File

@@ -375,6 +375,8 @@ class _RecurringOrderForm extends StatelessWidget {
label: oneTimeLabels.hub_manager_label, label: oneTimeLabels.hub_manager_label,
description: oneTimeLabels.hub_manager_desc, description: oneTimeLabels.hub_manager_desc,
hintText: oneTimeLabels.hub_manager_hint, hintText: oneTimeLabels.hub_manager_hint,
noManagersText: oneTimeLabels.hub_manager_empty,
noneText: oneTimeLabels.hub_manager_none,
managers: hubManagers, managers: hubManagers,
selectedManager: selectedHubManager, selectedManager: selectedHubManager,
onChanged: onHubManagerChanged, onChanged: onHubManagerChanged,

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:firebase_auth/firebase_auth.dart' as firebase; import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:firebase_data_connect/firebase_data_connect.dart';
@@ -686,7 +687,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
padding: const EdgeInsets.all(UiConstants.space5), padding: const EdgeInsets.all(UiConstants.space5),
children: <Widget>[ children: <Widget>[
Text( Text(
'Edit Your Order', t.client_view_orders.order_edit_sheet.title,
style: UiTypography.headline3m.textPrimary, style: UiTypography.headline3m.textPrimary,
), ),
const SizedBox(height: UiConstants.space4), const SizedBox(height: UiConstants.space4),
@@ -744,7 +745,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
_buildSectionHeader('ORDER NAME'), _buildSectionHeader('ORDER NAME'),
UiTextField( UiTextField(
controller: _orderNameController, controller: _orderNameController,
hintText: 'Order name', hintText: t.client_view_orders.order_edit_sheet.order_name_hint,
prefixIcon: UiIcons.briefcase, prefixIcon: UiIcons.briefcase,
), ),
const SizedBox(height: UiConstants.space4), const SizedBox(height: UiConstants.space4),
@@ -801,7 +802,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Text( Text(
'POSITIONS', t.client_view_orders.order_edit_sheet.positions_section,
style: UiTypography.headline4m.textPrimary, style: UiTypography.headline4m.textPrimary,
), ),
TextButton( TextButton(
@@ -821,7 +822,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
color: UiColors.primary, color: UiColors.primary,
), ),
Text( Text(
'Add Position', t.client_view_orders.order_edit_sheet.add_position,
style: UiTypography.body2m.primary, style: UiTypography.body2m.primary,
), ),
], ],
@@ -842,7 +843,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
), ),
), ),
_buildBottomAction( _buildBottomAction(
label: 'Review ${_positions.length} Positions', label: t.client_view_orders.order_edit_sheet.review_positions(count: _positions.length.toString()),
onPressed: () => setState(() => _showReview = true), onPressed: () => setState(() => _showReview = true),
), ),
const Padding( const Padding(
@@ -859,11 +860,13 @@ class OrderEditSheetState extends State<OrderEditSheet> {
} }
Widget _buildHubManagerSelector() { Widget _buildHubManagerSelector() {
final TranslationsClientViewOrdersOrderEditSheetEn oes =
t.client_view_orders.order_edit_sheet;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
_buildSectionHeader('SHIFT CONTACT'), _buildSectionHeader(oes.shift_contact_section),
Text('On-site manager or supervisor for this shift', style: UiTypography.body2r.textSecondary), Text(oes.shift_contact_desc, style: UiTypography.body2r.textSecondary),
const SizedBox(height: UiConstants.space2), const SizedBox(height: UiConstants.space2),
InkWell( InkWell(
onTap: () => _showHubManagerSelector(), onTap: () => _showHubManagerSelector(),
@@ -895,7 +898,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
), ),
const SizedBox(width: UiConstants.space3), const SizedBox(width: UiConstants.space3),
Text( Text(
_selectedManager?.user.fullName ?? 'Select Contact', _selectedManager?.user.fullName ?? oes.select_contact,
style: _selectedManager != null style: _selectedManager != null
? UiTypography.body1r.textPrimary ? UiTypography.body1r.textPrimary
: UiTypography.body2r.textPlaceholder, : UiTypography.body2r.textPlaceholder,
@@ -925,7 +928,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
borderRadius: BorderRadius.circular(UiConstants.radiusBase), borderRadius: BorderRadius.circular(UiConstants.radiusBase),
), ),
title: Text( title: Text(
'Shift Contact', t.client_view_orders.order_edit_sheet.shift_contact_section,
style: UiTypography.headline3m.textPrimary, style: UiTypography.headline3m.textPrimary,
), ),
contentPadding: const EdgeInsets.symmetric(vertical: 16), contentPadding: const EdgeInsets.symmetric(vertical: 16),
@@ -939,14 +942,14 @@ class OrderEditSheetState extends State<OrderEditSheet> {
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
if (_managers.isEmpty) { if (_managers.isEmpty) {
if (index == 0) { if (index == 0) {
return const Padding( return Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: Text('No hub managers available'), child: Text(t.client_view_orders.order_edit_sheet.no_hub_managers),
); );
} }
return ListTile( return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text('None', style: UiTypography.body1m.textSecondary), title: Text(t.client_view_orders.order_edit_sheet.none, style: UiTypography.body1m.textSecondary),
onTap: () => Navigator.of(context).pop(null), onTap: () => Navigator.of(context).pop(null),
); );
} }
@@ -954,7 +957,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
if (index == _managers.length) { if (index == _managers.length) {
return ListTile( return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text('None', style: UiTypography.body1m.textSecondary), title: Text(t.client_view_orders.order_edit_sheet.none, style: UiTypography.body1m.textSecondary),
onTap: () => Navigator.of(context).pop(null), onTap: () => Navigator.of(context).pop(null),
); );
} }
@@ -1014,11 +1017,11 @@ class OrderEditSheetState extends State<OrderEditSheet> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( Text(
'One-Time Order', t.client_view_orders.order_edit_sheet.one_time_order_title,
style: UiTypography.headline3m.copyWith(color: UiColors.white), style: UiTypography.headline3m.copyWith(color: UiColors.white),
), ),
Text( Text(
'Refine your staffing needs', t.client_view_orders.order_edit_sheet.refine_subtitle,
style: UiTypography.footnote2r.copyWith( style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.8), color: UiColors.white.withValues(alpha: 0.8),
), ),
@@ -1060,7 +1063,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
GestureDetector( GestureDetector(
onTap: () => _removePosition(index), onTap: () => _removePosition(index),
child: Text( child: Text(
'Remove', t.client_view_orders.order_edit_sheet.remove,
style: UiTypography.footnote1m.copyWith( style: UiTypography.footnote1m.copyWith(
color: UiColors.destructive, color: UiColors.destructive,
), ),
@@ -1071,7 +1074,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
const SizedBox(height: UiConstants.space3), const SizedBox(height: UiConstants.space3),
_buildDropdownField( _buildDropdownField(
hint: 'Select role', hint: t.client_view_orders.order_edit_sheet.select_role_hint,
value: pos['roleId'], value: pos['roleId'],
items: <String>[ items: <String>[
..._roles.map((_RoleOption role) => role.id), ..._roles.map((_RoleOption role) => role.id),
@@ -1106,7 +1109,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: _buildInlineTimeInput( child: _buildInlineTimeInput(
label: 'Start', label: t.client_view_orders.order_edit_sheet.start_label,
value: pos['start_time'], value: pos['start_time'],
onTap: () async { onTap: () async {
final TimeOfDay? picked = await showTimePicker( final TimeOfDay? picked = await showTimePicker(
@@ -1126,7 +1129,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
const SizedBox(width: UiConstants.space2), const SizedBox(width: UiConstants.space2),
Expanded( Expanded(
child: _buildInlineTimeInput( child: _buildInlineTimeInput(
label: 'End', label: t.client_view_orders.order_edit_sheet.end_label,
value: pos['end_time'], value: pos['end_time'],
onTap: () async { onTap: () async {
final TimeOfDay? picked = await showTimePicker( final TimeOfDay? picked = await showTimePicker(
@@ -1149,7 +1152,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( Text(
'Workers', t.client_view_orders.order_edit_sheet.workers_label,
style: UiTypography.footnote2r.textSecondary, style: UiTypography.footnote2r.textSecondary,
), ),
const SizedBox(height: UiConstants.space1), const SizedBox(height: UiConstants.space1),
@@ -1204,7 +1207,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
const Icon(UiIcons.mapPin, size: 14, color: UiColors.primary), const Icon(UiIcons.mapPin, size: 14, color: UiColors.primary),
const SizedBox(width: UiConstants.space1), const SizedBox(width: UiConstants.space1),
Text( Text(
'Use different location for this position', t.client_view_orders.order_edit_sheet.different_location,
style: UiTypography.footnote1m.copyWith( style: UiTypography.footnote1m.copyWith(
color: UiColors.primary, color: UiColors.primary,
), ),
@@ -1228,7 +1231,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
), ),
const SizedBox(width: UiConstants.space1), const SizedBox(width: UiConstants.space1),
Text( Text(
'Different Location', t.client_view_orders.order_edit_sheet.different_location_title,
style: UiTypography.footnote1m.textSecondary, style: UiTypography.footnote1m.textSecondary,
), ),
], ],
@@ -1246,7 +1249,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
const SizedBox(height: UiConstants.space2), const SizedBox(height: UiConstants.space2),
UiTextField( UiTextField(
controller: TextEditingController(text: pos['location']), controller: TextEditingController(text: pos['location']),
hintText: 'Enter different address', hintText: t.client_view_orders.order_edit_sheet.enter_address_hint,
onChanged: (String val) => onChanged: (String val) =>
_updatePosition(index, 'location', val), _updatePosition(index, 'location', val),
), ),
@@ -1257,7 +1260,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
_buildSectionHeader('LUNCH BREAK'), _buildSectionHeader('LUNCH BREAK'),
_buildDropdownField( _buildDropdownField(
hint: 'No Break', hint: t.client_view_orders.order_edit_sheet.no_break,
value: pos['lunch_break'], value: pos['lunch_break'],
items: <String>[ items: <String>[
'NO_BREAK', 'NO_BREAK',
@@ -1280,7 +1283,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
case 'MIN_60': case 'MIN_60':
return '60 min (Unpaid)'; return '60 min (Unpaid)';
default: default:
return 'No Break'; return t.client_view_orders.order_edit_sheet.no_break;
} }
}, },
onChanged: (dynamic val) => onChanged: (dynamic val) =>
@@ -1438,11 +1441,11 @@ class OrderEditSheetState extends State<OrderEditSheet> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[ children: <Widget>[
_buildSummaryItem('${_positions.length}', 'Positions'), _buildSummaryItem('${_positions.length}', t.client_view_orders.order_edit_sheet.positions),
_buildSummaryItem('$totalWorkers', 'Workers'), _buildSummaryItem('$totalWorkers', t.client_view_orders.order_edit_sheet.workers),
_buildSummaryItem( _buildSummaryItem(
'\$${totalCost.round()}', '\$${totalCost.round()}',
'Est. Cost', t.client_view_orders.order_edit_sheet.est_cost,
), ),
], ],
), ),
@@ -1501,7 +1504,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
'Positions Breakdown', t.client_view_orders.order_edit_sheet.positions_breakdown,
style: UiTypography.body2b.textPrimary, style: UiTypography.body2b.textPrimary,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@@ -1532,14 +1535,14 @@ class OrderEditSheetState extends State<OrderEditSheet> {
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: UiButton.secondary( child: UiButton.secondary(
text: 'Edit', text: t.client_view_orders.order_edit_sheet.edit_button,
onPressed: () => setState(() => _showReview = false), onPressed: () => setState(() => _showReview = false),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: UiButton.primary( child: UiButton.primary(
text: 'Confirm & Save', text: t.client_view_orders.order_edit_sheet.confirm_save,
onPressed: () async { onPressed: () async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
await _saveOrderChanges(); await _saveOrderChanges();
@@ -1601,7 +1604,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
children: <Widget>[ children: <Widget>[
Text( Text(
(role?.name ?? pos['roleName']?.toString() ?? '').isEmpty (role?.name ?? pos['roleName']?.toString() ?? '').isEmpty
? 'Position' ? t.client_view_orders.order_edit_sheet.position_singular
: (role?.name ?? pos['roleName']?.toString() ?? ''), : (role?.name ?? pos['roleName']?.toString() ?? ''),
style: UiTypography.body2b.textPrimary, style: UiTypography.body2b.textPrimary,
), ),
@@ -1667,14 +1670,14 @@ class OrderEditSheetState extends State<OrderEditSheet> {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
'Order Updated!', t.client_view_orders.order_edit_sheet.order_updated_title,
style: UiTypography.headline1m.copyWith(color: UiColors.white), style: UiTypography.headline1m.copyWith(color: UiColors.white),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 40), padding: const EdgeInsets.symmetric(horizontal: 40),
child: Text( child: Text(
'Your shift has been updated successfully.', t.client_view_orders.order_edit_sheet.order_updated_message,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: UiTypography.body1r.copyWith( style: UiTypography.body1r.copyWith(
color: UiColors.white.withValues(alpha: 0.7), color: UiColors.white.withValues(alpha: 0.7),
@@ -1685,7 +1688,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 40), padding: const EdgeInsets.symmetric(horizontal: 40),
child: UiButton.secondary( child: UiButton.secondary(
text: 'Back to Orders', text: t.client_view_orders.order_edit_sheet.back_to_orders,
fullWidth: true, fullWidth: true,
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
backgroundColor: UiColors.white, backgroundColor: UiColors.white,