finalcommitform4
This commit is contained in:
@@ -239,6 +239,24 @@
|
|||||||
"address_hint": "Full address",
|
"address_hint": "Full address",
|
||||||
"create_button": "Create Hub"
|
"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": {
|
"nfc_dialog": {
|
||||||
"title": "Identify NFC Tag",
|
"title": "Identify NFC Tag",
|
||||||
"instruction": "Tap your phone to the NFC tag to identify it",
|
"instruction": "Tap your phone to the NFC tag to identify it",
|
||||||
@@ -1154,6 +1172,7 @@
|
|||||||
"success": {
|
"success": {
|
||||||
"hub": {
|
"hub": {
|
||||||
"created": "Hub created successfully!",
|
"created": "Hub created successfully!",
|
||||||
|
"updated": "Hub updated successfully!",
|
||||||
"deleted": "Hub deleted successfully!",
|
"deleted": "Hub deleted successfully!",
|
||||||
"nfc_assigned": "NFC tag assigned 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.",
|
"dependency_warning": "Ten en cuenta que si hay turnos/órdenes asignados a este hub no deberíamos poder eliminarlo.",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"delete": "Eliminar"
|
"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": {
|
"client_create_order": {
|
||||||
@@ -1154,6 +1172,7 @@
|
|||||||
"success": {
|
"success": {
|
||||||
"hub": {
|
"hub": {
|
||||||
"created": "¡Hub creado exitosamente!",
|
"created": "¡Hub creado exitosamente!",
|
||||||
|
"updated": "¡Hub actualizado exitosamente!",
|
||||||
"deleted": "¡Hub eliminado exitosamente!",
|
"deleted": "¡Hub eliminado exitosamente!",
|
||||||
"nfc_assigned": "¡Etiqueta NFC asignada exitosamente!"
|
"nfc_assigned": "¡Etiqueta NFC asignada exitosamente!"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,23 +1,13 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
<<<<<<< Updated upstream
|
|
||||||
import 'recurring_order_position.dart';
|
import 'recurring_order_position.dart';
|
||||||
|
|
||||||
/// Represents a recurring staffing request spanning a date range.
|
/// 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 {
|
class RecurringOrder extends Equatable {
|
||||||
const RecurringOrder({
|
const RecurringOrder({
|
||||||
required this.startDate,
|
required this.startDate,
|
||||||
required this.endDate,
|
required this.endDate,
|
||||||
required this.recurringDays,
|
required this.recurringDays,
|
||||||
<<<<<<< Updated upstream
|
|
||||||
required this.location,
|
required this.location,
|
||||||
=======
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
required this.positions,
|
required this.positions,
|
||||||
this.hub,
|
this.hub,
|
||||||
this.eventName,
|
this.eventName,
|
||||||
@@ -25,7 +15,6 @@ class RecurringOrder extends Equatable {
|
|||||||
this.roleRates = const <String, double>{},
|
this.roleRates = const <String, double>{},
|
||||||
});
|
});
|
||||||
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
/// Start date for the recurring schedule.
|
/// Start date for the recurring schedule.
|
||||||
final DateTime startDate;
|
final DateTime startDate;
|
||||||
|
|
||||||
@@ -59,25 +48,6 @@ class RecurringOrder extends Equatable {
|
|||||||
endDate,
|
endDate,
|
||||||
recurringDays,
|
recurringDays,
|
||||||
location,
|
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,
|
positions,
|
||||||
hub,
|
hub,
|
||||||
eventName,
|
eventName,
|
||||||
@@ -85,7 +55,6 @@ class RecurringOrder extends Equatable {
|
|||||||
roleRates,
|
roleRates,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
<<<<<<< Updated upstream
|
|
||||||
|
|
||||||
/// Minimal hub details used during recurring order creation.
|
/// Minimal hub details used during recurring order creation.
|
||||||
class RecurringOrderHubDetails extends Equatable {
|
class RecurringOrderHubDetails extends Equatable {
|
||||||
@@ -130,5 +99,3 @@ class RecurringOrderHubDetails extends Equatable {
|
|||||||
zipCode,
|
zipCode,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
=======
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
.date(orderTimestamp)
|
.date(orderTimestamp)
|
||||||
.startDate(startTimestamp)
|
.startDate(startTimestamp)
|
||||||
.endDate(endTimestamp)
|
.endDate(endTimestamp)
|
||||||
.recurringDays(fdc.AnyValue(order.recurringDays))
|
.recurringDays(order.recurringDays)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
final String orderId = orderResult.data.order_insert.id;
|
final String orderId = orderResult.data.order_insert.id;
|
||||||
@@ -274,7 +274,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
if (vendorId == null || vendorId.isEmpty) {
|
if (vendorId == null || vendorId.isEmpty) {
|
||||||
throw Exception('Vendor is missing.');
|
throw Exception('Vendor is missing.');
|
||||||
}
|
}
|
||||||
final domain.PermanentOrderHubDetails? hub = order.hub;
|
final domain.OneTimeOrderHubDetails? hub = order.hub;
|
||||||
if (hub == null || hub.id.isEmpty) {
|
if (hub == null || hub.id.isEmpty) {
|
||||||
throw Exception('Hub is missing.');
|
throw Exception('Hub is missing.');
|
||||||
}
|
}
|
||||||
@@ -299,7 +299,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
.status(dc.OrderStatus.POSTED)
|
.status(dc.OrderStatus.POSTED)
|
||||||
.date(orderTimestamp)
|
.date(orderTimestamp)
|
||||||
.startDate(startTimestamp)
|
.startDate(startTimestamp)
|
||||||
.permanentDays(fdc.AnyValue(order.permanentDays))
|
.permanentDays(order.permanentDays)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
final String orderId = orderResult.data.order_insert.id;
|
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 Set<String> selectedDays = Set<String>.from(order.permanentDays);
|
||||||
final int workersNeeded = order.positions.fold<int>(
|
final int workersNeeded = order.positions.fold<int>(
|
||||||
0,
|
0,
|
||||||
(int sum, domain.PermanentOrderPosition position) => sum + position.count,
|
(int sum, domain.OneTimeOrderPosition position) => sum + position.count,
|
||||||
);
|
);
|
||||||
final double shiftCost = _calculatePermanentShiftCost(order);
|
final double shiftCost = _calculatePermanentShiftCost(order);
|
||||||
|
|
||||||
@@ -352,7 +352,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
final String shiftId = shiftResult.data.shift_insert.id;
|
final String shiftId = shiftResult.data.shift_insert.id;
|
||||||
shiftIds.add(shiftId);
|
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 start = _parseTime(day, position.startTime);
|
||||||
final DateTime end = _parseTime(day, position.endTime);
|
final DateTime end = _parseTime(day, position.endTime);
|
||||||
final DateTime normalizedEnd =
|
final DateTime normalizedEnd =
|
||||||
@@ -420,7 +420,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
|
|
||||||
double _calculatePermanentShiftCost(domain.PermanentOrder order) {
|
double _calculatePermanentShiftCost(domain.PermanentOrder order) {
|
||||||
double total = 0;
|
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 start = _parseTime(order.startDate, position.startTime);
|
||||||
final DateTime end = _parseTime(order.startDate, position.endTime);
|
final DateTime end = _parseTime(order.startDate, position.endTime);
|
||||||
final DateTime normalizedEnd =
|
final DateTime normalizedEnd =
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
import '../repositories/client_create_order_repository_interface.dart';
|
import '../repositories/client_create_order_repository_interface.dart';
|
||||||
|
|
||||||
/// Use case for creating a permanent staffing order.
|
/// 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);
|
const CreatePermanentOrderUseCase(this._repository);
|
||||||
|
|
||||||
final ClientCreateOrderRepositoryInterface _repository;
|
final ClientCreateOrderRepositoryInterface _repository;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
import '../repositories/client_create_order_repository_interface.dart';
|
import '../repositories/client_create_order_repository_interface.dart';
|
||||||
|
|
||||||
/// Use case for creating a recurring staffing order.
|
/// 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);
|
const CreateRecurringOrderUseCase(this._repository);
|
||||||
|
|
||||||
final ClientCreateOrderRepositoryInterface _repository;
|
final ClientCreateOrderRepositoryInterface _repository;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||||
import '../../domain/arguments/permanent_order_arguments.dart';
|
|
||||||
import '../../domain/usecases/create_permanent_order_usecase.dart';
|
import '../../domain/usecases/create_permanent_order_usecase.dart';
|
||||||
import 'permanent_order_event.dart';
|
import 'permanent_order_event.dart';
|
||||||
import 'permanent_order_state.dart';
|
import 'permanent_order_state.dart';
|
||||||
@@ -286,10 +285,9 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
|||||||
final domain.PermanentOrder order = domain.PermanentOrder(
|
final domain.PermanentOrder order = domain.PermanentOrder(
|
||||||
startDate: state.startDate,
|
startDate: state.startDate,
|
||||||
permanentDays: state.permanentDays,
|
permanentDays: state.permanentDays,
|
||||||
location: selectedHub.name,
|
|
||||||
positions: state.positions
|
positions: state.positions
|
||||||
.map(
|
.map(
|
||||||
(PermanentOrderPosition p) => domain.PermanentOrderPosition(
|
(PermanentOrderPosition p) => domain.OneTimeOrderPosition(
|
||||||
role: p.role,
|
role: p.role,
|
||||||
count: p.count,
|
count: p.count,
|
||||||
startTime: p.startTime,
|
startTime: p.startTime,
|
||||||
@@ -299,7 +297,7 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
hub: domain.PermanentOrderHubDetails(
|
hub: domain.OneTimeOrderHubDetails(
|
||||||
id: selectedHub.id,
|
id: selectedHub.id,
|
||||||
name: selectedHub.name,
|
name: selectedHub.name,
|
||||||
address: selectedHub.address,
|
address: selectedHub.address,
|
||||||
@@ -316,9 +314,7 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
|||||||
vendorId: state.selectedVendor?.id,
|
vendorId: state.selectedVendor?.id,
|
||||||
roleRates: roleRates,
|
roleRates: roleRates,
|
||||||
);
|
);
|
||||||
await _createPermanentOrderUseCase(
|
await _createPermanentOrderUseCase(order);
|
||||||
PermanentOrderArguments(order: order),
|
|
||||||
);
|
|
||||||
emit(state.copyWith(status: PermanentOrderStatus.success));
|
emit(state.copyWith(status: PermanentOrderStatus.success));
|
||||||
},
|
},
|
||||||
onError: (String errorKey) => state.copyWith(
|
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_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||||
import '../../domain/arguments/recurring_order_arguments.dart';
|
|
||||||
import '../../domain/usecases/create_recurring_order_usecase.dart';
|
import '../../domain/usecases/create_recurring_order_usecase.dart';
|
||||||
import 'recurring_order_event.dart';
|
import 'recurring_order_event.dart';
|
||||||
import 'recurring_order_state.dart';
|
import 'recurring_order_state.dart';
|
||||||
@@ -334,9 +333,7 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
|||||||
vendorId: state.selectedVendor?.id,
|
vendorId: state.selectedVendor?.id,
|
||||||
roleRates: roleRates,
|
roleRates: roleRates,
|
||||||
);
|
);
|
||||||
await _createRecurringOrderUseCase(
|
await _createRecurringOrderUseCase(order);
|
||||||
RecurringOrderArguments(order: order),
|
|
||||||
);
|
|
||||||
emit(state.copyWith(status: RecurringOrderStatus.success));
|
emit(state.copyWith(status: RecurringOrderStatus.success));
|
||||||
},
|
},
|
||||||
onError: (String errorKey) => state.copyWith(
|
onError: (String errorKey) => state.copyWith(
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class ClientHomeEditBanner extends StatelessWidget {
|
|||||||
builder: (BuildContext context, ClientHomeState state) {
|
builder: (BuildContext context, ClientHomeState state) {
|
||||||
return AnimatedContainer(
|
return AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
height: state.isEditMode ? 76 : 0,
|
height: state.isEditMode ? 80 : 0,
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
margin: const EdgeInsets.symmetric(
|
margin: const EdgeInsets.symmetric(
|
||||||
horizontal: UiConstants.space4,
|
horizontal: UiConstants.space4,
|
||||||
@@ -43,21 +43,23 @@ class ClientHomeEditBanner extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const Icon(UiIcons.edit, size: 16, color: UiColors.primary),
|
const Icon(UiIcons.edit, size: 16, color: UiColors.primary),
|
||||||
const SizedBox(width: UiConstants.space2),
|
const SizedBox(width: UiConstants.space2),
|
||||||
Column(
|
Expanded(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
Text(
|
children: <Widget>[
|
||||||
i18n.dashboard.edit_mode_active,
|
Text(
|
||||||
style: UiTypography.footnote1b.copyWith(
|
i18n.dashboard.edit_mode_active,
|
||||||
color: UiColors.primary,
|
style: UiTypography.footnote1b.copyWith(
|
||||||
|
color: UiColors.primary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Text(
|
||||||
Text(
|
i18n.dashboard.drag_instruction,
|
||||||
i18n.dashboard.drag_instruction,
|
style: UiTypography.footnote2r.textSecondary,
|
||||||
style: UiTypography.footnote2r.textSecondary,
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
UiButton.secondary(
|
UiButton.secondary(
|
||||||
text: i18n.dashboard.reset,
|
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/create_hub_usecase.dart';
|
||||||
import 'src/domain/usecases/delete_hub_usecase.dart';
|
import 'src/domain/usecases/delete_hub_usecase.dart';
|
||||||
import 'src/domain/usecases/get_hubs_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/blocs/client_hubs_bloc.dart';
|
||||||
import 'src/presentation/pages/client_hubs_page.dart';
|
import 'src/presentation/pages/client_hubs_page.dart';
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ class ClientHubsModule extends Module {
|
|||||||
i.addLazySingleton(CreateHubUseCase.new);
|
i.addLazySingleton(CreateHubUseCase.new);
|
||||||
i.addLazySingleton(DeleteHubUseCase.new);
|
i.addLazySingleton(DeleteHubUseCase.new);
|
||||||
i.addLazySingleton(AssignNfcTagUseCase.new);
|
i.addLazySingleton(AssignNfcTagUseCase.new);
|
||||||
|
i.addLazySingleton(UpdateHubUseCase.new);
|
||||||
|
|
||||||
// BLoCs
|
// BLoCs
|
||||||
i.add<ClientHubsBloc>(ClientHubsBloc.new);
|
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>
|
Future<dc.GetBusinessesByUserIdBusinesses>
|
||||||
_getBusinessForCurrentUser() async {
|
_getBusinessForCurrentUser() async {
|
||||||
final dc.ClientSession? session = dc.ClientSessionStore.instance.session;
|
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.
|
/// Takes the [hubId] and the [nfcTagId] to be associated.
|
||||||
Future<void> assignNfcTag({required String hubId, required String nfcTagId});
|
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 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
import '../repositories/hub_repository_interface.dart';
|
import '../repositories/hub_repository_interface.dart';
|
||||||
import '../../domain/arguments/create_hub_arguments.dart';
|
|
||||||
|
|
||||||
/// Arguments for the UpdateHubUseCase.
|
/// Arguments for the UpdateHubUseCase.
|
||||||
class UpdateHubArguments {
|
class UpdateHubArguments extends UseCaseArgument {
|
||||||
const UpdateHubArguments({
|
const UpdateHubArguments({
|
||||||
required this.id,
|
required this.id,
|
||||||
this.name,
|
this.name,
|
||||||
@@ -30,10 +30,25 @@ class UpdateHubArguments {
|
|||||||
final String? street;
|
final String? street;
|
||||||
final String? country;
|
final String? country;
|
||||||
final String? zipCode;
|
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.
|
/// Use case for updating an existing hub.
|
||||||
class UpdateHubUseCase implements UseCase<Future<Hub>, UpdateHubArguments> {
|
class UpdateHubUseCase implements UseCase<UpdateHubArguments, Hub> {
|
||||||
UpdateHubUseCase(this.repository);
|
UpdateHubUseCase(this.repository);
|
||||||
|
|
||||||
final HubRepositoryInterface 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/create_hub_usecase.dart';
|
||||||
import '../../domain/usecases/delete_hub_usecase.dart';
|
import '../../domain/usecases/delete_hub_usecase.dart';
|
||||||
import '../../domain/usecases/get_hubs_usecase.dart';
|
import '../../domain/usecases/get_hubs_usecase.dart';
|
||||||
|
import '../../domain/usecases/update_hub_usecase.dart';
|
||||||
import 'client_hubs_event.dart';
|
import 'client_hubs_event.dart';
|
||||||
import 'client_hubs_state.dart';
|
import 'client_hubs_state.dart';
|
||||||
|
|
||||||
@@ -25,13 +26,16 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
|||||||
required CreateHubUseCase createHubUseCase,
|
required CreateHubUseCase createHubUseCase,
|
||||||
required DeleteHubUseCase deleteHubUseCase,
|
required DeleteHubUseCase deleteHubUseCase,
|
||||||
required AssignNfcTagUseCase assignNfcTagUseCase,
|
required AssignNfcTagUseCase assignNfcTagUseCase,
|
||||||
|
required UpdateHubUseCase updateHubUseCase,
|
||||||
}) : _getHubsUseCase = getHubsUseCase,
|
}) : _getHubsUseCase = getHubsUseCase,
|
||||||
_createHubUseCase = createHubUseCase,
|
_createHubUseCase = createHubUseCase,
|
||||||
_deleteHubUseCase = deleteHubUseCase,
|
_deleteHubUseCase = deleteHubUseCase,
|
||||||
_assignNfcTagUseCase = assignNfcTagUseCase,
|
_assignNfcTagUseCase = assignNfcTagUseCase,
|
||||||
|
_updateHubUseCase = updateHubUseCase,
|
||||||
super(const ClientHubsState()) {
|
super(const ClientHubsState()) {
|
||||||
on<ClientHubsFetched>(_onFetched);
|
on<ClientHubsFetched>(_onFetched);
|
||||||
on<ClientHubsAddRequested>(_onAddRequested);
|
on<ClientHubsAddRequested>(_onAddRequested);
|
||||||
|
on<ClientHubsUpdateRequested>(_onUpdateRequested);
|
||||||
on<ClientHubsDeleteRequested>(_onDeleteRequested);
|
on<ClientHubsDeleteRequested>(_onDeleteRequested);
|
||||||
on<ClientHubsNfcTagAssignRequested>(_onNfcTagAssignRequested);
|
on<ClientHubsNfcTagAssignRequested>(_onNfcTagAssignRequested);
|
||||||
on<ClientHubsMessageCleared>(_onMessageCleared);
|
on<ClientHubsMessageCleared>(_onMessageCleared);
|
||||||
@@ -42,6 +46,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
|||||||
final CreateHubUseCase _createHubUseCase;
|
final CreateHubUseCase _createHubUseCase;
|
||||||
final DeleteHubUseCase _deleteHubUseCase;
|
final DeleteHubUseCase _deleteHubUseCase;
|
||||||
final AssignNfcTagUseCase _assignNfcTagUseCase;
|
final AssignNfcTagUseCase _assignNfcTagUseCase;
|
||||||
|
final UpdateHubUseCase _updateHubUseCase;
|
||||||
|
|
||||||
void _onAddDialogToggled(
|
void _onAddDialogToggled(
|
||||||
ClientHubsAddDialogToggled event,
|
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(
|
Future<void> _onDeleteRequested(
|
||||||
ClientHubsDeleteRequested event,
|
ClientHubsDeleteRequested event,
|
||||||
Emitter<ClientHubsState> emit,
|
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.
|
/// Event triggered to delete a hub.
|
||||||
class ClientHubsDeleteRequested extends ClientHubsEvent {
|
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:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
|
||||||
import 'package:krow_domain/krow_domain.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 {
|
class HubDetailsPage extends StatelessWidget {
|
||||||
const HubDetailsPage({
|
const HubDetailsPage({
|
||||||
required this.hub,
|
required this.hub,
|
||||||
@@ -19,50 +23,51 @@ class HubDetailsPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<ClientHubsBloc>.value(
|
return Scaffold(
|
||||||
value: bloc,
|
appBar: AppBar(
|
||||||
child: Scaffold(
|
title: Text(hub.name),
|
||||||
appBar: AppBar(
|
backgroundColor: UiColors.foreground,
|
||||||
title: Text(hub.name),
|
leading: IconButton(
|
||||||
backgroundColor: UiColors.foreground,
|
icon: const Icon(UiIcons.arrowLeft, color: UiColors.white),
|
||||||
leading: IconButton(
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
icon: const Icon(UiIcons.arrowLeft, color: UiColors.white),
|
),
|
||||||
onPressed: () => Modular.to.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),
|
backgroundColor: UiColors.bgMenu,
|
||||||
onPressed: () => _showEditDialog(context),
|
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(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.white,
|
color: UiColors.white,
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
boxShadow: const [
|
boxShadow: const <BoxShadow>[
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: UiColors.popupShadow,
|
color: UiColors.popupShadow,
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
@@ -87,11 +92,11 @@ class HubDetailsPage extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: <Widget>[
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(UiConstants.space3),
|
padding: const EdgeInsets.all(UiConstants.space3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isHighlight ? UiColors.tagInProgress : UiColors.bgInput,
|
color: isHighlight ? UiColors.tagInProgress : UiColors.bgInputField,
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
@@ -104,16 +109,10 @@ class HubDetailsPage extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: <Widget>[
|
||||||
Text(
|
Text(label, style: UiTypography.footnote1r.textSecondary),
|
||||||
label,
|
|
||||||
style: UiTypography.footnote1r.textSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space1),
|
const SizedBox(height: UiConstants.space1),
|
||||||
Text(
|
Text(value, style: UiTypography.body1m.textPrimary),
|
||||||
value,
|
|
||||||
style: UiTypography.body1m.textPrimary,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -122,33 +121,17 @@ class HubDetailsPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showEditDialog(BuildContext context) {
|
Future<void> _navigateToEditPage(BuildContext context) async {
|
||||||
showDialog(
|
// Navigate to the dedicated edit page and await result.
|
||||||
context: context,
|
// If the page returns `true` (save succeeded), pop the details page too so
|
||||||
barrierDismissible: false,
|
// the user sees the refreshed hub list (the BLoC already holds updated data).
|
||||||
builder: (_) => HubFormDialog(
|
final bool? saved = await Navigator.of(context).push<bool>(
|
||||||
hub: hub,
|
MaterialPageRoute<bool>(
|
||||||
onSave: (name, address, {placeId, latitude, longitude, city, state, street, country, zipCode}) {
|
builder: (_) => EditHubPage(hub: hub, bloc: bloc),
|
||||||
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(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (saved == true && context.mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,14 @@ class _NoShowReportPageState extends State<NoShowReportPage> {
|
|||||||
bottom: 32,
|
bottom: 32,
|
||||||
),
|
),
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Color(0xFF1A1A2E),
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
UiColors.primary,
|
||||||
|
UiColors.buttonPrimaryHover,
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class ClientSettingsPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Scaffold(
|
child: const Scaffold(
|
||||||
|
backgroundColor: UiColors.bgMenu,
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
slivers: <Widget>[
|
slivers: <Widget>[
|
||||||
SettingsProfileHeader(),
|
SettingsProfileHeader(),
|
||||||
|
|||||||
@@ -14,27 +14,46 @@ class SettingsActions extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
/// Builds the settings actions UI.
|
/// Builds the settings actions UI.
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Get the translations for the client settings profile.
|
|
||||||
final TranslationsClientSettingsProfileEn labels =
|
final TranslationsClientSettingsProfileEn labels =
|
||||||
t.client_settings.profile;
|
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(
|
return SliverPadding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
|
||||||
sliver: SliverList(
|
sliver: SliverList(
|
||||||
delegate: SliverChildListDelegate(<Widget>[
|
delegate: SliverChildListDelegate(<Widget>[
|
||||||
const SizedBox(height: UiConstants.space5),
|
const SizedBox(height: UiConstants.space5),
|
||||||
|
|
||||||
/// TODO: FEATURE_NOT_YET_IMPLEMENTED
|
// Edit Profile button (yellow)
|
||||||
// Edit profile is not yet implemented
|
UiButton.primary(
|
||||||
|
text: labels.edit_profile,
|
||||||
|
style: yellowStyle,
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
// Hubs button
|
// Hubs button (yellow)
|
||||||
UiButton.primary(
|
UiButton.primary(
|
||||||
text: labels.hubs,
|
text: labels.hubs,
|
||||||
|
style: yellowStyle,
|
||||||
onPressed: () => Modular.to.toClientHubs(),
|
onPressed: () => Modular.to.toClientHubs(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space4),
|
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>(
|
BlocBuilder<ClientSettingsBloc, ClientSettingsState>(
|
||||||
builder: (BuildContext context, ClientSettingsState state) {
|
builder: (BuildContext context, ClientSettingsState state) {
|
||||||
return UiButton.secondary(
|
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.
|
/// Shows a confirmation dialog for signing out.
|
||||||
Future<void> _showSignOutDialog(BuildContext context) {
|
Future<void> _showSignOutDialog(BuildContext context) {
|
||||||
@@ -74,13 +87,10 @@ class SettingsActions extends StatelessWidget {
|
|||||||
style: UiTypography.body2r.textSecondary,
|
style: UiTypography.body2r.textSecondary,
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
// Log out button
|
|
||||||
UiButton.secondary(
|
UiButton.secondary(
|
||||||
text: t.client_settings.profile.log_out,
|
text: t.client_settings.profile.log_out,
|
||||||
onPressed: () => _onSignoutClicked(context),
|
onPressed: () => _onSignoutClicked(context),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Cancel button
|
|
||||||
UiButton.secondary(
|
UiButton.secondary(
|
||||||
text: t.common.cancel,
|
text: t.common.cancel,
|
||||||
onPressed: () => Modular.to.pop(),
|
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});
|
const SettingsProfileHeader({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
/// Builds the profile header UI.
|
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final TranslationsClientSettingsProfileEn labels = t.client_settings.profile;
|
final TranslationsClientSettingsProfileEn labels = t.client_settings.profile;
|
||||||
final dc.ClientSession? session = dc.ClientSessionStore.instance.session;
|
final dc.ClientSession? session = dc.ClientSessionStore.instance.session;
|
||||||
@@ -23,78 +22,115 @@ class SettingsProfileHeader extends StatelessWidget {
|
|||||||
? businessName.trim()[0].toUpperCase()
|
? businessName.trim()[0].toUpperCase()
|
||||||
: 'C';
|
: 'C';
|
||||||
|
|
||||||
return SliverAppBar(
|
return SliverToBoxAdapter(
|
||||||
backgroundColor: UiColors.bgSecondary,
|
child: Container(
|
||||||
expandedHeight: 140,
|
width: double.infinity,
|
||||||
pinned: true,
|
padding: const EdgeInsets.only(bottom: 36),
|
||||||
elevation: 0,
|
decoration: const BoxDecoration(
|
||||||
shape: const Border(bottom: BorderSide(color: UiColors.border, width: 1)),
|
color: UiColors.primary,
|
||||||
leading: IconButton(
|
),
|
||||||
icon: const Icon(UiIcons.chevronLeft, color: UiColors.textSecondary),
|
child: Column(
|
||||||
onPressed: () => Modular.to.toClientHome(),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
),
|
children: <Widget>[
|
||||||
flexibleSpace: FlexibleSpaceBar(
|
// ── Top bar: back arrow + title ──────────────────
|
||||||
background: Container(
|
SafeArea(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space8),
|
bottom: false,
|
||||||
margin: const EdgeInsets.only(top: UiConstants.space24),
|
child: Padding(
|
||||||
child: Row(
|
padding: const EdgeInsets.symmetric(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
horizontal: UiConstants.space4,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
vertical: UiConstants.space2,
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
child: CircleAvatar(
|
child: Row(
|
||||||
backgroundColor: UiColors.primary.withValues(alpha: 0.1),
|
children: <Widget>[
|
||||||
backgroundImage:
|
GestureDetector(
|
||||||
photoUrl != null && photoUrl.isNotEmpty
|
onTap: () => Modular.to.toClientHome(),
|
||||||
? NetworkImage(photoUrl)
|
child: const Icon(
|
||||||
: null,
|
UiIcons.arrowLeft,
|
||||||
child:
|
color: UiColors.white,
|
||||||
photoUrl != null && photoUrl.isNotEmpty
|
size: 22,
|
||||||
? null
|
),
|
||||||
: Text(
|
),
|
||||||
avatarLetter,
|
const SizedBox(width: UiConstants.space3),
|
||||||
style: UiTypography.headline1m.copyWith(
|
Text(
|
||||||
color: UiColors.primary,
|
labels.title,
|
||||||
),
|
style: UiTypography.body1b.copyWith(
|
||||||
),
|
color: UiColors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
const SizedBox(height: UiConstants.space6),
|
||||||
children: <Widget>[
|
|
||||||
Text(businessName, style: UiTypography.body1b.textPrimary),
|
// ── Avatar ───────────────────────────────────────
|
||||||
const SizedBox(height: UiConstants.space1),
|
Container(
|
||||||
Row(
|
width: 88,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
height: 88,
|
||||||
spacing: UiConstants.space1,
|
decoration: BoxDecoration(
|
||||||
children: <Widget>[
|
shape: BoxShape.circle,
|
||||||
Icon(
|
color: UiColors.white,
|
||||||
UiIcons.mail,
|
border: Border.all(
|
||||||
size: 14,
|
color: UiColors.white.withValues(alpha: 0.6),
|
||||||
color: UiColors.textSecondary,
|
width: 3,
|
||||||
),
|
),
|
||||||
Text(
|
boxShadow: [
|
||||||
email,
|
BoxShadow(
|
||||||
style: UiTypography.footnote1r.textSecondary,
|
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