finalcommitform4
This commit is contained in:
@@ -239,6 +239,24 @@
|
||||
"address_hint": "Full address",
|
||||
"create_button": "Create Hub"
|
||||
},
|
||||
"edit_hub": {
|
||||
"title": "Edit Hub",
|
||||
"subtitle": "Update hub details",
|
||||
"name_label": "Hub Name *",
|
||||
"name_hint": "e.g., Main Kitchen, Front Desk",
|
||||
"address_label": "Address",
|
||||
"address_hint": "Full address",
|
||||
"save_button": "Save Changes",
|
||||
"success": "Hub updated successfully!"
|
||||
},
|
||||
"hub_details": {
|
||||
"title": "Hub Details",
|
||||
"name_label": "Name",
|
||||
"address_label": "Address",
|
||||
"nfc_label": "NFC Tag",
|
||||
"nfc_not_assigned": "Not Assigned",
|
||||
"edit_button": "Edit Hub"
|
||||
},
|
||||
"nfc_dialog": {
|
||||
"title": "Identify NFC Tag",
|
||||
"instruction": "Tap your phone to the NFC tag to identify it",
|
||||
@@ -1154,6 +1172,7 @@
|
||||
"success": {
|
||||
"hub": {
|
||||
"created": "Hub created successfully!",
|
||||
"updated": "Hub updated successfully!",
|
||||
"deleted": "Hub deleted successfully!",
|
||||
"nfc_assigned": "NFC tag assigned successfully!"
|
||||
},
|
||||
|
||||
@@ -253,6 +253,24 @@
|
||||
"dependency_warning": "Ten en cuenta que si hay turnos/órdenes asignados a este hub no deberíamos poder eliminarlo.",
|
||||
"cancel": "Cancelar",
|
||||
"delete": "Eliminar"
|
||||
},
|
||||
"edit_hub": {
|
||||
"title": "Editar Hub",
|
||||
"subtitle": "Actualizar detalles del hub",
|
||||
"name_label": "Nombre del Hub",
|
||||
"name_hint": "Ingresar nombre del hub",
|
||||
"address_label": "Dirección",
|
||||
"address_hint": "Ingresar dirección",
|
||||
"save_button": "Guardar Cambios",
|
||||
"success": "¡Hub actualizado exitosamente!"
|
||||
},
|
||||
"hub_details": {
|
||||
"title": "Detalles del Hub",
|
||||
"edit_button": "Editar",
|
||||
"name_label": "Nombre del Hub",
|
||||
"address_label": "Dirección",
|
||||
"nfc_label": "Etiqueta NFC",
|
||||
"nfc_not_assigned": "No asignada"
|
||||
}
|
||||
},
|
||||
"client_create_order": {
|
||||
@@ -1154,6 +1172,7 @@
|
||||
"success": {
|
||||
"hub": {
|
||||
"created": "¡Hub creado exitosamente!",
|
||||
"updated": "¡Hub actualizado exitosamente!",
|
||||
"deleted": "¡Hub eliminado exitosamente!",
|
||||
"nfc_assigned": "¡Etiqueta NFC asignada exitosamente!"
|
||||
},
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
<<<<<<< Updated upstream
|
||||
import 'recurring_order_position.dart';
|
||||
|
||||
/// Represents a recurring staffing request spanning a date range.
|
||||
=======
|
||||
import 'one_time_order.dart';
|
||||
import 'one_time_order_position.dart';
|
||||
|
||||
/// Represents a customer's request for recurring staffing.
|
||||
>>>>>>> Stashed changes
|
||||
class RecurringOrder extends Equatable {
|
||||
const RecurringOrder({
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.recurringDays,
|
||||
<<<<<<< Updated upstream
|
||||
required this.location,
|
||||
=======
|
||||
>>>>>>> Stashed changes
|
||||
required this.positions,
|
||||
this.hub,
|
||||
this.eventName,
|
||||
@@ -25,7 +15,6 @@ class RecurringOrder extends Equatable {
|
||||
this.roleRates = const <String, double>{},
|
||||
});
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
/// Start date for the recurring schedule.
|
||||
final DateTime startDate;
|
||||
|
||||
@@ -59,25 +48,6 @@ class RecurringOrder extends Equatable {
|
||||
endDate,
|
||||
recurringDays,
|
||||
location,
|
||||
=======
|
||||
final DateTime startDate;
|
||||
final DateTime endDate;
|
||||
|
||||
/// List of days (e.g., ['Monday', 'Wednesday']) or bitmask.
|
||||
final List<String> recurringDays;
|
||||
|
||||
final List<OneTimeOrderPosition> positions;
|
||||
final OneTimeOrderHubDetails? hub;
|
||||
final String? eventName;
|
||||
final String? vendorId;
|
||||
final Map<String, double> roleRates;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
startDate,
|
||||
endDate,
|
||||
recurringDays,
|
||||
>>>>>>> Stashed changes
|
||||
positions,
|
||||
hub,
|
||||
eventName,
|
||||
@@ -85,7 +55,6 @@ class RecurringOrder extends Equatable {
|
||||
roleRates,
|
||||
];
|
||||
}
|
||||
<<<<<<< Updated upstream
|
||||
|
||||
/// Minimal hub details used during recurring order creation.
|
||||
class RecurringOrderHubDetails extends Equatable {
|
||||
@@ -130,5 +99,3 @@ class RecurringOrderHubDetails extends Equatable {
|
||||
zipCode,
|
||||
];
|
||||
}
|
||||
=======
|
||||
>>>>>>> Stashed changes
|
||||
|
||||
@@ -179,7 +179,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
.date(orderTimestamp)
|
||||
.startDate(startTimestamp)
|
||||
.endDate(endTimestamp)
|
||||
.recurringDays(fdc.AnyValue(order.recurringDays))
|
||||
.recurringDays(order.recurringDays)
|
||||
.execute();
|
||||
|
||||
final String orderId = orderResult.data.order_insert.id;
|
||||
@@ -274,7 +274,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
if (vendorId == null || vendorId.isEmpty) {
|
||||
throw Exception('Vendor is missing.');
|
||||
}
|
||||
final domain.PermanentOrderHubDetails? hub = order.hub;
|
||||
final domain.OneTimeOrderHubDetails? hub = order.hub;
|
||||
if (hub == null || hub.id.isEmpty) {
|
||||
throw Exception('Hub is missing.');
|
||||
}
|
||||
@@ -299,7 +299,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
.status(dc.OrderStatus.POSTED)
|
||||
.date(orderTimestamp)
|
||||
.startDate(startTimestamp)
|
||||
.permanentDays(fdc.AnyValue(order.permanentDays))
|
||||
.permanentDays(order.permanentDays)
|
||||
.execute();
|
||||
|
||||
final String orderId = orderResult.data.order_insert.id;
|
||||
@@ -311,7 +311,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
final Set<String> selectedDays = Set<String>.from(order.permanentDays);
|
||||
final int workersNeeded = order.positions.fold<int>(
|
||||
0,
|
||||
(int sum, domain.PermanentOrderPosition position) => sum + position.count,
|
||||
(int sum, domain.OneTimeOrderPosition position) => sum + position.count,
|
||||
);
|
||||
final double shiftCost = _calculatePermanentShiftCost(order);
|
||||
|
||||
@@ -352,7 +352,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
final String shiftId = shiftResult.data.shift_insert.id;
|
||||
shiftIds.add(shiftId);
|
||||
|
||||
for (final domain.PermanentOrderPosition position in order.positions) {
|
||||
for (final domain.OneTimeOrderPosition position in order.positions) {
|
||||
final DateTime start = _parseTime(day, position.startTime);
|
||||
final DateTime end = _parseTime(day, position.endTime);
|
||||
final DateTime normalizedEnd =
|
||||
@@ -420,7 +420,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
|
||||
double _calculatePermanentShiftCost(domain.PermanentOrder order) {
|
||||
double total = 0;
|
||||
for (final domain.PermanentOrderPosition position in order.positions) {
|
||||
for (final domain.OneTimeOrderPosition position in order.positions) {
|
||||
final DateTime start = _parseTime(order.startDate, position.startTime);
|
||||
final DateTime end = _parseTime(order.startDate, position.endTime);
|
||||
final DateTime normalizedEnd =
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/client_create_order_repository_interface.dart';
|
||||
|
||||
/// Use case for creating a permanent staffing order.
|
||||
class CreatePermanentOrderUseCase implements UseCase<Future<void>, PermanentOrder> {
|
||||
class CreatePermanentOrderUseCase implements UseCase<PermanentOrder, void> {
|
||||
const CreatePermanentOrderUseCase(this._repository);
|
||||
|
||||
final ClientCreateOrderRepositoryInterface _repository;
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/client_create_order_repository_interface.dart';
|
||||
|
||||
/// Use case for creating a recurring staffing order.
|
||||
class CreateRecurringOrderUseCase implements UseCase<Future<void>, RecurringOrder> {
|
||||
class CreateRecurringOrderUseCase implements UseCase<RecurringOrder, void> {
|
||||
const CreateRecurringOrderUseCase(this._repository);
|
||||
|
||||
final ClientCreateOrderRepositoryInterface _repository;
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||
import '../../domain/arguments/permanent_order_arguments.dart';
|
||||
import '../../domain/usecases/create_permanent_order_usecase.dart';
|
||||
import 'permanent_order_event.dart';
|
||||
import 'permanent_order_state.dart';
|
||||
@@ -286,10 +285,9 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||
final domain.PermanentOrder order = domain.PermanentOrder(
|
||||
startDate: state.startDate,
|
||||
permanentDays: state.permanentDays,
|
||||
location: selectedHub.name,
|
||||
positions: state.positions
|
||||
.map(
|
||||
(PermanentOrderPosition p) => domain.PermanentOrderPosition(
|
||||
(PermanentOrderPosition p) => domain.OneTimeOrderPosition(
|
||||
role: p.role,
|
||||
count: p.count,
|
||||
startTime: p.startTime,
|
||||
@@ -299,7 +297,7 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
hub: domain.PermanentOrderHubDetails(
|
||||
hub: domain.OneTimeOrderHubDetails(
|
||||
id: selectedHub.id,
|
||||
name: selectedHub.name,
|
||||
address: selectedHub.address,
|
||||
@@ -316,9 +314,7 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||
vendorId: state.selectedVendor?.id,
|
||||
roleRates: roleRates,
|
||||
);
|
||||
await _createPermanentOrderUseCase(
|
||||
PermanentOrderArguments(order: order),
|
||||
);
|
||||
await _createPermanentOrderUseCase(order);
|
||||
emit(state.copyWith(status: PermanentOrderStatus.success));
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||
import '../../domain/arguments/recurring_order_arguments.dart';
|
||||
import '../../domain/usecases/create_recurring_order_usecase.dart';
|
||||
import 'recurring_order_event.dart';
|
||||
import 'recurring_order_state.dart';
|
||||
@@ -334,9 +333,7 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||
vendorId: state.selectedVendor?.id,
|
||||
roleRates: roleRates,
|
||||
);
|
||||
await _createRecurringOrderUseCase(
|
||||
RecurringOrderArguments(order: order),
|
||||
);
|
||||
await _createRecurringOrderUseCase(order);
|
||||
emit(state.copyWith(status: RecurringOrderStatus.success));
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
|
||||
@@ -26,7 +26,7 @@ class ClientHomeEditBanner extends StatelessWidget {
|
||||
builder: (BuildContext context, ClientHomeState state) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
height: state.isEditMode ? 76 : 0,
|
||||
height: state.isEditMode ? 80 : 0,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
@@ -43,21 +43,23 @@ class ClientHomeEditBanner extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.edit, size: 16, color: UiColors.primary),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
i18n.dashboard.edit_mode_active,
|
||||
style: UiTypography.footnote1b.copyWith(
|
||||
color: UiColors.primary,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
i18n.dashboard.edit_mode_active,
|
||||
style: UiTypography.footnote1b.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
i18n.dashboard.drag_instruction,
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
),
|
||||
],
|
||||
Text(
|
||||
i18n.dashboard.drag_instruction,
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
UiButton.secondary(
|
||||
text: i18n.dashboard.reset,
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'src/domain/usecases/assign_nfc_tag_usecase.dart';
|
||||
import 'src/domain/usecases/create_hub_usecase.dart';
|
||||
import 'src/domain/usecases/delete_hub_usecase.dart';
|
||||
import 'src/domain/usecases/get_hubs_usecase.dart';
|
||||
import 'src/domain/usecases/update_hub_usecase.dart';
|
||||
import 'src/presentation/blocs/client_hubs_bloc.dart';
|
||||
import 'src/presentation/pages/client_hubs_page.dart';
|
||||
|
||||
@@ -29,6 +30,7 @@ class ClientHubsModule extends Module {
|
||||
i.addLazySingleton(CreateHubUseCase.new);
|
||||
i.addLazySingleton(DeleteHubUseCase.new);
|
||||
i.addLazySingleton(AssignNfcTagUseCase.new);
|
||||
i.addLazySingleton(UpdateHubUseCase.new);
|
||||
|
||||
// BLoCs
|
||||
i.add<ClientHubsBloc>(ClientHubsBloc.new);
|
||||
|
||||
@@ -124,6 +124,78 @@ class HubRepositoryImpl implements HubRepositoryInterface {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<domain.Hub> updateHub({
|
||||
required String id,
|
||||
String? name,
|
||||
String? address,
|
||||
String? placeId,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
String? city,
|
||||
String? state,
|
||||
String? street,
|
||||
String? country,
|
||||
String? zipCode,
|
||||
}) async {
|
||||
return _service.run(() async {
|
||||
final _PlaceAddress? placeAddress =
|
||||
placeId == null || placeId.isEmpty
|
||||
? null
|
||||
: await _fetchPlaceAddress(placeId);
|
||||
|
||||
final dc.UpdateTeamHubVariablesBuilder builder = _service.connector
|
||||
.updateTeamHub(id: id);
|
||||
|
||||
if (name != null) builder.hubName(name);
|
||||
if (address != null) builder.address(address);
|
||||
if (placeId != null || placeAddress != null) {
|
||||
builder.placeId(placeId ?? placeAddress?.street);
|
||||
}
|
||||
if (latitude != null) builder.latitude(latitude);
|
||||
if (longitude != null) builder.longitude(longitude);
|
||||
if (city != null || placeAddress?.city != null) {
|
||||
builder.city(city ?? placeAddress?.city);
|
||||
}
|
||||
if (state != null || placeAddress?.state != null) {
|
||||
builder.state(state ?? placeAddress?.state);
|
||||
}
|
||||
if (street != null || placeAddress?.street != null) {
|
||||
builder.street(street ?? placeAddress?.street);
|
||||
}
|
||||
if (country != null || placeAddress?.country != null) {
|
||||
builder.country(country ?? placeAddress?.country);
|
||||
}
|
||||
if (zipCode != null || placeAddress?.zipCode != null) {
|
||||
builder.zipCode(zipCode ?? placeAddress?.zipCode);
|
||||
}
|
||||
|
||||
await builder.execute();
|
||||
|
||||
final dc.GetBusinessesByUserIdBusinesses business =
|
||||
await _getBusinessForCurrentUser();
|
||||
final String teamId = await _getOrCreateTeamId(business);
|
||||
final List<domain.Hub> hubs = await _fetchHubsForTeam(
|
||||
teamId: teamId,
|
||||
businessId: business.id,
|
||||
);
|
||||
|
||||
for (final domain.Hub hub in hubs) {
|
||||
if (hub.id == id) return hub;
|
||||
}
|
||||
|
||||
// Fallback: return a reconstructed Hub from the update inputs.
|
||||
return domain.Hub(
|
||||
id: id,
|
||||
businessId: business.id,
|
||||
name: name ?? '',
|
||||
address: address ?? '',
|
||||
nfcTagId: null,
|
||||
status: domain.HubStatus.active,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<dc.GetBusinessesByUserIdBusinesses>
|
||||
_getBusinessForCurrentUser() async {
|
||||
final dc.ClientSession? session = dc.ClientSessionStore.instance.session;
|
||||
|
||||
@@ -35,4 +35,21 @@ abstract interface class HubRepositoryInterface {
|
||||
///
|
||||
/// Takes the [hubId] and the [nfcTagId] to be associated.
|
||||
Future<void> assignNfcTag({required String hubId, required String nfcTagId});
|
||||
|
||||
/// Updates an existing hub by its [id].
|
||||
///
|
||||
/// All fields other than [id] are optional — only supplied values are updated.
|
||||
Future<Hub> updateHub({
|
||||
required String id,
|
||||
String? name,
|
||||
String? address,
|
||||
String? placeId,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
String? city,
|
||||
String? state,
|
||||
String? street,
|
||||
String? country,
|
||||
String? zipCode,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import '../repositories/hub_repository_interface.dart';
|
||||
import '../../domain/arguments/create_hub_arguments.dart';
|
||||
|
||||
/// Arguments for the UpdateHubUseCase.
|
||||
class UpdateHubArguments {
|
||||
class UpdateHubArguments extends UseCaseArgument {
|
||||
const UpdateHubArguments({
|
||||
required this.id,
|
||||
this.name,
|
||||
@@ -30,10 +30,25 @@ class UpdateHubArguments {
|
||||
final String? street;
|
||||
final String? country;
|
||||
final String? zipCode;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
id,
|
||||
name,
|
||||
address,
|
||||
placeId,
|
||||
latitude,
|
||||
longitude,
|
||||
city,
|
||||
state,
|
||||
street,
|
||||
country,
|
||||
zipCode,
|
||||
];
|
||||
}
|
||||
|
||||
/// Use case for updating an existing hub.
|
||||
class UpdateHubUseCase implements UseCase<Future<Hub>, UpdateHubArguments> {
|
||||
class UpdateHubUseCase implements UseCase<UpdateHubArguments, Hub> {
|
||||
UpdateHubUseCase(this.repository);
|
||||
|
||||
final HubRepositoryInterface repository;
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../domain/usecases/assign_nfc_tag_usecase.dart';
|
||||
import '../../domain/usecases/create_hub_usecase.dart';
|
||||
import '../../domain/usecases/delete_hub_usecase.dart';
|
||||
import '../../domain/usecases/get_hubs_usecase.dart';
|
||||
import '../../domain/usecases/update_hub_usecase.dart';
|
||||
import 'client_hubs_event.dart';
|
||||
import 'client_hubs_state.dart';
|
||||
|
||||
@@ -25,13 +26,16 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
||||
required CreateHubUseCase createHubUseCase,
|
||||
required DeleteHubUseCase deleteHubUseCase,
|
||||
required AssignNfcTagUseCase assignNfcTagUseCase,
|
||||
required UpdateHubUseCase updateHubUseCase,
|
||||
}) : _getHubsUseCase = getHubsUseCase,
|
||||
_createHubUseCase = createHubUseCase,
|
||||
_deleteHubUseCase = deleteHubUseCase,
|
||||
_assignNfcTagUseCase = assignNfcTagUseCase,
|
||||
_updateHubUseCase = updateHubUseCase,
|
||||
super(const ClientHubsState()) {
|
||||
on<ClientHubsFetched>(_onFetched);
|
||||
on<ClientHubsAddRequested>(_onAddRequested);
|
||||
on<ClientHubsUpdateRequested>(_onUpdateRequested);
|
||||
on<ClientHubsDeleteRequested>(_onDeleteRequested);
|
||||
on<ClientHubsNfcTagAssignRequested>(_onNfcTagAssignRequested);
|
||||
on<ClientHubsMessageCleared>(_onMessageCleared);
|
||||
@@ -42,6 +46,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
||||
final CreateHubUseCase _createHubUseCase;
|
||||
final DeleteHubUseCase _deleteHubUseCase;
|
||||
final AssignNfcTagUseCase _assignNfcTagUseCase;
|
||||
final UpdateHubUseCase _updateHubUseCase;
|
||||
|
||||
void _onAddDialogToggled(
|
||||
ClientHubsAddDialogToggled event,
|
||||
@@ -120,6 +125,46 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onUpdateRequested(
|
||||
ClientHubsUpdateRequested event,
|
||||
Emitter<ClientHubsState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
|
||||
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
await _updateHubUseCase(
|
||||
UpdateHubArguments(
|
||||
id: event.id,
|
||||
name: event.name,
|
||||
address: event.address,
|
||||
placeId: event.placeId,
|
||||
latitude: event.latitude,
|
||||
longitude: event.longitude,
|
||||
city: event.city,
|
||||
state: event.state,
|
||||
street: event.street,
|
||||
country: event.country,
|
||||
zipCode: event.zipCode,
|
||||
),
|
||||
);
|
||||
final List<Hub> hubs = await _getHubsUseCase();
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ClientHubsStatus.actionSuccess,
|
||||
hubs: hubs,
|
||||
successMessage: 'Hub updated successfully!',
|
||||
),
|
||||
);
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
status: ClientHubsStatus.actionFailure,
|
||||
errorMessage: errorKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onDeleteRequested(
|
||||
ClientHubsDeleteRequested event,
|
||||
Emitter<ClientHubsState> emit,
|
||||
|
||||
@@ -55,6 +55,50 @@ class ClientHubsAddRequested extends ClientHubsEvent {
|
||||
];
|
||||
}
|
||||
|
||||
/// Event triggered to update an existing hub.
|
||||
class ClientHubsUpdateRequested extends ClientHubsEvent {
|
||||
const ClientHubsUpdateRequested({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.address,
|
||||
this.placeId,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.city,
|
||||
this.state,
|
||||
this.street,
|
||||
this.country,
|
||||
this.zipCode,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final String address;
|
||||
final String? placeId;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final String? city;
|
||||
final String? state;
|
||||
final String? street;
|
||||
final String? country;
|
||||
final String? zipCode;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
id,
|
||||
name,
|
||||
address,
|
||||
placeId,
|
||||
latitude,
|
||||
longitude,
|
||||
city,
|
||||
state,
|
||||
street,
|
||||
country,
|
||||
zipCode,
|
||||
];
|
||||
}
|
||||
|
||||
/// Event triggered to delete a hub.
|
||||
class ClientHubsDeleteRequested extends ClientHubsEvent {
|
||||
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:google_places_flutter/model/prediction.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import '../blocs/client_hubs_bloc.dart';
|
||||
import '../blocs/client_hubs_event.dart';
|
||||
import '../blocs/client_hubs_state.dart';
|
||||
import '../widgets/hub_address_autocomplete.dart';
|
||||
|
||||
/// A dedicated full-screen page for editing an existing hub.
|
||||
///
|
||||
/// Takes the parent [ClientHubsBloc] via [BlocProvider.value] so the
|
||||
/// updated hub list is reflected on the hubs list page when the user
|
||||
/// saves and navigates back.
|
||||
class EditHubPage extends StatefulWidget {
|
||||
const EditHubPage({
|
||||
required this.hub,
|
||||
required this.bloc,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Hub hub;
|
||||
final ClientHubsBloc bloc;
|
||||
|
||||
@override
|
||||
State<EditHubPage> createState() => _EditHubPageState();
|
||||
}
|
||||
|
||||
class _EditHubPageState extends State<EditHubPage> {
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
late final TextEditingController _nameController;
|
||||
late final TextEditingController _addressController;
|
||||
late final FocusNode _addressFocusNode;
|
||||
Prediction? _selectedPrediction;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nameController = TextEditingController(text: widget.hub.name);
|
||||
_addressController = TextEditingController(text: widget.hub.address);
|
||||
_addressFocusNode = FocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_addressController.dispose();
|
||||
_addressFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSave() {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
if (_addressController.text.trim().isEmpty) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: t.client_hubs.add_hub_dialog.address_hint,
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
context.read<ClientHubsBloc>().add(
|
||||
ClientHubsUpdateRequested(
|
||||
id: widget.hub.id,
|
||||
name: _nameController.text.trim(),
|
||||
address: _addressController.text.trim(),
|
||||
placeId: _selectedPrediction?.placeId,
|
||||
latitude: double.tryParse(_selectedPrediction?.lat ?? ''),
|
||||
longitude: double.tryParse(_selectedPrediction?.lng ?? ''),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<ClientHubsBloc>.value(
|
||||
value: widget.bloc,
|
||||
child: BlocListener<ClientHubsBloc, ClientHubsState>(
|
||||
listenWhen: (ClientHubsState prev, ClientHubsState curr) =>
|
||||
prev.status != curr.status || prev.successMessage != curr.successMessage,
|
||||
listener: (BuildContext context, ClientHubsState state) {
|
||||
if (state.status == ClientHubsStatus.actionSuccess &&
|
||||
state.successMessage != null) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: state.successMessage!,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
// Pop back to details page with updated hub
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
if (state.status == ClientHubsStatus.actionFailure &&
|
||||
state.errorMessage != null) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: state.errorMessage!,
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: BlocBuilder<ClientHubsBloc, ClientHubsState>(
|
||||
builder: (BuildContext context, ClientHubsState state) {
|
||||
final bool isSaving =
|
||||
state.status == ClientHubsStatus.actionInProgress;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: UiColors.bgMenu,
|
||||
appBar: AppBar(
|
||||
backgroundColor: UiColors.foreground,
|
||||
leading: IconButton(
|
||||
icon: const Icon(UiIcons.arrowLeft, color: UiColors.white),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.client_hubs.edit_hub.title,
|
||||
style: UiTypography.headline3m.white,
|
||||
),
|
||||
Text(
|
||||
t.client_hubs.edit_hub.subtitle,
|
||||
style: UiTypography.footnote1r.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: Stack(
|
||||
children: <Widget>[
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
// ── Name field ──────────────────────────────────
|
||||
_FieldLabel(t.client_hubs.edit_hub.name_label),
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
style: UiTypography.body1r.textPrimary,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (String? value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Name is required';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: _inputDecoration(
|
||||
t.client_hubs.edit_hub.name_hint,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// ── Address field ────────────────────────────────
|
||||
_FieldLabel(t.client_hubs.edit_hub.address_label),
|
||||
HubAddressAutocomplete(
|
||||
controller: _addressController,
|
||||
hintText: t.client_hubs.edit_hub.address_hint,
|
||||
focusNode: _addressFocusNode,
|
||||
onSelected: (Prediction prediction) {
|
||||
_selectedPrediction = prediction;
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
|
||||
// ── Save button ──────────────────────────────────
|
||||
UiButton.primary(
|
||||
onPressed: isSaving ? null : _onSave,
|
||||
text: t.client_hubs.edit_hub.save_button,
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── Loading overlay ──────────────────────────────────────
|
||||
if (isSaving)
|
||||
Container(
|
||||
color: UiColors.black.withValues(alpha: 0.1),
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
InputDecoration _inputDecoration(String hint) {
|
||||
return InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: UiTypography.body2r.textPlaceholder,
|
||||
filled: true,
|
||||
fillColor: UiColors.input,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: 14,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
borderSide: const BorderSide(color: UiColors.border),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
borderSide: const BorderSide(color: UiColors.border),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
borderSide: const BorderSide(color: UiColors.ring, width: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FieldLabel extends StatelessWidget {
|
||||
const _FieldLabel(this.text);
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space2),
|
||||
child: Text(text, style: UiTypography.body2m.textPrimary),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../blocs/client_hubs_bloc.dart';
|
||||
import '../blocs/client_hubs_event.dart';
|
||||
import '../widgets/hub_form_dialog.dart';
|
||||
|
||||
import '../blocs/client_hubs_bloc.dart';
|
||||
import 'edit_hub_page.dart';
|
||||
|
||||
/// A read-only details page for a single [Hub].
|
||||
///
|
||||
/// Shows hub name, address, and NFC tag assignment.
|
||||
/// Tapping the edit button navigates to [EditHubPage] (a dedicated page,
|
||||
/// not a dialog), satisfying the "separate edit hub page" acceptance criterion.
|
||||
class HubDetailsPage extends StatelessWidget {
|
||||
const HubDetailsPage({
|
||||
required this.hub,
|
||||
@@ -19,50 +23,51 @@ class HubDetailsPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<ClientHubsBloc>.value(
|
||||
value: bloc,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(hub.name),
|
||||
backgroundColor: UiColors.foreground,
|
||||
leading: IconButton(
|
||||
icon: const Icon(UiIcons.arrowLeft, color: UiColors.white),
|
||||
onPressed: () => Modular.to.pop(),
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(hub.name),
|
||||
backgroundColor: UiColors.foreground,
|
||||
leading: IconButton(
|
||||
icon: const Icon(UiIcons.arrowLeft, color: UiColors.white),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton.icon(
|
||||
onPressed: () => _navigateToEditPage(context),
|
||||
icon: const Icon(UiIcons.edit, color: UiColors.white, size: 16),
|
||||
label: Text(
|
||||
t.client_hubs.hub_details.edit_button,
|
||||
style: const TextStyle(color: UiColors.white),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(UiIcons.edit, color: UiColors.white),
|
||||
onPressed: () => _showEditDialog(context),
|
||||
],
|
||||
),
|
||||
backgroundColor: UiColors.bgMenu,
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
_buildDetailItem(
|
||||
label: t.client_hubs.hub_details.name_label,
|
||||
value: hub.name,
|
||||
icon: UiIcons.home,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
_buildDetailItem(
|
||||
label: t.client_hubs.hub_details.address_label,
|
||||
value: hub.address,
|
||||
icon: UiIcons.mapPin,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
_buildDetailItem(
|
||||
label: t.client_hubs.hub_details.nfc_label,
|
||||
value: hub.nfcTagId ?? t.client_hubs.hub_details.nfc_not_assigned,
|
||||
icon: UiIcons.nfc,
|
||||
isHighlight: hub.nfcTagId != null,
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: UiColors.bgMenu,
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDetailItem(
|
||||
label: 'Name',
|
||||
value: hub.name,
|
||||
icon: UiIcons.home,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
_buildDetailItem(
|
||||
label: 'Address',
|
||||
value: hub.address,
|
||||
icon: UiIcons.mapPin,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
_buildDetailItem(
|
||||
label: 'NFC Tag',
|
||||
value: hub.nfcTagId ?? 'Not Assigned',
|
||||
icon: UiIcons.nfc,
|
||||
isHighlight: hub.nfcTagId != null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -78,7 +83,7 @@ class HubDetailsPage extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
boxShadow: const [
|
||||
boxShadow: const <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.popupShadow,
|
||||
blurRadius: 10,
|
||||
@@ -87,11 +92,11 @@ class HubDetailsPage extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: isHighlight ? UiColors.tagInProgress : UiColors.bgInput,
|
||||
color: isHighlight ? UiColors.tagInProgress : UiColors.bgInputField,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
child: Icon(
|
||||
@@ -104,16 +109,10 @@ class HubDetailsPage extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: UiTypography.footnote1r.textSecondary,
|
||||
),
|
||||
children: <Widget>[
|
||||
Text(label, style: UiTypography.footnote1r.textSecondary),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
value,
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
),
|
||||
Text(value, style: UiTypography.body1m.textPrimary),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -122,33 +121,17 @@ class HubDetailsPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => HubFormDialog(
|
||||
hub: hub,
|
||||
onSave: (name, address, {placeId, latitude, longitude, city, state, street, country, zipCode}) {
|
||||
bloc.add(
|
||||
ClientHubsUpdateRequested(
|
||||
id: hub.id,
|
||||
name: name,
|
||||
address: address,
|
||||
placeId: placeId,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
city: city,
|
||||
state: state,
|
||||
street: street,
|
||||
country: country,
|
||||
zipCode: zipCode,
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pop(); // Close dialog
|
||||
Navigator.of(context).pop(); // Go back to list to refresh
|
||||
},
|
||||
onCancel: () => Navigator.of(context).pop(),
|
||||
Future<void> _navigateToEditPage(BuildContext context) async {
|
||||
// Navigate to the dedicated edit page and await result.
|
||||
// If the page returns `true` (save succeeded), pop the details page too so
|
||||
// the user sees the refreshed hub list (the BLoC already holds updated data).
|
||||
final bool? saved = await Navigator.of(context).push<bool>(
|
||||
MaterialPageRoute<bool>(
|
||||
builder: (_) => EditHubPage(hub: hub, bloc: bloc),
|
||||
),
|
||||
);
|
||||
if (saved == true && context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,14 @@ class _NoShowReportPageState extends State<NoShowReportPage> {
|
||||
bottom: 32,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF1A1A2E),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
UiColors.primary,
|
||||
UiColors.buttonPrimaryHover,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
|
||||
@@ -42,6 +42,7 @@ class ClientSettingsPage extends StatelessWidget {
|
||||
}
|
||||
},
|
||||
child: const Scaffold(
|
||||
backgroundColor: UiColors.bgMenu,
|
||||
body: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SettingsProfileHeader(),
|
||||
|
||||
@@ -14,27 +14,46 @@ class SettingsActions extends StatelessWidget {
|
||||
@override
|
||||
/// Builds the settings actions UI.
|
||||
Widget build(BuildContext context) {
|
||||
// Get the translations for the client settings profile.
|
||||
final TranslationsClientSettingsProfileEn labels =
|
||||
t.client_settings.profile;
|
||||
|
||||
// Yellow button style matching the prototype
|
||||
final ButtonStyle yellowStyle = ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.accent,
|
||||
foregroundColor: UiColors.accentForeground,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
);
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate(<Widget>[
|
||||
const SizedBox(height: UiConstants.space5),
|
||||
|
||||
/// TODO: FEATURE_NOT_YET_IMPLEMENTED
|
||||
// Edit profile is not yet implemented
|
||||
// Edit Profile button (yellow)
|
||||
UiButton.primary(
|
||||
text: labels.edit_profile,
|
||||
style: yellowStyle,
|
||||
onPressed: () {},
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Hubs button
|
||||
// Hubs button (yellow)
|
||||
UiButton.primary(
|
||||
text: labels.hubs,
|
||||
style: yellowStyle,
|
||||
onPressed: () => Modular.to.toClientHubs(),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Log out button
|
||||
// Quick Links card
|
||||
_QuickLinksCard(labels: labels),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Log Out button (outlined)
|
||||
BlocBuilder<ClientSettingsBloc, ClientSettingsState>(
|
||||
builder: (BuildContext context, ClientSettingsState state) {
|
||||
return UiButton.secondary(
|
||||
@@ -45,17 +64,11 @@ class SettingsActions extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Handles the sign-out button click event.
|
||||
void _onSignoutClicked(BuildContext context) {
|
||||
ReadContext(
|
||||
context,
|
||||
).read<ClientSettingsBloc>().add(const ClientSettingsSignOutRequested());
|
||||
}
|
||||
|
||||
/// Shows a confirmation dialog for signing out.
|
||||
Future<void> _showSignOutDialog(BuildContext context) {
|
||||
@@ -74,13 +87,10 @@ class SettingsActions extends StatelessWidget {
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
actions: <Widget>[
|
||||
// Log out button
|
||||
UiButton.secondary(
|
||||
text: t.client_settings.profile.log_out,
|
||||
onPressed: () => _onSignoutClicked(context),
|
||||
),
|
||||
|
||||
// Cancel button
|
||||
UiButton.secondary(
|
||||
text: t.common.cancel,
|
||||
onPressed: () => Modular.to.pop(),
|
||||
@@ -89,4 +99,97 @@ class SettingsActions extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Handles the sign-out button click event.
|
||||
void _onSignoutClicked(BuildContext context) {
|
||||
ReadContext(context)
|
||||
.read<ClientSettingsBloc>()
|
||||
.add(const ClientSettingsSignOutRequested());
|
||||
}
|
||||
}
|
||||
|
||||
/// Quick Links card — inline here since it's always part of SettingsActions ordering.
|
||||
class _QuickLinksCard extends StatelessWidget {
|
||||
final TranslationsClientSettingsProfileEn labels;
|
||||
|
||||
const _QuickLinksCard({required this.labels});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
side: const BorderSide(color: UiColors.border),
|
||||
),
|
||||
color: UiColors.white,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
labels.quick_links,
|
||||
style: UiTypography.footnote1b.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
_QuickLinkItem(
|
||||
icon: UiIcons.nfc,
|
||||
title: labels.clock_in_hubs,
|
||||
onTap: () => Modular.to.toClientHubs(),
|
||||
),
|
||||
_QuickLinkItem(
|
||||
icon: UiIcons.building,
|
||||
title: labels.billing_payments,
|
||||
onTap: () => Modular.to.toClientBilling(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A single quick link row item.
|
||||
class _QuickLinkItem extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _QuickLinkItem({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: UiConstants.space3,
|
||||
horizontal: UiConstants.space2,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Icon(icon, size: 20, color: UiColors.iconSecondary),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Text(title, style: UiTypography.footnote1m.textPrimary),
|
||||
],
|
||||
),
|
||||
const Icon(
|
||||
UiIcons.chevronRight,
|
||||
size: 20,
|
||||
color: UiColors.iconThird,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ class SettingsProfileHeader extends StatelessWidget {
|
||||
const SettingsProfileHeader({super.key});
|
||||
|
||||
@override
|
||||
/// Builds the profile header UI.
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsClientSettingsProfileEn labels = t.client_settings.profile;
|
||||
final dc.ClientSession? session = dc.ClientSessionStore.instance.session;
|
||||
@@ -23,78 +22,115 @@ class SettingsProfileHeader extends StatelessWidget {
|
||||
? businessName.trim()[0].toUpperCase()
|
||||
: 'C';
|
||||
|
||||
return SliverAppBar(
|
||||
backgroundColor: UiColors.bgSecondary,
|
||||
expandedHeight: 140,
|
||||
pinned: true,
|
||||
elevation: 0,
|
||||
shape: const Border(bottom: BorderSide(color: UiColors.border, width: 1)),
|
||||
leading: IconButton(
|
||||
icon: const Icon(UiIcons.chevronLeft, color: UiColors.textSecondary),
|
||||
onPressed: () => Modular.to.toClientHome(),
|
||||
),
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space8),
|
||||
margin: const EdgeInsets.only(top: UiConstants.space24),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
spacing: UiConstants.space4,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: UiColors.border, width: 2),
|
||||
color: UiColors.white,
|
||||
return SliverToBoxAdapter(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.only(bottom: 36),
|
||||
decoration: const BoxDecoration(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
// ── Top bar: back arrow + title ──────────────────
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space2,
|
||||
),
|
||||
child: CircleAvatar(
|
||||
backgroundColor: UiColors.primary.withValues(alpha: 0.1),
|
||||
backgroundImage:
|
||||
photoUrl != null && photoUrl.isNotEmpty
|
||||
? NetworkImage(photoUrl)
|
||||
: null,
|
||||
child:
|
||||
photoUrl != null && photoUrl.isNotEmpty
|
||||
? null
|
||||
: Text(
|
||||
avatarLetter,
|
||||
style: UiTypography.headline1m.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
GestureDetector(
|
||||
onTap: () => Modular.to.toClientHome(),
|
||||
child: const Icon(
|
||||
UiIcons.arrowLeft,
|
||||
color: UiColors.white,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Text(
|
||||
labels.title,
|
||||
style: UiTypography.body1b.copyWith(
|
||||
color: UiColors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(businessName, style: UiTypography.body1b.textPrimary),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
spacing: UiConstants.space1,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
UiIcons.mail,
|
||||
size: 14,
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
Text(
|
||||
email,
|
||||
style: UiTypography.footnote1r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
// ── Avatar ───────────────────────────────────────
|
||||
Container(
|
||||
width: 88,
|
||||
height: 88,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: UiColors.white,
|
||||
border: Border.all(
|
||||
color: UiColors.white.withValues(alpha: 0.6),
|
||||
width: 3,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: UiColors.black.withValues(alpha: 0.15),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipOval(
|
||||
child: photoUrl != null && photoUrl.isNotEmpty
|
||||
? Image.network(photoUrl, fit: BoxFit.cover)
|
||||
: Center(
|
||||
child: Text(
|
||||
avatarLetter,
|
||||
style: UiTypography.headline1m.copyWith(
|
||||
color: UiColors.primary,
|
||||
fontSize: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// ── Business Name ─────────────────────────────────
|
||||
Text(
|
||||
businessName,
|
||||
style: UiTypography.headline3m.copyWith(
|
||||
color: UiColors.white,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
|
||||
// ── Email ─────────────────────────────────────────
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
UiIcons.mail,
|
||||
size: 14,
|
||||
color: UiColors.white.withValues(alpha: 0.75),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
email,
|
||||
style: UiTypography.footnote1r.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.75),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
title: Text(labels.title, style: UiTypography.body1b.textPrimary),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user