Merge 592-migrate-frontend-applications-to-v2-backend-and-database into feature/session-persistence-new
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'src/data/repositories_impl/home_repository_impl.dart';
|
||||
import 'src/domain/repositories/home_repository_interface.dart';
|
||||
import 'src/domain/usecases/get_dashboard_data_usecase.dart';
|
||||
import 'src/domain/usecases/get_recent_reorders_usecase.dart';
|
||||
import 'src/domain/usecases/get_user_session_data_usecase.dart';
|
||||
import 'src/presentation/blocs/client_home_bloc.dart';
|
||||
import 'src/presentation/pages/client_home_page.dart';
|
||||
|
||||
@@ -14,24 +13,34 @@ export 'src/presentation/pages/client_home_page.dart';
|
||||
|
||||
/// A [Module] for the client home feature.
|
||||
///
|
||||
/// This module configures the dependencies for the client home feature,
|
||||
/// including repositories, use cases, and BLoCs.
|
||||
/// Imports [CoreModule] for [BaseApiService] and registers repositories,
|
||||
/// use cases, and BLoCs for the client dashboard.
|
||||
class ClientHomeModule extends Module {
|
||||
@override
|
||||
List<Module> get imports => <Module>[DataConnectModule()];
|
||||
List<Module> get imports => <Module>[CoreModule()];
|
||||
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repositories
|
||||
i.addLazySingleton<HomeRepositoryInterface>(HomeRepositoryImpl.new);
|
||||
i.addLazySingleton<HomeRepositoryInterface>(
|
||||
() => HomeRepositoryImpl(apiService: i.get<BaseApiService>()),
|
||||
);
|
||||
|
||||
// UseCases
|
||||
i.addLazySingleton(GetDashboardDataUseCase.new);
|
||||
i.addLazySingleton(GetRecentReordersUseCase.new);
|
||||
i.addLazySingleton(GetUserSessionDataUseCase.new);
|
||||
i.addLazySingleton(
|
||||
() => GetDashboardDataUseCase(i.get<HomeRepositoryInterface>()),
|
||||
);
|
||||
i.addLazySingleton(
|
||||
() => GetRecentReordersUseCase(i.get<HomeRepositoryInterface>()),
|
||||
);
|
||||
|
||||
// BLoCs
|
||||
i.add<ClientHomeBloc>(ClientHomeBloc.new);
|
||||
i.add<ClientHomeBloc>(
|
||||
() => ClientHomeBloc(
|
||||
getDashboardDataUseCase: i.get<GetDashboardDataUseCase>(),
|
||||
getRecentReordersUseCase: i.get<GetRecentReordersUseCase>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,198 +1,37 @@
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/repositories/home_repository_interface.dart';
|
||||
|
||||
/// Implementation of [HomeRepositoryInterface] that directly interacts with the Data Connect SDK.
|
||||
import 'package:client_home/src/domain/repositories/home_repository_interface.dart';
|
||||
|
||||
/// V2 API implementation of [HomeRepositoryInterface].
|
||||
///
|
||||
/// Fetches client dashboard data from `GET /client/dashboard` and recent
|
||||
/// reorders from `GET /client/reorders`.
|
||||
class HomeRepositoryImpl implements HomeRepositoryInterface {
|
||||
HomeRepositoryImpl({dc.DataConnectService? service})
|
||||
: _service = service ?? dc.DataConnectService.instance;
|
||||
/// Creates a [HomeRepositoryImpl].
|
||||
HomeRepositoryImpl({required BaseApiService apiService})
|
||||
: _apiService = apiService;
|
||||
|
||||
final dc.DataConnectService _service;
|
||||
/// The API service used for network requests.
|
||||
final BaseApiService _apiService;
|
||||
|
||||
@override
|
||||
Future<HomeDashboardData> getDashboardData() async {
|
||||
return _service.run(() async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
final DateTime now = DateTime.now();
|
||||
final int daysFromMonday = now.weekday - DateTime.monday;
|
||||
final DateTime monday = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
).subtract(Duration(days: daysFromMonday));
|
||||
final DateTime weekRangeStart = monday;
|
||||
final DateTime weekRangeEnd = monday.add(
|
||||
const Duration(days: 13, hours: 23, minutes: 59, seconds: 59),
|
||||
);
|
||||
|
||||
final QueryResult<
|
||||
dc.GetCompletedShiftsByBusinessIdData,
|
||||
dc.GetCompletedShiftsByBusinessIdVariables
|
||||
>
|
||||
completedResult = await _service.connector
|
||||
.getCompletedShiftsByBusinessId(
|
||||
businessId: businessId,
|
||||
dateFrom: _service.toTimestamp(weekRangeStart),
|
||||
dateTo: _service.toTimestamp(weekRangeEnd),
|
||||
)
|
||||
.execute();
|
||||
|
||||
double weeklySpending = 0.0;
|
||||
double next7DaysSpending = 0.0;
|
||||
int weeklyShifts = 0;
|
||||
int next7DaysScheduled = 0;
|
||||
|
||||
for (final dc.GetCompletedShiftsByBusinessIdShifts shift
|
||||
in completedResult.data.shifts) {
|
||||
final DateTime? shiftDate = _service.toDateTime(shift.date);
|
||||
if (shiftDate == null) continue;
|
||||
|
||||
final int offset = shiftDate.difference(weekRangeStart).inDays;
|
||||
if (offset < 0 || offset > 13) continue;
|
||||
|
||||
final double cost = shift.cost ?? 0.0;
|
||||
if (offset <= 6) {
|
||||
weeklySpending += cost;
|
||||
weeklyShifts += 1;
|
||||
} else {
|
||||
next7DaysSpending += cost;
|
||||
next7DaysScheduled += 1;
|
||||
}
|
||||
}
|
||||
|
||||
final DateTime start = DateTime(now.year, now.month, now.day);
|
||||
final DateTime end = start.add(
|
||||
const Duration(hours: 23, minutes: 59, seconds: 59),
|
||||
);
|
||||
|
||||
final QueryResult<
|
||||
dc.ListShiftRolesByBusinessAndDateRangeData,
|
||||
dc.ListShiftRolesByBusinessAndDateRangeVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listShiftRolesByBusinessAndDateRange(
|
||||
businessId: businessId,
|
||||
start: _service.toTimestamp(start),
|
||||
end: _service.toTimestamp(end),
|
||||
)
|
||||
.execute();
|
||||
|
||||
int totalNeeded = 0;
|
||||
int totalFilled = 0;
|
||||
for (final dc.ListShiftRolesByBusinessAndDateRangeShiftRoles shiftRole
|
||||
in result.data.shiftRoles) {
|
||||
totalNeeded += shiftRole.count;
|
||||
totalFilled += shiftRole.assigned ?? 0;
|
||||
}
|
||||
|
||||
return HomeDashboardData(
|
||||
weeklySpending: weeklySpending,
|
||||
next7DaysSpending: next7DaysSpending,
|
||||
weeklyShifts: weeklyShifts,
|
||||
next7DaysScheduled: next7DaysScheduled,
|
||||
totalNeeded: totalNeeded,
|
||||
totalFilled: totalFilled,
|
||||
);
|
||||
});
|
||||
Future<ClientDashboard> getDashboard() async {
|
||||
final ApiResponse response =
|
||||
await _apiService.get(ClientEndpoints.dashboard);
|
||||
final Map<String, dynamic> data = response.data as Map<String, dynamic>;
|
||||
return ClientDashboard.fromJson(data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UserSessionData> getUserSessionData() async {
|
||||
return await _service.run(() async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
final QueryResult<dc.GetBusinessByIdData, dc.GetBusinessByIdVariables>
|
||||
businessResult = await _service.connector
|
||||
.getBusinessById(id: businessId)
|
||||
.execute();
|
||||
|
||||
final dc.GetBusinessByIdBusiness? b = businessResult.data.business;
|
||||
if (b == null) {
|
||||
throw Exception('Business data not found for ID: $businessId');
|
||||
}
|
||||
|
||||
final dc.ClientSession updatedSession = dc.ClientSession(
|
||||
business: dc.ClientBusinessSession(
|
||||
id: b.id,
|
||||
businessName: b.businessName,
|
||||
email: b.email ?? '',
|
||||
city: b.city ?? '',
|
||||
contactName: b.contactName ?? '',
|
||||
companyLogoUrl: b.companyLogoUrl,
|
||||
),
|
||||
);
|
||||
dc.ClientSessionStore.instance.setSession(updatedSession);
|
||||
|
||||
return UserSessionData(
|
||||
businessName: b.businessName,
|
||||
photoUrl: b.companyLogoUrl,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ReorderItem>> getRecentReorders() async {
|
||||
return _service.run(() async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
final DateTime now = DateTime.now();
|
||||
final DateTime start = now.subtract(const Duration(days: 30));
|
||||
|
||||
final QueryResult<
|
||||
dc.ListCompletedOrdersByBusinessAndDateRangeData,
|
||||
dc.ListCompletedOrdersByBusinessAndDateRangeVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listCompletedOrdersByBusinessAndDateRange(
|
||||
businessId: businessId,
|
||||
start: _service.toTimestamp(start),
|
||||
end: _service.toTimestamp(now),
|
||||
)
|
||||
.execute();
|
||||
|
||||
return result.data.orders.map((
|
||||
dc.ListCompletedOrdersByBusinessAndDateRangeOrders order,
|
||||
) {
|
||||
final String title =
|
||||
order.eventName ??
|
||||
(order.shifts_on_order.isNotEmpty
|
||||
? order.shifts_on_order[0].title
|
||||
: 'Order');
|
||||
|
||||
final String location = order.shifts_on_order.isNotEmpty
|
||||
? (order.shifts_on_order[0].location ??
|
||||
order.shifts_on_order[0].locationAddress ??
|
||||
'')
|
||||
: '';
|
||||
|
||||
int totalWorkers = 0;
|
||||
double totalHours = 0;
|
||||
double totalRate = 0;
|
||||
int roleCount = 0;
|
||||
|
||||
for (final dc.ListCompletedOrdersByBusinessAndDateRangeOrdersShiftsOnOrder
|
||||
shift
|
||||
in order.shifts_on_order) {
|
||||
for (final dc.ListCompletedOrdersByBusinessAndDateRangeOrdersShiftsOnOrderShiftRolesOnShift
|
||||
role
|
||||
in shift.shiftRoles_on_shift) {
|
||||
totalWorkers += role.count;
|
||||
totalHours += role.hours ?? 0;
|
||||
totalRate += role.role.costPerHour;
|
||||
roleCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return ReorderItem(
|
||||
orderId: order.id,
|
||||
title: title,
|
||||
location: location,
|
||||
totalCost: order.total ?? 0.0,
|
||||
workers: totalWorkers,
|
||||
type: order.orderType.stringValue,
|
||||
hourlyRate: roleCount > 0 ? totalRate / roleCount : 0.0,
|
||||
hours: totalHours,
|
||||
);
|
||||
}).toList();
|
||||
});
|
||||
Future<List<RecentOrder>> getRecentReorders() async {
|
||||
final ApiResponse response =
|
||||
await _apiService.get(ClientEndpoints.reorders);
|
||||
final Map<String, dynamic> body = response.data as Map<String, dynamic>;
|
||||
final List<dynamic> items = body['items'] as List<dynamic>;
|
||||
return items
|
||||
.map((dynamic json) =>
|
||||
RecentOrder.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,15 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// User session data for the home page.
|
||||
class UserSessionData {
|
||||
|
||||
/// Creates a [UserSessionData].
|
||||
const UserSessionData({
|
||||
required this.businessName,
|
||||
this.photoUrl,
|
||||
});
|
||||
/// The business name of the logged-in user.
|
||||
final String businessName;
|
||||
|
||||
/// The photo URL of the logged-in user (optional).
|
||||
final String? photoUrl;
|
||||
}
|
||||
|
||||
/// Interface for the Client Home repository.
|
||||
///
|
||||
/// This repository is responsible for providing data required for the
|
||||
/// client home screen dashboard.
|
||||
/// Provides data required for the client home screen dashboard
|
||||
/// via the V2 REST API.
|
||||
abstract interface class HomeRepositoryInterface {
|
||||
/// Fetches the [HomeDashboardData] containing aggregated dashboard metrics.
|
||||
Future<HomeDashboardData> getDashboardData();
|
||||
/// Fetches the [ClientDashboard] containing aggregated dashboard metrics,
|
||||
/// user name, and business info from `GET /client/dashboard`.
|
||||
Future<ClientDashboard> getDashboard();
|
||||
|
||||
/// Fetches the user's session data (business name and photo).
|
||||
Future<UserSessionData> getUserSessionData();
|
||||
|
||||
/// Fetches recently completed shift roles for reorder suggestions.
|
||||
Future<List<ReorderItem>> getRecentReorders();
|
||||
/// Fetches recent completed orders for reorder suggestions
|
||||
/// from `GET /client/reorders`.
|
||||
Future<List<RecentOrder>> getRecentReorders();
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/home_repository_interface.dart';
|
||||
|
||||
/// Use case to fetch dashboard data for the client home screen.
|
||||
import 'package:client_home/src/domain/repositories/home_repository_interface.dart';
|
||||
|
||||
/// Use case to fetch the client dashboard from the V2 API.
|
||||
///
|
||||
/// This use case coordinates with the [HomeRepositoryInterface] to retrieve
|
||||
/// the [HomeDashboardData] required for the dashboard display.
|
||||
class GetDashboardDataUseCase implements NoInputUseCase<HomeDashboardData> {
|
||||
|
||||
/// Returns a [ClientDashboard] containing spending, coverage,
|
||||
/// live-activity metrics and user/business info.
|
||||
class GetDashboardDataUseCase implements NoInputUseCase<ClientDashboard> {
|
||||
/// Creates a [GetDashboardDataUseCase].
|
||||
GetDashboardDataUseCase(this._repository);
|
||||
|
||||
/// The repository providing dashboard data.
|
||||
final HomeRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<HomeDashboardData> call() {
|
||||
return _repository.getDashboardData();
|
||||
Future<ClientDashboard> call() {
|
||||
return _repository.getDashboard();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/home_repository_interface.dart';
|
||||
|
||||
/// Use case to fetch recent completed shift roles for reorder suggestions.
|
||||
class GetRecentReordersUseCase implements NoInputUseCase<List<ReorderItem>> {
|
||||
import 'package:client_home/src/domain/repositories/home_repository_interface.dart';
|
||||
|
||||
/// Use case to fetch recent completed orders for reorder suggestions.
|
||||
///
|
||||
/// Returns a list of [RecentOrder] from the V2 API.
|
||||
class GetRecentReordersUseCase implements NoInputUseCase<List<RecentOrder>> {
|
||||
/// Creates a [GetRecentReordersUseCase].
|
||||
GetRecentReordersUseCase(this._repository);
|
||||
|
||||
/// The repository providing reorder data.
|
||||
final HomeRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<List<ReorderItem>> call() {
|
||||
Future<List<RecentOrder>> call() {
|
||||
return _repository.getRecentReorders();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import '../repositories/home_repository_interface.dart';
|
||||
|
||||
/// Use case for retrieving user session data.
|
||||
///
|
||||
/// Returns the user's business name and photo URL for display in the header.
|
||||
class GetUserSessionDataUseCase {
|
||||
|
||||
/// Creates a [GetUserSessionDataUseCase].
|
||||
GetUserSessionDataUseCase(this._repository);
|
||||
final HomeRepositoryInterface _repository;
|
||||
|
||||
/// Executes the use case to get session data.
|
||||
Future<UserSessionData> call() {
|
||||
return _repository.getUserSessionData();
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,27 @@
|
||||
import 'package:client_home/src/domain/repositories/home_repository_interface.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/usecases/get_dashboard_data_usecase.dart';
|
||||
import '../../domain/usecases/get_recent_reorders_usecase.dart';
|
||||
import '../../domain/usecases/get_user_session_data_usecase.dart';
|
||||
import 'client_home_event.dart';
|
||||
import 'client_home_state.dart';
|
||||
|
||||
/// BLoC responsible for managing the state and business logic of the client home dashboard.
|
||||
import 'package:client_home/src/domain/usecases/get_dashboard_data_usecase.dart';
|
||||
import 'package:client_home/src/domain/usecases/get_recent_reorders_usecase.dart';
|
||||
import 'package:client_home/src/presentation/blocs/client_home_event.dart';
|
||||
import 'package:client_home/src/presentation/blocs/client_home_state.dart';
|
||||
|
||||
/// BLoC responsible for managing the client home dashboard state.
|
||||
///
|
||||
/// Fetches the [ClientDashboard] and recent reorders from the V2 API
|
||||
/// and exposes layout-editing capabilities (reorder, toggle visibility).
|
||||
class ClientHomeBloc extends Bloc<ClientHomeEvent, ClientHomeState>
|
||||
with BlocErrorHandler<ClientHomeState>, SafeBloc<ClientHomeEvent, ClientHomeState> {
|
||||
with
|
||||
BlocErrorHandler<ClientHomeState>,
|
||||
SafeBloc<ClientHomeEvent, ClientHomeState> {
|
||||
/// Creates a [ClientHomeBloc].
|
||||
ClientHomeBloc({
|
||||
required GetDashboardDataUseCase getDashboardDataUseCase,
|
||||
required GetRecentReordersUseCase getRecentReordersUseCase,
|
||||
required GetUserSessionDataUseCase getUserSessionDataUseCase,
|
||||
}) : _getDashboardDataUseCase = getDashboardDataUseCase,
|
||||
_getRecentReordersUseCase = getRecentReordersUseCase,
|
||||
_getUserSessionDataUseCase = getUserSessionDataUseCase,
|
||||
super(const ClientHomeState()) {
|
||||
}) : _getDashboardDataUseCase = getDashboardDataUseCase,
|
||||
_getRecentReordersUseCase = getRecentReordersUseCase,
|
||||
super(const ClientHomeState()) {
|
||||
on<ClientHomeStarted>(_onStarted);
|
||||
on<ClientHomeEditModeToggled>(_onEditModeToggled);
|
||||
on<ClientHomeWidgetVisibilityToggled>(_onWidgetVisibilityToggled);
|
||||
@@ -27,9 +30,12 @@ class ClientHomeBloc extends Bloc<ClientHomeEvent, ClientHomeState>
|
||||
|
||||
add(ClientHomeStarted());
|
||||
}
|
||||
|
||||
/// Use case that fetches the client dashboard.
|
||||
final GetDashboardDataUseCase _getDashboardDataUseCase;
|
||||
|
||||
/// Use case that fetches recent reorders.
|
||||
final GetRecentReordersUseCase _getRecentReordersUseCase;
|
||||
final GetUserSessionDataUseCase _getUserSessionDataUseCase;
|
||||
|
||||
Future<void> _onStarted(
|
||||
ClientHomeStarted event,
|
||||
@@ -39,20 +45,15 @@ class ClientHomeBloc extends Bloc<ClientHomeEvent, ClientHomeState>
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
// Get session data
|
||||
final UserSessionData sessionData = await _getUserSessionDataUseCase();
|
||||
|
||||
// Get dashboard data
|
||||
final HomeDashboardData data = await _getDashboardDataUseCase();
|
||||
final List<ReorderItem> reorderItems = await _getRecentReordersUseCase();
|
||||
final ClientDashboard dashboard = await _getDashboardDataUseCase();
|
||||
final List<RecentOrder> reorderItems =
|
||||
await _getRecentReordersUseCase();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ClientHomeStatus.success,
|
||||
dashboardData: data,
|
||||
dashboard: dashboard,
|
||||
reorderItems: reorderItems,
|
||||
businessName: sessionData.businessName,
|
||||
photoUrl: sessionData.photoUrl,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -121,4 +122,3 @@ class ClientHomeBloc extends Bloc<ClientHomeEvent, ClientHomeState>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,23 @@ import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Status of the client home dashboard.
|
||||
enum ClientHomeStatus { initial, loading, success, error }
|
||||
enum ClientHomeStatus {
|
||||
/// Initial state before any data is loaded.
|
||||
initial,
|
||||
|
||||
/// Data is being fetched.
|
||||
loading,
|
||||
|
||||
/// Data was fetched successfully.
|
||||
success,
|
||||
|
||||
/// An error occurred.
|
||||
error,
|
||||
}
|
||||
|
||||
/// Represents the state of the client home dashboard.
|
||||
class ClientHomeState extends Equatable {
|
||||
|
||||
/// Creates a [ClientHomeState].
|
||||
const ClientHomeState({
|
||||
this.status = ClientHomeStatus.initial,
|
||||
this.widgetOrder = const <String>[
|
||||
@@ -25,38 +37,46 @@ class ClientHomeState extends Equatable {
|
||||
},
|
||||
this.isEditMode = false,
|
||||
this.errorMessage,
|
||||
this.dashboardData = const HomeDashboardData(
|
||||
weeklySpending: 0.0,
|
||||
next7DaysSpending: 0.0,
|
||||
weeklyShifts: 0,
|
||||
next7DaysScheduled: 0,
|
||||
totalNeeded: 0,
|
||||
totalFilled: 0,
|
||||
),
|
||||
this.reorderItems = const <ReorderItem>[],
|
||||
this.businessName = 'Your Company',
|
||||
this.photoUrl,
|
||||
this.dashboard,
|
||||
this.reorderItems = const <RecentOrder>[],
|
||||
});
|
||||
final ClientHomeStatus status;
|
||||
final List<String> widgetOrder;
|
||||
final Map<String, bool> widgetVisibility;
|
||||
final bool isEditMode;
|
||||
final String? errorMessage;
|
||||
final HomeDashboardData dashboardData;
|
||||
final List<ReorderItem> reorderItems;
|
||||
final String businessName;
|
||||
final String? photoUrl;
|
||||
|
||||
/// The current loading status.
|
||||
final ClientHomeStatus status;
|
||||
|
||||
/// Ordered list of widget identifiers for the dashboard layout.
|
||||
final List<String> widgetOrder;
|
||||
|
||||
/// Visibility map keyed by widget identifier.
|
||||
final Map<String, bool> widgetVisibility;
|
||||
|
||||
/// Whether the dashboard is in edit/customise mode.
|
||||
final bool isEditMode;
|
||||
|
||||
/// Error key for translation when [status] is [ClientHomeStatus.error].
|
||||
final String? errorMessage;
|
||||
|
||||
/// The V2 client dashboard data (null until loaded).
|
||||
final ClientDashboard? dashboard;
|
||||
|
||||
/// Recent orders available for quick reorder.
|
||||
final List<RecentOrder> reorderItems;
|
||||
|
||||
/// The business name from the dashboard, with a safe fallback.
|
||||
String get businessName => dashboard?.businessName ?? 'Your Company';
|
||||
|
||||
/// The user display name from the dashboard.
|
||||
String get userName => dashboard?.userName ?? '';
|
||||
|
||||
/// Creates a copy of this state with the given fields replaced.
|
||||
ClientHomeState copyWith({
|
||||
ClientHomeStatus? status,
|
||||
List<String>? widgetOrder,
|
||||
Map<String, bool>? widgetVisibility,
|
||||
bool? isEditMode,
|
||||
String? errorMessage,
|
||||
HomeDashboardData? dashboardData,
|
||||
List<ReorderItem>? reorderItems,
|
||||
String? businessName,
|
||||
String? photoUrl,
|
||||
ClientDashboard? dashboard,
|
||||
List<RecentOrder>? reorderItems,
|
||||
}) {
|
||||
return ClientHomeState(
|
||||
status: status ?? this.status,
|
||||
@@ -64,23 +84,19 @@ class ClientHomeState extends Equatable {
|
||||
widgetVisibility: widgetVisibility ?? this.widgetVisibility,
|
||||
isEditMode: isEditMode ?? this.isEditMode,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
dashboardData: dashboardData ?? this.dashboardData,
|
||||
dashboard: dashboard ?? this.dashboard,
|
||||
reorderItems: reorderItems ?? this.reorderItems,
|
||||
businessName: businessName ?? this.businessName,
|
||||
photoUrl: photoUrl ?? this.photoUrl,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
status,
|
||||
widgetOrder,
|
||||
widgetVisibility,
|
||||
isEditMode,
|
||||
errorMessage,
|
||||
dashboardData,
|
||||
reorderItems,
|
||||
businessName,
|
||||
photoUrl,
|
||||
];
|
||||
status,
|
||||
widgetOrder,
|
||||
widgetVisibility,
|
||||
isEditMode,
|
||||
errorMessage,
|
||||
dashboard,
|
||||
reorderItems,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
|
||||
import '../blocs/client_home_bloc.dart';
|
||||
import '../widgets/client_home_body.dart';
|
||||
import '../widgets/client_home_edit_banner.dart';
|
||||
import '../widgets/client_home_header.dart';
|
||||
import 'package:client_home/src/presentation/blocs/client_home_bloc.dart';
|
||||
import 'package:client_home/src/presentation/widgets/client_home_body.dart';
|
||||
import 'package:client_home/src/presentation/widgets/client_home_edit_banner.dart';
|
||||
import 'package:client_home/src/presentation/widgets/client_home_header.dart';
|
||||
|
||||
/// The main Home page for client users.
|
||||
///
|
||||
|
||||
@@ -3,12 +3,12 @@ import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../blocs/client_home_bloc.dart';
|
||||
import '../blocs/client_home_state.dart';
|
||||
import 'client_home_edit_mode_body.dart';
|
||||
import 'client_home_error_state.dart';
|
||||
import 'client_home_normal_mode_body.dart';
|
||||
import 'client_home_page_skeleton.dart';
|
||||
import 'package:client_home/src/presentation/blocs/client_home_bloc.dart';
|
||||
import 'package:client_home/src/presentation/blocs/client_home_state.dart';
|
||||
import 'package:client_home/src/presentation/widgets/client_home_edit_mode_body.dart';
|
||||
import 'package:client_home/src/presentation/widgets/client_home_error_state.dart';
|
||||
import 'package:client_home/src/presentation/widgets/client_home_normal_mode_body.dart';
|
||||
import 'package:client_home/src/presentation/widgets/client_home_page_skeleton.dart';
|
||||
|
||||
/// Main body widget for the client home page.
|
||||
///
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../blocs/client_home_bloc.dart';
|
||||
import '../blocs/client_home_event.dart';
|
||||
import '../blocs/client_home_state.dart';
|
||||
import 'package:client_home/src/presentation/blocs/client_home_bloc.dart';
|
||||
import 'package:client_home/src/presentation/blocs/client_home_event.dart';
|
||||
import 'package:client_home/src/presentation/blocs/client_home_state.dart';
|
||||
|
||||
/// A banner displayed when edit mode is active.
|
||||
///
|
||||
|
||||
@@ -2,10 +2,10 @@ import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../blocs/client_home_bloc.dart';
|
||||
import '../blocs/client_home_event.dart';
|
||||
import '../blocs/client_home_state.dart';
|
||||
import 'dashboard_widget_builder.dart';
|
||||
import 'package:client_home/src/presentation/blocs/client_home_bloc.dart';
|
||||
import 'package:client_home/src/presentation/blocs/client_home_event.dart';
|
||||
import 'package:client_home/src/presentation/blocs/client_home_state.dart';
|
||||
import 'package:client_home/src/presentation/widgets/dashboard_widget_builder.dart';
|
||||
|
||||
/// Widget that displays the home dashboard in edit mode with drag-and-drop support.
|
||||
///
|
||||
|
||||
@@ -3,9 +3,9 @@ import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../blocs/client_home_bloc.dart';
|
||||
import '../blocs/client_home_event.dart';
|
||||
import '../blocs/client_home_state.dart';
|
||||
import 'package:client_home/src/presentation/blocs/client_home_bloc.dart';
|
||||
import 'package:client_home/src/presentation/blocs/client_home_event.dart';
|
||||
import 'package:client_home/src/presentation/blocs/client_home_state.dart';
|
||||
|
||||
/// Widget that displays an error state for the client home page.
|
||||
///
|
||||
|
||||
@@ -3,23 +3,24 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import '../blocs/client_home_bloc.dart';
|
||||
import '../blocs/client_home_event.dart';
|
||||
import '../blocs/client_home_state.dart';
|
||||
import 'header_icon_button.dart';
|
||||
import 'client_home_header_skeleton.dart';
|
||||
|
||||
import 'package:client_home/src/presentation/blocs/client_home_bloc.dart';
|
||||
import 'package:client_home/src/presentation/blocs/client_home_event.dart';
|
||||
import 'package:client_home/src/presentation/blocs/client_home_state.dart';
|
||||
import 'package:client_home/src/presentation/widgets/header_icon_button.dart';
|
||||
import 'package:client_home/src/presentation/widgets/client_home_header_skeleton.dart';
|
||||
|
||||
/// The header section of the client home page.
|
||||
///
|
||||
/// Displays the user's business name, avatar, and action buttons
|
||||
/// (edit mode, notifications, settings).
|
||||
/// (edit mode, settings).
|
||||
class ClientHomeHeader extends StatelessWidget {
|
||||
|
||||
/// Creates a [ClientHomeHeader].
|
||||
const ClientHomeHeader({
|
||||
required this.i18n,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The internationalization object for localized strings.
|
||||
final dynamic i18n;
|
||||
|
||||
@@ -33,7 +34,6 @@ class ClientHomeHeader extends StatelessWidget {
|
||||
}
|
||||
|
||||
final String businessName = state.businessName;
|
||||
final String? photoUrl = state.photoUrl;
|
||||
final String avatarLetter = businessName.trim().isNotEmpty
|
||||
? businessName.trim()[0].toUpperCase()
|
||||
: 'C';
|
||||
@@ -62,18 +62,12 @@ class ClientHomeHeader extends StatelessWidget {
|
||||
),
|
||||
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.body2b.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
avatarLetter,
|
||||
style: UiTypography.body2b.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../blocs/client_home_state.dart';
|
||||
import 'dashboard_widget_builder.dart';
|
||||
import 'package:client_home/src/presentation/blocs/client_home_state.dart';
|
||||
import 'package:client_home/src/presentation/widgets/dashboard_widget_builder.dart';
|
||||
|
||||
/// Widget that displays the home dashboard in normal mode.
|
||||
///
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'shift_order_form_sheet.dart';
|
||||
|
||||
/// Helper class for showing modal sheets in the client home feature.
|
||||
class ClientHomeSheets {
|
||||
/// Shows the shift order form bottom sheet.
|
||||
///
|
||||
/// Optionally accepts [initialData] to pre-populate the form for reordering.
|
||||
/// Calls [onSubmit] when the user submits the form successfully.
|
||||
static void showOrderFormSheet(
|
||||
BuildContext context,
|
||||
Map<String, dynamic>? initialData, {
|
||||
required void Function(Map<String, dynamic>) onSubmit,
|
||||
}) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (BuildContext context) {
|
||||
return ShiftOrderFormSheet(
|
||||
initialData: initialData,
|
||||
onSubmit: onSubmit,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A dashboard widget that displays today's coverage status.
|
||||
class CoverageDashboard extends StatelessWidget {
|
||||
/// Creates a [CoverageDashboard].
|
||||
const CoverageDashboard({
|
||||
super.key,
|
||||
required this.shifts,
|
||||
required this.applications,
|
||||
});
|
||||
|
||||
/// The list of shifts for today.
|
||||
final List<dynamic> shifts;
|
||||
|
||||
/// The list of applications for today's shifts.
|
||||
final List<dynamic> applications;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
int totalNeeded = 0;
|
||||
int totalConfirmed = 0;
|
||||
double todayCost = 0;
|
||||
|
||||
for (final dynamic s in shifts) {
|
||||
final int needed =
|
||||
(s as Map<String, dynamic>)['workersNeeded'] as int? ?? 0;
|
||||
final int confirmed = s['filled'] as int? ?? 0;
|
||||
final double rate = s['hourlyRate'] as double? ?? 0.0;
|
||||
final double hours = s['hours'] as double? ?? 0.0;
|
||||
|
||||
totalNeeded += needed;
|
||||
totalConfirmed += confirmed;
|
||||
todayCost += rate * hours;
|
||||
}
|
||||
|
||||
final int coveragePercent = totalNeeded > 0
|
||||
? ((totalConfirmed / totalNeeded) * 100).round()
|
||||
: 100;
|
||||
final int unfilledPositions = totalNeeded - totalConfirmed;
|
||||
|
||||
final int checkedInCount = applications
|
||||
.where(
|
||||
(dynamic a) => (a as Map<String, dynamic>)['checkInTime'] != null,
|
||||
)
|
||||
.length;
|
||||
final int lateWorkersCount = applications
|
||||
.where((dynamic a) => (a as Map<String, dynamic>)['status'] == 'LATE')
|
||||
.length;
|
||||
|
||||
final bool isCoverageGood = coveragePercent >= 90;
|
||||
final Color coverageBadgeColor = isCoverageGood
|
||||
? UiColors.tagSuccess
|
||||
: UiColors.tagPending;
|
||||
final Color coverageTextColor = isCoverageGood
|
||||
? UiColors.textSuccess
|
||||
: UiColors.textWarning;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border, width: 0.5),
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text("Today's Status", style: UiTypography.body1m.textSecondary),
|
||||
if (totalNeeded > 0 || totalConfirmed > 0)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space2,
|
||||
vertical: 2.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: coverageBadgeColor,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
),
|
||||
child: Text(
|
||||
'$coveragePercent% Covered',
|
||||
style: UiTypography.footnote1b.copyWith(
|
||||
color: coverageTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
_StatusCard(
|
||||
label: 'Unfilled Today',
|
||||
value: '$unfilledPositions',
|
||||
icon: UiIcons.warning,
|
||||
isWarning: unfilledPositions > 0,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
_StatusCard(
|
||||
label: 'Running Late',
|
||||
value: '$lateWorkersCount',
|
||||
icon: UiIcons.error,
|
||||
isError: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
_StatusCard(
|
||||
label: 'Checked In',
|
||||
value: '$checkedInCount/$totalNeeded',
|
||||
icon: UiIcons.success,
|
||||
isInfo: true,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
_StatusCard(
|
||||
label: "Today's Cost",
|
||||
value: '\$${todayCost.round()}',
|
||||
icon: UiIcons.dollar,
|
||||
isInfo: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusCard extends StatelessWidget {
|
||||
const _StatusCard({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.icon,
|
||||
this.isWarning = false,
|
||||
this.isError = false,
|
||||
this.isInfo = false,
|
||||
});
|
||||
final String label;
|
||||
final String value;
|
||||
final IconData icon;
|
||||
final bool isWarning;
|
||||
final bool isError;
|
||||
final bool isInfo;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color bg = UiColors.bgSecondary;
|
||||
Color border = UiColors.border;
|
||||
Color iconColor = UiColors.iconSecondary;
|
||||
Color textColor = UiColors.textPrimary;
|
||||
|
||||
if (isWarning) {
|
||||
bg = UiColors.tagPending.withAlpha(80);
|
||||
border = UiColors.textWarning.withAlpha(80);
|
||||
iconColor = UiColors.textWarning;
|
||||
textColor = UiColors.textWarning;
|
||||
} else if (isError) {
|
||||
bg = UiColors.tagError.withAlpha(80);
|
||||
border = UiColors.borderError.withAlpha(80);
|
||||
iconColor = UiColors.textError;
|
||||
textColor = UiColors.textError;
|
||||
} else if (isInfo) {
|
||||
bg = UiColors.tagInProgress.withAlpha(80);
|
||||
border = UiColors.primary.withValues(alpha: 0.2);
|
||||
iconColor = UiColors.primary;
|
||||
textColor = UiColors.primary;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
border: Border.all(color: border),
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Icon(icon, size: 16, color: iconColor),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: UiTypography.footnote1m.copyWith(
|
||||
color: textColor.withValues(alpha: 0.8),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
value,
|
||||
style: UiTypography.headline3m.copyWith(color: textColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,20 @@ import 'package:core_localization/core_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import '../blocs/client_home_state.dart';
|
||||
import '../widgets/actions_widget.dart';
|
||||
import '../widgets/coverage_widget.dart';
|
||||
import '../widgets/draggable_widget_wrapper.dart';
|
||||
import '../widgets/live_activity_widget.dart';
|
||||
import '../widgets/reorder_widget.dart';
|
||||
import '../widgets/spending_widget.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'package:client_home/src/presentation/blocs/client_home_state.dart';
|
||||
import 'package:client_home/src/presentation/widgets/actions_widget.dart';
|
||||
import 'package:client_home/src/presentation/widgets/coverage_widget.dart';
|
||||
import 'package:client_home/src/presentation/widgets/draggable_widget_wrapper.dart';
|
||||
import 'package:client_home/src/presentation/widgets/live_activity_widget.dart';
|
||||
import 'package:client_home/src/presentation/widgets/reorder_widget.dart';
|
||||
import 'package:client_home/src/presentation/widgets/spending_widget.dart';
|
||||
|
||||
/// A widget that builds dashboard content based on widget ID.
|
||||
///
|
||||
/// This widget encapsulates the logic for rendering different dashboard
|
||||
/// widgets based on their unique identifiers and current state.
|
||||
/// Renders different dashboard sections depending on their unique identifier
|
||||
/// and the current [ClientHomeState].
|
||||
class DashboardWidgetBuilder extends StatelessWidget {
|
||||
/// Creates a [DashboardWidgetBuilder].
|
||||
const DashboardWidgetBuilder({
|
||||
@@ -55,11 +57,16 @@ class DashboardWidgetBuilder extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// Builds the actual widget content based on the widget ID.
|
||||
Widget _buildWidgetContent(BuildContext context, TranslationsClientHomeWidgetsEn i18n) {
|
||||
Widget _buildWidgetContent(
|
||||
BuildContext context,
|
||||
TranslationsClientHomeWidgetsEn i18n,
|
||||
) {
|
||||
final String title = _getWidgetTitle(i18n);
|
||||
// Only show subtitle in normal mode
|
||||
final String? subtitle = !isEditMode ? _getWidgetSubtitle(id) : null;
|
||||
|
||||
final ClientDashboard? dashboard = state.dashboard;
|
||||
|
||||
switch (id) {
|
||||
case 'actions':
|
||||
return ActionsWidget(title: title, subtitle: subtitle);
|
||||
@@ -71,28 +78,32 @@ class DashboardWidgetBuilder extends StatelessWidget {
|
||||
);
|
||||
case 'spending':
|
||||
return SpendingWidget(
|
||||
weeklySpending: state.dashboardData.weeklySpending,
|
||||
next7DaysSpending: state.dashboardData.next7DaysSpending,
|
||||
weeklyShifts: state.dashboardData.weeklyShifts,
|
||||
next7DaysScheduled: state.dashboardData.next7DaysScheduled,
|
||||
weeklySpendCents: dashboard?.spending.weeklySpendCents ?? 0,
|
||||
projectedNext7DaysCents:
|
||||
dashboard?.spending.projectedNext7DaysCents ?? 0,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
);
|
||||
case 'coverage':
|
||||
final CoverageMetrics? coverage = dashboard?.coverage;
|
||||
final int needed = coverage?.neededWorkersToday ?? 0;
|
||||
final int filled = coverage?.filledWorkersToday ?? 0;
|
||||
return CoverageWidget(
|
||||
totalNeeded: state.dashboardData.totalNeeded,
|
||||
totalConfirmed: state.dashboardData.totalFilled,
|
||||
coveragePercent: state.dashboardData.totalNeeded > 0
|
||||
? ((state.dashboardData.totalFilled /
|
||||
state.dashboardData.totalNeeded) *
|
||||
100)
|
||||
.toInt()
|
||||
: 0,
|
||||
totalNeeded: needed,
|
||||
totalConfirmed: filled,
|
||||
coveragePercent: needed > 0 ? ((filled / needed) * 100).toInt() : 0,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
);
|
||||
case 'liveActivity':
|
||||
return LiveActivityWidget(
|
||||
metrics: dashboard?.liveActivity ??
|
||||
const LiveActivityMetrics(
|
||||
lateWorkersToday: 0,
|
||||
checkedInWorkersToday: 0,
|
||||
averageShiftCostCents: 0,
|
||||
),
|
||||
coverageNeeded: dashboard?.coverage.neededWorkersToday ?? 0,
|
||||
onViewAllPressed: () => Modular.to.toClientCoverage(),
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
@@ -106,20 +117,21 @@ class DashboardWidgetBuilder extends StatelessWidget {
|
||||
String _getWidgetTitle(dynamic i18n) {
|
||||
switch (id) {
|
||||
case 'actions':
|
||||
return i18n.actions;
|
||||
return i18n.actions as String;
|
||||
case 'reorder':
|
||||
return i18n.reorder;
|
||||
return i18n.reorder as String;
|
||||
case 'coverage':
|
||||
return i18n.coverage;
|
||||
return i18n.coverage as String;
|
||||
case 'spending':
|
||||
return i18n.spending;
|
||||
return i18n.spending as String;
|
||||
case 'liveActivity':
|
||||
return i18n.live_activity;
|
||||
return i18n.live_activity as String;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the subtitle for the widget based on its ID.
|
||||
String _getWidgetSubtitle(String id) {
|
||||
switch (id) {
|
||||
case 'actions':
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../blocs/client_home_bloc.dart';
|
||||
import '../blocs/client_home_event.dart';
|
||||
import 'package:client_home/src/presentation/blocs/client_home_bloc.dart';
|
||||
import 'package:client_home/src/presentation/blocs/client_home_event.dart';
|
||||
|
||||
/// A wrapper for dashboard widgets in edit mode.
|
||||
///
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'coverage_dashboard.dart';
|
||||
import 'section_layout.dart';
|
||||
|
||||
/// A widget that displays live activity information.
|
||||
class LiveActivityWidget extends StatefulWidget {
|
||||
import 'package:client_home/src/presentation/widgets/section_layout.dart';
|
||||
|
||||
/// A widget that displays live activity metrics for today.
|
||||
///
|
||||
/// Renders checked-in count, late workers, and average shift cost
|
||||
/// from the [LiveActivityMetrics] provided by the V2 dashboard endpoint.
|
||||
class LiveActivityWidget extends StatelessWidget {
|
||||
/// Creates a [LiveActivityWidget].
|
||||
const LiveActivityWidget({
|
||||
super.key,
|
||||
required this.metrics,
|
||||
required this.coverageNeeded,
|
||||
required this.onViewAllPressed,
|
||||
this.title,
|
||||
this.subtitle
|
||||
this.subtitle,
|
||||
});
|
||||
|
||||
/// Live activity metrics from the V2 dashboard.
|
||||
final LiveActivityMetrics metrics;
|
||||
|
||||
/// Workers needed today (from coverage metrics) for the checked-in ratio.
|
||||
final int coverageNeeded;
|
||||
|
||||
/// Callback when "View all" is pressed.
|
||||
final VoidCallback onViewAllPressed;
|
||||
|
||||
@@ -25,159 +35,180 @@ class LiveActivityWidget extends StatefulWidget {
|
||||
/// Optional subtitle for the section.
|
||||
final String? subtitle;
|
||||
|
||||
@override
|
||||
State<LiveActivityWidget> createState() => _LiveActivityWidgetState();
|
||||
}
|
||||
|
||||
class _LiveActivityWidgetState extends State<LiveActivityWidget> {
|
||||
late final Future<_LiveActivityData> _liveActivityFuture =
|
||||
_loadLiveActivity();
|
||||
|
||||
Future<_LiveActivityData> _loadLiveActivity() async {
|
||||
final String? businessId =
|
||||
dc.ClientSessionStore.instance.session?.business?.id;
|
||||
if (businessId == null || businessId.isEmpty) {
|
||||
return _LiveActivityData.empty();
|
||||
}
|
||||
|
||||
final DateTime now = DateTime.now();
|
||||
final DateTime start = DateTime(now.year, now.month, now.day);
|
||||
final DateTime end = DateTime(now.year, now.month, now.day, 23, 59, 59, 999);
|
||||
final fdc.QueryResult<dc.ListShiftRolesByBusinessAndDateRangeData,
|
||||
dc.ListShiftRolesByBusinessAndDateRangeVariables> shiftRolesResult =
|
||||
await dc.ExampleConnector.instance
|
||||
.listShiftRolesByBusinessAndDateRange(
|
||||
businessId: businessId,
|
||||
start: _toTimestamp(start),
|
||||
end: _toTimestamp(end),
|
||||
)
|
||||
.execute();
|
||||
final fdc.QueryResult<dc.ListStaffsApplicationsByBusinessForDayData,
|
||||
dc.ListStaffsApplicationsByBusinessForDayVariables> result =
|
||||
await dc.ExampleConnector.instance
|
||||
.listStaffsApplicationsByBusinessForDay(
|
||||
businessId: businessId,
|
||||
dayStart: _toTimestamp(start),
|
||||
dayEnd: _toTimestamp(end),
|
||||
)
|
||||
.execute();
|
||||
|
||||
if (shiftRolesResult.data.shiftRoles.isEmpty &&
|
||||
result.data.applications.isEmpty) {
|
||||
return _LiveActivityData.empty();
|
||||
}
|
||||
|
||||
int totalNeeded = 0;
|
||||
double totalCost = 0;
|
||||
for (final dc.ListShiftRolesByBusinessAndDateRangeShiftRoles shiftRole
|
||||
in shiftRolesResult.data.shiftRoles) {
|
||||
totalNeeded += shiftRole.count;
|
||||
totalCost += shiftRole.totalValue ?? 0;
|
||||
}
|
||||
|
||||
final int totalAssigned = result.data.applications.length;
|
||||
int lateCount = 0;
|
||||
int checkedInCount = 0;
|
||||
for (final dc.ListStaffsApplicationsByBusinessForDayApplications app
|
||||
in result.data.applications) {
|
||||
if (app.checkInTime != null) {
|
||||
checkedInCount += 1;
|
||||
}
|
||||
if (app.status is dc.Known<dc.ApplicationStatus> &&
|
||||
(app.status as dc.Known<dc.ApplicationStatus>).value ==
|
||||
dc.ApplicationStatus.LATE) {
|
||||
lateCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return _LiveActivityData(
|
||||
totalNeeded: totalNeeded,
|
||||
totalAssigned: totalAssigned,
|
||||
totalCost: totalCost,
|
||||
checkedInCount: checkedInCount,
|
||||
lateCount: lateCount,
|
||||
);
|
||||
}
|
||||
|
||||
fdc.Timestamp _toTimestamp(DateTime dateTime) {
|
||||
final DateTime utc = dateTime.toUtc();
|
||||
final int seconds = utc.millisecondsSinceEpoch ~/ 1000;
|
||||
final int nanoseconds =
|
||||
(utc.millisecondsSinceEpoch % 1000) * 1000000;
|
||||
return fdc.Timestamp(nanoseconds, seconds);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsClientHomeEn i18n = t.client_home;
|
||||
|
||||
final int checkedIn = metrics.checkedInWorkersToday;
|
||||
final int late_ = metrics.lateWorkersToday;
|
||||
final String avgCostDisplay =
|
||||
'\$${(metrics.averageShiftCostCents / 100).toStringAsFixed(0)}';
|
||||
|
||||
final int coveragePercent =
|
||||
coverageNeeded > 0 ? ((checkedIn / coverageNeeded) * 100).round() : 100;
|
||||
|
||||
final bool isCoverageGood = coveragePercent >= 90;
|
||||
final Color coverageBadgeColor =
|
||||
isCoverageGood ? UiColors.tagSuccess : UiColors.tagPending;
|
||||
final Color coverageTextColor =
|
||||
isCoverageGood ? UiColors.textSuccess : UiColors.textWarning;
|
||||
|
||||
return SectionLayout(
|
||||
title: widget.title,
|
||||
subtitle: widget.subtitle,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
action: i18n.dashboard.view_all,
|
||||
onAction: widget.onViewAllPressed,
|
||||
child: FutureBuilder<_LiveActivityData>(
|
||||
future: _liveActivityFuture,
|
||||
builder: (BuildContext context,
|
||||
AsyncSnapshot<_LiveActivityData> snapshot) {
|
||||
final _LiveActivityData data =
|
||||
snapshot.data ?? _LiveActivityData.empty();
|
||||
final List<Map<String, Object>> shifts =
|
||||
<Map<String, Object>>[
|
||||
<String, Object>{
|
||||
'workersNeeded': data.totalNeeded,
|
||||
'filled': data.totalAssigned,
|
||||
'hourlyRate': 1.0,
|
||||
'hours': data.totalCost,
|
||||
'status': 'OPEN',
|
||||
'date': DateTime.now().toIso8601String().split('T')[0],
|
||||
},
|
||||
];
|
||||
final List<Map<String, Object?>> applications =
|
||||
<Map<String, Object?>>[];
|
||||
for (int i = 0; i < data.checkedInCount; i += 1) {
|
||||
applications.add(
|
||||
<String, Object?>{
|
||||
'status': 'CONFIRMED',
|
||||
'checkInTime': '09:00',
|
||||
},
|
||||
);
|
||||
}
|
||||
for (int i = 0; i < data.lateCount; i += 1) {
|
||||
applications.add(<String, Object?>{'status': 'LATE'});
|
||||
}
|
||||
return CoverageDashboard(
|
||||
shifts: shifts,
|
||||
applications: applications,
|
||||
);
|
||||
},
|
||||
onAction: onViewAllPressed,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border, width: 0.5),
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
// ASSUMPTION: Reusing hardcoded string from previous
|
||||
// CoverageDashboard widget — a future localization pass should
|
||||
// add a dedicated i18n key.
|
||||
Text(
|
||||
"Today's Status",
|
||||
style: UiTypography.body1m.textSecondary,
|
||||
),
|
||||
if (coverageNeeded > 0)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space2,
|
||||
vertical: 2.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: coverageBadgeColor,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
),
|
||||
child: Text(
|
||||
i18n.dashboard.percent_covered(percent: coveragePercent),
|
||||
style: UiTypography.footnote1b.copyWith(
|
||||
color: coverageTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
// ASSUMPTION: Reusing hardcoded strings from previous
|
||||
// CoverageDashboard widget.
|
||||
_StatusCard(
|
||||
label: 'Running Late',
|
||||
value: '$late_',
|
||||
icon: UiIcons.error,
|
||||
isError: true,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
_StatusCard(
|
||||
label: "Today's Cost",
|
||||
value: avgCostDisplay,
|
||||
icon: UiIcons.dollar,
|
||||
isInfo: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
_StatusCard(
|
||||
label: 'Checked In',
|
||||
value: '$checkedIn/$coverageNeeded',
|
||||
icon: UiIcons.success,
|
||||
isInfo: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LiveActivityData {
|
||||
|
||||
factory _LiveActivityData.empty() {
|
||||
return const _LiveActivityData(
|
||||
totalNeeded: 0,
|
||||
totalAssigned: 0,
|
||||
totalCost: 0,
|
||||
checkedInCount: 0,
|
||||
lateCount: 0,
|
||||
);
|
||||
}
|
||||
const _LiveActivityData({
|
||||
required this.totalNeeded,
|
||||
required this.totalAssigned,
|
||||
required this.totalCost,
|
||||
required this.checkedInCount,
|
||||
required this.lateCount,
|
||||
class _StatusCard extends StatelessWidget {
|
||||
const _StatusCard({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.icon,
|
||||
this.isError = false,
|
||||
this.isInfo = false,
|
||||
});
|
||||
|
||||
final int totalNeeded;
|
||||
final int totalAssigned;
|
||||
final double totalCost;
|
||||
final int checkedInCount;
|
||||
final int lateCount;
|
||||
final String label;
|
||||
final String value;
|
||||
final IconData icon;
|
||||
final bool isError;
|
||||
final bool isInfo;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color bg = UiColors.bgSecondary;
|
||||
Color border = UiColors.border;
|
||||
Color iconColor = UiColors.iconSecondary;
|
||||
Color textColor = UiColors.textPrimary;
|
||||
|
||||
if (isError) {
|
||||
bg = UiColors.tagError.withAlpha(80);
|
||||
border = UiColors.borderError.withAlpha(80);
|
||||
iconColor = UiColors.textError;
|
||||
textColor = UiColors.textError;
|
||||
} else if (isInfo) {
|
||||
bg = UiColors.tagInProgress.withAlpha(80);
|
||||
border = UiColors.primary.withValues(alpha: 0.2);
|
||||
iconColor = UiColors.primary;
|
||||
textColor = UiColors.primary;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
border: Border.all(color: border),
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Icon(icon, size: 16, color: iconColor),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: UiTypography.footnote1m.copyWith(
|
||||
color: textColor.withValues(alpha: 0.8),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
value,
|
||||
style: UiTypography.headline3m.copyWith(color: textColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'section_layout.dart';
|
||||
import 'package:client_home/src/presentation/widgets/section_layout.dart';
|
||||
|
||||
/// A widget that allows clients to reorder recent shifts.
|
||||
/// A widget that allows clients to reorder recent orders.
|
||||
///
|
||||
/// Displays a horizontal list of [RecentOrder] cards with a reorder button.
|
||||
class ReorderWidget extends StatelessWidget {
|
||||
/// Creates a [ReorderWidget].
|
||||
const ReorderWidget({
|
||||
@@ -18,7 +20,7 @@ class ReorderWidget extends StatelessWidget {
|
||||
});
|
||||
|
||||
/// Recent completed orders for reorder.
|
||||
final List<ReorderItem> orders;
|
||||
final List<RecentOrder> orders;
|
||||
|
||||
/// Optional title for the section.
|
||||
final String? title;
|
||||
@@ -33,25 +35,23 @@ class ReorderWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
final TranslationsClientHomeReorderEn i18n = t.client_home.reorder;
|
||||
|
||||
final List<ReorderItem> recentOrders = orders;
|
||||
final Size size = MediaQuery.sizeOf(context);
|
||||
|
||||
return SectionLayout(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
child: SizedBox(
|
||||
height: 164,
|
||||
height: size.height * 0.18,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: recentOrders.length,
|
||||
itemCount: orders.length,
|
||||
separatorBuilder: (BuildContext context, int index) =>
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final ReorderItem order = recentOrders[index];
|
||||
final double totalCost = order.totalCost;
|
||||
final RecentOrder order = orders[index];
|
||||
|
||||
return Container(
|
||||
width: 260,
|
||||
width: size.width * 0.8,
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
@@ -71,9 +71,7 @@ class ReorderWidget extends StatelessWidget {
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withValues(
|
||||
alpha: 0.1,
|
||||
),
|
||||
color: UiColors.primary.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: const Icon(
|
||||
@@ -89,34 +87,35 @@ class ReorderWidget extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
Text(
|
||||
order.title,
|
||||
style: UiTypography.body2b,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
order.location,
|
||||
style:
|
||||
UiTypography.footnote1r.textSecondary,
|
||||
style: UiTypography.body2m,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (order.hubName != null &&
|
||||
order.hubName!.isNotEmpty)
|
||||
Text(
|
||||
order.hubName!,
|
||||
style:
|
||||
UiTypography.footnote1r.textSecondary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'\$${totalCost.toStringAsFixed(0)}',
|
||||
style: UiTypography.body1b,
|
||||
),
|
||||
Text(
|
||||
'${i18n.per_hr(amount: order.hourlyRate.toString())} · ${order.hours}h',
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
// Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.end,
|
||||
// children: <Widget>[
|
||||
// // ASSUMPTION: No i18n key for 'positions' under
|
||||
// // reorder section — carrying forward existing
|
||||
// // hardcoded string pattern for this migration.
|
||||
// Text(
|
||||
// '${order.positionCount} positions',
|
||||
// style: UiTypography.footnote2r.textSecondary,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
@@ -124,7 +123,7 @@ class ReorderWidget extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
_Badge(
|
||||
icon: UiIcons.success,
|
||||
text: order.type,
|
||||
text: order.orderType.value,
|
||||
color: UiColors.primary,
|
||||
bg: UiColors.buttonSecondaryStill,
|
||||
textColor: UiColors.primary,
|
||||
@@ -132,7 +131,7 @@ class ReorderWidget extends StatelessWidget {
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
_Badge(
|
||||
icon: UiIcons.building,
|
||||
text: '${order.workers}',
|
||||
text: '${order.positionCount}',
|
||||
color: UiColors.textSecondary,
|
||||
bg: UiColors.buttonSecondaryStill,
|
||||
textColor: UiColors.textSecondary,
|
||||
@@ -140,24 +139,13 @@ class ReorderWidget extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
|
||||
UiButton.secondary(
|
||||
size: UiButtonSize.small,
|
||||
text: i18n.reorder_button,
|
||||
leadingIcon: UiIcons.zap,
|
||||
iconSize: 12,
|
||||
fullWidth: true,
|
||||
onPressed: () =>
|
||||
_handleReorderPressed(context, <String, dynamic>{
|
||||
'orderId': order.orderId,
|
||||
'title': order.title,
|
||||
'location': order.location,
|
||||
'hourlyRate': order.hourlyRate,
|
||||
'hours': order.hours,
|
||||
'workers': order.workers,
|
||||
'type': order.type,
|
||||
'totalCost': order.totalCost,
|
||||
}),
|
||||
onPressed: () => _handleReorderPressed(order),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -168,28 +156,27 @@ class ReorderWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _handleReorderPressed(BuildContext context, Map<String, dynamic> data) {
|
||||
// Override start date with today's date as requested
|
||||
final Map<String, dynamic> populatedData = Map<String, dynamic>.from(data)
|
||||
..['startDate'] = DateTime.now();
|
||||
/// Navigates to the appropriate create-order form pre-populated
|
||||
/// with data from the selected [order].
|
||||
void _handleReorderPressed(RecentOrder order) {
|
||||
final Map<String, dynamic> populatedData = <String, dynamic>{
|
||||
'orderId': order.id,
|
||||
'title': order.title,
|
||||
'location': order.hubName ?? '',
|
||||
'workers': order.positionCount,
|
||||
'type': order.orderType.value,
|
||||
'startDate': DateTime.now(),
|
||||
};
|
||||
|
||||
final String? typeStr = populatedData['type']?.toString();
|
||||
if (typeStr == null || typeStr.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final OrderType orderType = OrderType.fromString(typeStr);
|
||||
switch (orderType) {
|
||||
switch (order.orderType) {
|
||||
case OrderType.recurring:
|
||||
Modular.to.toCreateOrderRecurring(arguments: populatedData);
|
||||
break;
|
||||
case OrderType.permanent:
|
||||
Modular.to.toCreateOrderPermanent(arguments: populatedData);
|
||||
break;
|
||||
case OrderType.oneTime:
|
||||
default:
|
||||
case OrderType.rapid:
|
||||
case OrderType.unknown:
|
||||
Modular.to.toCreateOrderOneTime(arguments: populatedData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,6 +189,7 @@ class _Badge extends StatelessWidget {
|
||||
required this.bg,
|
||||
required this.textColor,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String text;
|
||||
final Color color;
|
||||
|
||||
@@ -2,32 +2,26 @@ import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'section_layout.dart';
|
||||
import 'package:client_home/src/presentation/widgets/section_layout.dart';
|
||||
|
||||
/// A widget that displays spending insights for the client.
|
||||
///
|
||||
/// All monetary values are in **cents** and converted to dollars for display.
|
||||
class SpendingWidget extends StatelessWidget {
|
||||
|
||||
/// Creates a [SpendingWidget].
|
||||
const SpendingWidget({
|
||||
super.key,
|
||||
required this.weeklySpending,
|
||||
required this.next7DaysSpending,
|
||||
required this.weeklyShifts,
|
||||
required this.next7DaysScheduled,
|
||||
required this.weeklySpendCents,
|
||||
required this.projectedNext7DaysCents,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
});
|
||||
/// The spending this week.
|
||||
final double weeklySpending;
|
||||
|
||||
/// The spending for the next 7 days.
|
||||
final double next7DaysSpending;
|
||||
/// Total spend this week in cents.
|
||||
final int weeklySpendCents;
|
||||
|
||||
/// The number of shifts this week.
|
||||
final int weeklyShifts;
|
||||
|
||||
/// The number of scheduled shifts for next 7 days.
|
||||
final int next7DaysScheduled;
|
||||
/// Projected spend for the next 7 days in cents.
|
||||
final int projectedNext7DaysCents;
|
||||
|
||||
/// Optional title for the section.
|
||||
final String? title;
|
||||
@@ -37,6 +31,11 @@ class SpendingWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String weeklyDisplay =
|
||||
'\$${(weeklySpendCents / 100).toStringAsFixed(0)}';
|
||||
final String projectedDisplay =
|
||||
'\$${(projectedNext7DaysCents / 100).toStringAsFixed(0)}';
|
||||
|
||||
return SectionLayout(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
@@ -77,19 +76,12 @@ class SpendingWidget extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
'\$${weeklySpending.toStringAsFixed(0)}',
|
||||
weeklyDisplay,
|
||||
style: UiTypography.headline3m.copyWith(
|
||||
color: UiColors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
t.client_home.dashboard.spending.shifts_count(count: weeklyShifts),
|
||||
style: UiTypography.footnote2r.white.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.6),
|
||||
fontSize: 9,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -106,19 +98,12 @@ class SpendingWidget extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
'\$${next7DaysSpending.toStringAsFixed(0)}',
|
||||
projectedDisplay,
|
||||
style: UiTypography.headline4m.copyWith(
|
||||
color: UiColors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
t.client_home.dashboard.spending.scheduled_count(count: next7DaysScheduled),
|
||||
style: UiTypography.footnote2r.white.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.6),
|
||||
fontSize: 9,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user