Merge branch '216-p0-staff-01-profile-setup-wizard' of https://github.com/Oloodi/krow-workforce into 216-p0-staff-01-profile-setup-wizard
This commit is contained in:
@@ -22,22 +22,7 @@ class InvoiceHistorySection extends StatelessWidget {
|
||||
t.client_billing.invoice_history,
|
||||
style: UiTypography.title2b.textPrimary,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {},
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.client_billing.view_all,
|
||||
style: UiTypography.footnote2b.textPrimary,
|
||||
),
|
||||
const Icon(
|
||||
UiIcons.chevronRight,
|
||||
size: 16,
|
||||
color: UiColors.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
@@ -117,8 +102,7 @@ class _InvoiceItem extends StatelessWidget {
|
||||
_StatusBadge(status: invoice.status),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
const Icon(UiIcons.download, size: 16, color: UiColors.iconSecondary),
|
||||
const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -69,29 +69,13 @@ class _PaymentMethodCardState extends State<PaymentMethodCard> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.client_billing.payment_method,
|
||||
style: UiTypography.title2b.textPrimary,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {},
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.add,
|
||||
size: 14,
|
||||
color: UiColors.primary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
t.client_billing.add_payment,
|
||||
style: UiTypography.footnote2b.textPrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
t.client_billing.payment_method,
|
||||
style: UiTypography.title2b.textPrimary,
|
||||
),
|
||||
const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
if (account != null) ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Container(
|
||||
|
||||
@@ -50,25 +50,7 @@ class SavingsCard extends StatelessWidget {
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
SizedBox(
|
||||
height: 28,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
foregroundColor: UiColors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space3,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
),
|
||||
textStyle: UiTypography.footnote2b,
|
||||
),
|
||||
child: Text(t.client_billing.view_details),
|
||||
),
|
||||
),
|
||||
const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'data/repositories_impl/coverage_repository_impl.dart';
|
||||
import 'domain/repositories/coverage_repository.dart';
|
||||
import 'domain/usecases/get_coverage_stats_usecase.dart';
|
||||
@@ -11,7 +12,9 @@ class CoverageModule extends Module {
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repositories
|
||||
i.addSingleton<CoverageRepository>(CoverageRepositoryImpl.new);
|
||||
i.addSingleton<CoverageRepository>(
|
||||
() => CoverageRepositoryImpl(dataConnect: ExampleConnector.instance),
|
||||
);
|
||||
|
||||
// Use Cases
|
||||
i.addSingleton(GetShiftsForDateUseCase.new);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import '../../domain/repositories/coverage_repository.dart';
|
||||
import '../../domain/ui_entities/coverage_entities.dart';
|
||||
|
||||
@@ -13,82 +15,58 @@ import '../../domain/ui_entities/coverage_entities.dart';
|
||||
/// - Returns domain entities from `domain/ui_entities`.
|
||||
class CoverageRepositoryImpl implements CoverageRepository {
|
||||
/// Creates a [CoverageRepositoryImpl].
|
||||
CoverageRepositoryImpl();
|
||||
CoverageRepositoryImpl({required dc.ExampleConnector dataConnect})
|
||||
: _dataConnect = dataConnect;
|
||||
|
||||
final dc.ExampleConnector _dataConnect;
|
||||
|
||||
/// Fetches shifts for a specific date.
|
||||
@override
|
||||
Future<List<CoverageShift>> getShiftsForDate({required DateTime date}) async {
|
||||
// Simulate network delay
|
||||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
// Mock data - in production, this would come from data_connect
|
||||
final DateTime today = DateTime.now();
|
||||
final bool isToday = date.year == today.year &&
|
||||
date.month == today.month &&
|
||||
date.day == today.day;
|
||||
|
||||
if (!isToday) {
|
||||
// Return empty list for non-today dates
|
||||
final String? businessId =
|
||||
dc.ClientSessionStore.instance.session?.business?.id;
|
||||
print('Coverage: now=${DateTime.now().toIso8601String()}');
|
||||
if (businessId == null || businessId.isEmpty) {
|
||||
print('Coverage: missing businessId for date=${date.toIso8601String()}');
|
||||
return <CoverageShift>[];
|
||||
}
|
||||
|
||||
return <CoverageShift>[
|
||||
CoverageShift(
|
||||
id: '1',
|
||||
title: 'Banquet Server',
|
||||
location: 'Grand Ballroom',
|
||||
startTime: '16:00',
|
||||
workersNeeded: 10,
|
||||
date: date,
|
||||
workers: const <CoverageWorker>[
|
||||
CoverageWorker(
|
||||
name: 'Sarah Wilson',
|
||||
status: 'confirmed',
|
||||
checkInTime: '15:55',
|
||||
),
|
||||
CoverageWorker(
|
||||
name: 'Mike Ross',
|
||||
status: 'confirmed',
|
||||
checkInTime: '16:00',
|
||||
),
|
||||
CoverageWorker(
|
||||
name: 'Jane Doe',
|
||||
status: 'confirmed',
|
||||
checkInTime: null,
|
||||
),
|
||||
CoverageWorker(
|
||||
name: 'John Smith',
|
||||
status: 'late',
|
||||
checkInTime: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
CoverageShift(
|
||||
id: '2',
|
||||
title: 'Bartender',
|
||||
location: 'Lobby Bar',
|
||||
startTime: '17:00',
|
||||
workersNeeded: 4,
|
||||
date: date,
|
||||
workers: const <CoverageWorker>[
|
||||
CoverageWorker(
|
||||
name: 'Emily Blunt',
|
||||
status: 'confirmed',
|
||||
checkInTime: '16:45',
|
||||
),
|
||||
CoverageWorker(
|
||||
name: 'Chris Evans',
|
||||
status: 'confirmed',
|
||||
checkInTime: '16:50',
|
||||
),
|
||||
CoverageWorker(
|
||||
name: 'Tom Holland',
|
||||
status: 'confirmed',
|
||||
checkInTime: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
final DateTime start = DateTime(date.year, date.month, date.day);
|
||||
final DateTime end =
|
||||
DateTime(date.year, date.month, date.day, 23, 59, 59, 999);
|
||||
print(
|
||||
'Coverage: request businessId=$businessId dayStart=${start.toIso8601String()} dayEnd=${end.toIso8601String()}',
|
||||
);
|
||||
final fdc.QueryResult<
|
||||
dc.ListShiftRolesByBusinessAndDateRangeData,
|
||||
dc.ListShiftRolesByBusinessAndDateRangeVariables> shiftRolesResult =
|
||||
await _dataConnect
|
||||
.listShiftRolesByBusinessAndDateRange(
|
||||
businessId: businessId,
|
||||
start: _toTimestamp(start),
|
||||
end: _toTimestamp(end),
|
||||
)
|
||||
.execute();
|
||||
|
||||
final fdc.QueryResult<
|
||||
dc.ListStaffsApplicationsByBusinessForDayData,
|
||||
dc.ListStaffsApplicationsByBusinessForDayVariables> applicationsResult =
|
||||
await _dataConnect
|
||||
.listStaffsApplicationsByBusinessForDay(
|
||||
businessId: businessId,
|
||||
dayStart: _toTimestamp(start),
|
||||
dayEnd: _toTimestamp(end),
|
||||
)
|
||||
.execute();
|
||||
print(
|
||||
'Coverage: ${date.toIso8601String()} staffsApplications=${applicationsResult.data.applications.length}',
|
||||
);
|
||||
|
||||
return _mapCoverageShifts(
|
||||
shiftRolesResult.data.shiftRoles,
|
||||
applicationsResult.data.applications,
|
||||
date,
|
||||
);
|
||||
}
|
||||
|
||||
/// Fetches coverage statistics for a specific date.
|
||||
@@ -120,4 +98,128 @@ class CoverageRepositoryImpl implements CoverageRepository {
|
||||
late: late,
|
||||
);
|
||||
}
|
||||
|
||||
fdc.Timestamp _toTimestamp(DateTime dateTime) {
|
||||
final int seconds = dateTime.millisecondsSinceEpoch ~/ 1000;
|
||||
final int nanoseconds =
|
||||
(dateTime.millisecondsSinceEpoch % 1000) * 1000000;
|
||||
return fdc.Timestamp(nanoseconds, seconds);
|
||||
}
|
||||
|
||||
List<CoverageShift> _mapCoverageShifts(
|
||||
List<dc.ListShiftRolesByBusinessAndDateRangeShiftRoles> shiftRoles,
|
||||
List<dc.ListStaffsApplicationsByBusinessForDayApplications> applications,
|
||||
DateTime date,
|
||||
) {
|
||||
if (shiftRoles.isEmpty && applications.isEmpty) {
|
||||
return <CoverageShift>[];
|
||||
}
|
||||
|
||||
final Map<String, _CoverageGroup> groups = <String, _CoverageGroup>{};
|
||||
for (final dc.ListShiftRolesByBusinessAndDateRangeShiftRoles shiftRole
|
||||
in shiftRoles) {
|
||||
final String key = '${shiftRole.shiftId}:${shiftRole.roleId}';
|
||||
groups[key] = _CoverageGroup(
|
||||
shiftId: shiftRole.shiftId,
|
||||
roleId: shiftRole.roleId,
|
||||
title: shiftRole.role.name,
|
||||
location: shiftRole.shift.location ?? '',
|
||||
startTime: _formatTime(shiftRole.startTime) ?? '00:00',
|
||||
workersNeeded: shiftRole.count,
|
||||
date: shiftRole.shift.date?.toDateTime() ?? date,
|
||||
workers: <CoverageWorker>[],
|
||||
);
|
||||
}
|
||||
|
||||
for (final dc.ListStaffsApplicationsByBusinessForDayApplications app
|
||||
in applications) {
|
||||
final String key = '${app.shiftId}:${app.roleId}';
|
||||
final _CoverageGroup existing = groups[key] ??
|
||||
_CoverageGroup(
|
||||
shiftId: app.shiftId,
|
||||
roleId: app.roleId,
|
||||
title: app.shiftRole.role.name,
|
||||
location: app.shiftRole.shift.location ?? '',
|
||||
startTime: '00:00',
|
||||
workersNeeded: 0,
|
||||
date: date,
|
||||
workers: <CoverageWorker>[],
|
||||
);
|
||||
|
||||
existing.workers.add(
|
||||
CoverageWorker(
|
||||
name: app.staff.fullName,
|
||||
status: _mapWorkerStatus(app.status),
|
||||
checkInTime: _formatTime(app.checkInTime),
|
||||
),
|
||||
);
|
||||
groups[key] = existing;
|
||||
}
|
||||
|
||||
return groups.values
|
||||
.map(
|
||||
(_CoverageGroup group) => CoverageShift(
|
||||
id: '${group.shiftId}:${group.roleId}',
|
||||
title: group.title,
|
||||
location: group.location,
|
||||
startTime: group.startTime,
|
||||
workersNeeded: group.workersNeeded,
|
||||
date: group.date,
|
||||
workers: group.workers,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
String _mapWorkerStatus(
|
||||
dc.EnumValue<dc.ApplicationStatus> status,
|
||||
) {
|
||||
if (status is dc.Known<dc.ApplicationStatus>) {
|
||||
switch (status.value) {
|
||||
case dc.ApplicationStatus.LATE:
|
||||
return 'late';
|
||||
case dc.ApplicationStatus.CHECKED_IN:
|
||||
case dc.ApplicationStatus.CHECKED_OUT:
|
||||
case dc.ApplicationStatus.ACCEPTED:
|
||||
case dc.ApplicationStatus.CONFIRMED:
|
||||
case dc.ApplicationStatus.PENDING:
|
||||
case dc.ApplicationStatus.REJECTED:
|
||||
case dc.ApplicationStatus.NO_SHOW:
|
||||
return 'confirmed';
|
||||
}
|
||||
}
|
||||
return 'confirmed';
|
||||
}
|
||||
|
||||
String? _formatTime(fdc.Timestamp? timestamp) {
|
||||
if (timestamp == null) {
|
||||
return null;
|
||||
}
|
||||
final DateTime date = timestamp.toDateTime();
|
||||
final String hour = date.hour.toString().padLeft(2, '0');
|
||||
final String minute = date.minute.toString().padLeft(2, '0');
|
||||
return '$hour:$minute';
|
||||
}
|
||||
}
|
||||
|
||||
class _CoverageGroup {
|
||||
_CoverageGroup({
|
||||
required this.shiftId,
|
||||
required this.roleId,
|
||||
required this.title,
|
||||
required this.location,
|
||||
required this.startTime,
|
||||
required this.workersNeeded,
|
||||
required this.date,
|
||||
required this.workers,
|
||||
});
|
||||
|
||||
final String shiftId;
|
||||
final String roleId;
|
||||
final String title;
|
||||
final String location;
|
||||
final String startTime;
|
||||
final int workersNeeded;
|
||||
final DateTime date;
|
||||
final List<CoverageWorker> workers;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ class CoverageHeader extends StatelessWidget {
|
||||
gradient: LinearGradient(
|
||||
colors: <Color>[
|
||||
UiColors.primary,
|
||||
UiColors.accent,
|
||||
UiColors.primary,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
|
||||
@@ -17,8 +17,95 @@ class HomeRepositoryImpl implements HomeRepositoryInterface {
|
||||
HomeRepositoryImpl(this._mock, this._dataConnect);
|
||||
|
||||
@override
|
||||
Future<HomeDashboardData> getDashboardData() {
|
||||
return _mock.getDashboardData();
|
||||
Future<HomeDashboardData> getDashboardData() async {
|
||||
final String? businessId = ClientSessionStore.instance.session?.business?.id;
|
||||
if (businessId == null || businessId.isEmpty) {
|
||||
return const HomeDashboardData(
|
||||
weeklySpending: 0,
|
||||
next7DaysSpending: 0,
|
||||
weeklyShifts: 0,
|
||||
next7DaysScheduled: 0,
|
||||
totalNeeded: 0,
|
||||
totalFilled: 0,
|
||||
);
|
||||
}
|
||||
|
||||
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 = DateTime(monday.year, monday.month, monday.day);
|
||||
final DateTime weekRangeEnd =
|
||||
DateTime(monday.year, monday.month, monday.day + 13, 23, 59, 59, 999);
|
||||
final fdc.QueryResult<
|
||||
GetCompletedShiftsByBusinessIdData,
|
||||
GetCompletedShiftsByBusinessIdVariables> completedResult =
|
||||
await _dataConnect
|
||||
.getCompletedShiftsByBusinessId(
|
||||
businessId: businessId,
|
||||
dateFrom: _toTimestamp(weekRangeStart),
|
||||
dateTo: _toTimestamp(weekRangeEnd),
|
||||
)
|
||||
.execute();
|
||||
print(
|
||||
'Home spending: businessId=$businessId dateFrom=${weekRangeStart.toIso8601String()} '
|
||||
'dateTo=${weekRangeEnd.toIso8601String()} shifts=${completedResult.data.shifts.length}',
|
||||
);
|
||||
|
||||
double weeklySpending = 0.0;
|
||||
double next7DaysSpending = 0.0;
|
||||
int weeklyShifts = 0;
|
||||
int next7DaysScheduled = 0;
|
||||
for (final GetCompletedShiftsByBusinessIdShifts shift
|
||||
in completedResult.data.shifts) {
|
||||
final DateTime? shiftDate = shift.date?.toDateTime();
|
||||
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 = DateTime(now.year, now.month, now.day, 23, 59, 59, 999);
|
||||
|
||||
final fdc.QueryResult<
|
||||
ListShiftRolesByBusinessAndDateRangeData,
|
||||
ListShiftRolesByBusinessAndDateRangeVariables> result =
|
||||
await _dataConnect
|
||||
.listShiftRolesByBusinessAndDateRange(
|
||||
businessId: businessId,
|
||||
start: _toTimestamp(start),
|
||||
end: _toTimestamp(end),
|
||||
)
|
||||
.execute();
|
||||
|
||||
int totalNeeded = 0;
|
||||
int totalFilled = 0;
|
||||
for (final 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,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -21,10 +21,16 @@ class ClientHomeState extends Equatable {
|
||||
this.widgetOrder = const <String>[
|
||||
'actions',
|
||||
'reorder',
|
||||
'coverage',
|
||||
'spending',
|
||||
'liveActivity',
|
||||
],
|
||||
this.widgetVisibility = const <String, bool>{
|
||||
'actions': true,
|
||||
'reorder': true,
|
||||
'coverage': true,
|
||||
'spending': true,
|
||||
'liveActivity': true,
|
||||
},
|
||||
this.isEditMode = false,
|
||||
this.errorMessage,
|
||||
|
||||
@@ -94,7 +94,9 @@ class DashboardWidgetBuilder extends StatelessWidget {
|
||||
: 0,
|
||||
);
|
||||
case 'liveActivity':
|
||||
return LiveActivityWidget(onViewAllPressed: () {});
|
||||
return LiveActivityWidget(
|
||||
onViewAllPressed: () => Modular.to.navigate('/client-main/coverage/'),
|
||||
);
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
@@ -1,44 +1,107 @@
|
||||
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 'coverage_dashboard.dart';
|
||||
|
||||
/// A widget that displays live activity information.
|
||||
class LiveActivityWidget extends StatelessWidget {
|
||||
class LiveActivityWidget extends StatefulWidget {
|
||||
/// Callback when "View all" is pressed.
|
||||
final VoidCallback onViewAllPressed;
|
||||
|
||||
/// Creates a [LiveActivityWidget].
|
||||
const LiveActivityWidget({super.key, required this.onViewAllPressed});
|
||||
|
||||
@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.ListStaffsApplicationsByBusinessForDayData,
|
||||
dc.ListStaffsApplicationsByBusinessForDayVariables> result =
|
||||
await dc.ExampleConnector.instance
|
||||
.listStaffsApplicationsByBusinessForDay(
|
||||
businessId: businessId,
|
||||
dayStart: _toTimestamp(start),
|
||||
dayEnd: _toTimestamp(end),
|
||||
)
|
||||
.execute();
|
||||
|
||||
if (result.data.applications.isEmpty) {
|
||||
return _LiveActivityData.empty();
|
||||
}
|
||||
|
||||
final Map<String, _LiveShiftAggregate> aggregates =
|
||||
<String, _LiveShiftAggregate>{};
|
||||
for (final dc.ListStaffsApplicationsByBusinessForDayApplications app
|
||||
in result.data.applications) {
|
||||
final String key = '${app.shiftId}:${app.roleId}';
|
||||
final _LiveShiftAggregate aggregate = aggregates[key] ??
|
||||
_LiveShiftAggregate(
|
||||
workersNeeded: app.shiftRole.count,
|
||||
assigned: app.shiftRole.assigned ?? 0,
|
||||
cost: app.shiftRole.shift.cost ?? 0,
|
||||
);
|
||||
aggregates[key] = aggregate;
|
||||
}
|
||||
|
||||
int totalNeeded = 0;
|
||||
int totalAssigned = 0;
|
||||
double totalCost = 0;
|
||||
for (final _LiveShiftAggregate aggregate in aggregates.values) {
|
||||
totalNeeded += aggregate.workersNeeded;
|
||||
totalAssigned += aggregate.assigned;
|
||||
totalCost += aggregate.cost;
|
||||
}
|
||||
|
||||
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 int seconds = dateTime.millisecondsSinceEpoch ~/ 1000;
|
||||
final int nanoseconds =
|
||||
(dateTime.millisecondsSinceEpoch % 1000) * 1000000;
|
||||
return fdc.Timestamp(nanoseconds, seconds);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsClientHomeEn i18n = t.client_home;
|
||||
|
||||
// Mock data
|
||||
final List<Map<String, Object>> shifts = <Map<String, Object>>[
|
||||
<String, Object>{
|
||||
'workersNeeded': 5,
|
||||
'filled': 4,
|
||||
'hourlyRate': 20.0,
|
||||
'status': 'OPEN',
|
||||
'date': DateTime.now().toIso8601String().split('T')[0],
|
||||
},
|
||||
<String, Object>{
|
||||
'workersNeeded': 5,
|
||||
'filled': 5,
|
||||
'hourlyRate': 22.0,
|
||||
'status': 'FILLED',
|
||||
'date': DateTime.now().toIso8601String().split('T')[0],
|
||||
},
|
||||
];
|
||||
final List<Map<String, String>> applications = <Map<String, String>>[
|
||||
<String, String>{'status': 'CONFIRMED', 'checkInTime': '09:00'},
|
||||
<String, String>{'status': 'CONFIRMED', 'checkInTime': '09:05'},
|
||||
<String, String>{'status': 'CONFIRMED'},
|
||||
<String, String>{'status': 'LATE'},
|
||||
];
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
@@ -51,7 +114,7 @@ class LiveActivityWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: onViewAllPressed,
|
||||
onTap: widget.onViewAllPressed,
|
||||
child: Text(
|
||||
i18n.dashboard.view_all,
|
||||
style: UiTypography.footnote1m.copyWith(
|
||||
@@ -62,8 +125,82 @@ class LiveActivityWidget extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
CoverageDashboard(shifts: shifts, applications: applications),
|
||||
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': data.totalAssigned == 0
|
||||
? 0.0
|
||||
: data.totalCost / data.totalAssigned,
|
||||
'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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LiveActivityData {
|
||||
const _LiveActivityData({
|
||||
required this.totalNeeded,
|
||||
required this.totalAssigned,
|
||||
required this.totalCost,
|
||||
required this.checkedInCount,
|
||||
required this.lateCount,
|
||||
});
|
||||
|
||||
final int totalNeeded;
|
||||
final int totalAssigned;
|
||||
final double totalCost;
|
||||
final int checkedInCount;
|
||||
final int lateCount;
|
||||
|
||||
factory _LiveActivityData.empty() {
|
||||
return const _LiveActivityData(
|
||||
totalNeeded: 0,
|
||||
totalAssigned: 0,
|
||||
totalCost: 0,
|
||||
checkedInCount: 0,
|
||||
lateCount: 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LiveShiftAggregate {
|
||||
_LiveShiftAggregate({
|
||||
required this.workersNeeded,
|
||||
required this.assigned,
|
||||
required this.cost,
|
||||
});
|
||||
|
||||
final int workersNeeded;
|
||||
final int assigned;
|
||||
final double cost;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user