view shits of ordes by date

This commit is contained in:
José Salazar
2026-01-23 20:43:18 -05:00
parent 5c159493dd
commit afba0e64df
20 changed files with 20246 additions and 18735 deletions

View File

@@ -107,7 +107,7 @@ class ClientCreateOrderRepositoryImpl
end.isBefore(start) ? end.add(const Duration(days: 1)) : end;
final hours = normalizedEnd.difference(start).inMinutes / 60.0;
final rate = order.roleRates[position.role] ?? 0;
final totalValue = rate * hours;
final totalValue = rate * hours * position.count;
await _dataConnect
.createShiftRole(
@@ -143,7 +143,7 @@ class ClientCreateOrderRepositoryImpl
end.isBefore(start) ? end.add(const Duration(days: 1)) : end;
final hours = normalizedEnd.difference(start).inMinutes / 60.0;
final rate = order.roleRates[position.role] ?? 0;
total += rate * hours;
total += rate * hours * position.count;
}
return total;
}

View File

@@ -1,17 +1,155 @@
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
import 'package:intl/intl.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart' as domain;
import '../../domain/repositories/i_view_orders_repository.dart';
/// Implementation of [IViewOrdersRepository] providing data from [OrderRepositoryMock].
/// Implementation of [IViewOrdersRepository] using Data Connect.
class ViewOrdersRepositoryImpl implements IViewOrdersRepository {
final OrderRepositoryMock _orderRepositoryMock;
final firebase.FirebaseAuth _firebaseAuth;
final dc.ExampleConnector _dataConnect;
/// Creates a [ViewOrdersRepositoryImpl] with the given [OrderRepositoryMock].
ViewOrdersRepositoryImpl({required OrderRepositoryMock orderRepositoryMock})
: _orderRepositoryMock = orderRepositoryMock;
ViewOrdersRepositoryImpl({
required firebase.FirebaseAuth firebaseAuth,
required dc.ExampleConnector dataConnect,
}) : _firebaseAuth = firebaseAuth,
_dataConnect = dataConnect;
@override
Future<List<OrderItem>> getOrders() {
return _orderRepositoryMock.getOrders();
Future<List<domain.OrderItem>> getOrdersForRange({
required DateTime start,
required DateTime end,
}) async {
final businessId = dc.ClientSessionStore.instance.session?.business?.id;
if (businessId == null || businessId.isEmpty) {
await _firebaseAuth.signOut();
throw Exception('Business is missing. Please sign in again.');
}
final startTimestamp = _toTimestamp(_startOfDay(start));
final endTimestamp = _toTimestamp(_endOfDay(end));
final result = await _dataConnect
.listShiftRolesByBusinessAndDateRange(
businessId: businessId,
start: startTimestamp,
end: endTimestamp,
)
.execute();
final businessName =
dc.ClientSessionStore.instance.session?.business?.businessName ??
'Your Company';
return result.data.shiftRoles.map((shiftRole) {
print(
'ViewOrders shiftRole: shiftId=${shiftRole.shiftId} roleId=${shiftRole.roleId} '
'startTime=${shiftRole.startTime?.toJson()} endTime=${shiftRole.endTime?.toJson()} '
'hours=${shiftRole.hours} totalValue=${shiftRole.totalValue}',
);
final shiftDate = shiftRole.shift.date?.toDateTime();
final dateStr = shiftDate == null
? ''
: DateFormat('yyyy-MM-dd').format(shiftDate);
final startTime = _formatTime(shiftRole.startTime);
final endTime = _formatTime(shiftRole.endTime);
final filled = shiftRole.assigned ?? 0;
final workersNeeded = shiftRole.count;
final hours = shiftRole.hours ?? 0;
final totalValue = shiftRole.totalValue ?? 0;
final hourlyRate = _hourlyRate(shiftRole.totalValue, shiftRole.hours);
final status = filled >= workersNeeded ? 'filled' : 'open';
return domain.OrderItem(
id: _shiftRoleKey(shiftRole.shiftId, shiftRole.roleId),
title: '${shiftRole.role.name} - ${shiftRole.shift.title}',
clientName: businessName,
status: status,
date: dateStr,
startTime: startTime,
endTime: endTime,
location: shiftRole.shift.location ?? '',
locationAddress: shiftRole.shift.locationAddress ?? '',
filled: filled,
workersNeeded: workersNeeded,
hourlyRate: hourlyRate,
hours: hours,
totalValue: totalValue,
confirmedApps: const <Map<String, dynamic>>[],
);
}).toList();
}
@override
Future<Map<String, List<Map<String, dynamic>>>> getAcceptedApplicationsForDay(
DateTime day,
) async {
final businessId = dc.ClientSessionStore.instance.session?.business?.id;
if (businessId == null || businessId.isEmpty) {
await _firebaseAuth.signOut();
throw Exception('Business is missing. Please sign in again.');
}
final dayStart = _toTimestamp(_startOfDay(day));
final dayEnd = _toTimestamp(_endOfDay(day));
final result = await _dataConnect
.listAcceptedApplicationsByBusinessForDay(
businessId: businessId,
dayStart: dayStart,
dayEnd: dayEnd,
)
.execute();
final Map<String, List<Map<String, dynamic>>> grouped = {};
for (final application in result.data.applications) {
print(
'ViewOrders app: shiftId=${application.shiftId} roleId=${application.roleId} '
'checkIn=${application.checkInTime?.toJson()} checkOut=${application.checkOutTime?.toJson()}',
);
final key = _shiftRoleKey(application.shiftId, application.roleId);
grouped.putIfAbsent(key, () => <Map<String, dynamic>>[]);
grouped[key]!.add(<String, dynamic>{
'id': application.id,
'worker_id': application.staff.id,
'worker_name': application.staff.fullName,
'status': 'confirmed',
'photo_url': application.staff.photoUrl,
});
}
return grouped;
}
String _shiftRoleKey(String shiftId, String roleId) {
return '$shiftId:$roleId';
}
fdc.Timestamp _toTimestamp(DateTime dateTime) {
final utc = dateTime.toUtc();
final seconds = utc.millisecondsSinceEpoch ~/ 1000;
final nanoseconds = (utc.microsecondsSinceEpoch % 1000000) * 1000;
return fdc.Timestamp(nanoseconds, seconds);
}
DateTime _startOfDay(DateTime dateTime) {
return DateTime(dateTime.year, dateTime.month, dateTime.day);
}
DateTime _endOfDay(DateTime dateTime) {
return DateTime(dateTime.year, dateTime.month, dateTime.day, 23, 59, 59);
}
String _formatTime(fdc.Timestamp? timestamp) {
if (timestamp == null) {
return '';
}
final dateTime = timestamp.toDateTime();
return DateFormat('HH:mm').format(dateTime);
}
double _hourlyRate(double? totalValue, double? hours) {
if (totalValue == null || hours == null || hours == 0) {
return 0;
}
return totalValue / hours;
}
}

View File

@@ -0,0 +1,10 @@
import 'package:krow_core/core.dart';
class OrdersDayArguments extends UseCaseArgument {
const OrdersDayArguments({required this.day});
final DateTime day;
@override
List<Object?> get props => <Object?>[day];
}

View File

@@ -0,0 +1,14 @@
import 'package:krow_core/core.dart';
class OrdersRangeArguments extends UseCaseArgument {
const OrdersRangeArguments({
required this.start,
required this.end,
});
final DateTime start;
final DateTime end;
@override
List<Object?> get props => <Object?>[start, end];
}

View File

@@ -3,5 +3,13 @@ import 'package:krow_domain/krow_domain.dart';
/// Repository interface for fetching and managing client orders.
abstract class IViewOrdersRepository {
/// Fetches a list of [OrderItem] for the client.
Future<List<OrderItem>> getOrders();
Future<List<OrderItem>> getOrdersForRange({
required DateTime start,
required DateTime end,
});
/// Fetches accepted staff applications for the given day, grouped by shift+role.
Future<Map<String, List<Map<String, dynamic>>>> getAcceptedApplicationsForDay(
DateTime day,
);
}

View File

@@ -0,0 +1,17 @@
import 'package:krow_core/core.dart';
import '../repositories/i_view_orders_repository.dart';
import '../arguments/orders_day_arguments.dart';
class GetAcceptedApplicationsForDayUseCase
implements UseCase<OrdersDayArguments, Map<String, List<Map<String, dynamic>>>> {
const GetAcceptedApplicationsForDayUseCase(this._repository);
final IViewOrdersRepository _repository;
@override
Future<Map<String, List<Map<String, dynamic>>>> call(
OrdersDayArguments input,
) {
return _repository.getAcceptedApplicationsForDay(input.day);
}
}

View File

@@ -1,19 +1,24 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/i_view_orders_repository.dart';
import '../arguments/orders_range_arguments.dart';
/// Use case for retrieving the list of client orders.
///
/// This use case encapsulates the business rule of fetching orders
/// and delegates the data retrieval to the [IViewOrdersRepository].
class GetOrdersUseCase implements NoInputUseCase<List<OrderItem>> {
class GetOrdersUseCase
implements UseCase<OrdersRangeArguments, List<OrderItem>> {
final IViewOrdersRepository _repository;
/// Creates a [GetOrdersUseCase] with the required [IViewOrdersRepository].
GetOrdersUseCase(this._repository);
@override
Future<List<OrderItem>> call() {
return _repository.getOrders();
Future<List<OrderItem>> call(OrdersRangeArguments input) {
return _repository.getOrdersForRange(
start: input.start,
end: input.end,
);
}
}

View File

@@ -1,6 +1,9 @@
import 'package:intl/intl.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/arguments/orders_day_arguments.dart';
import '../../domain/arguments/orders_range_arguments.dart';
import '../../domain/usecases/get_accepted_applications_for_day_use_case.dart';
import '../../domain/usecases/get_orders_use_case.dart';
import 'view_orders_state.dart';
@@ -8,25 +11,43 @@ import 'view_orders_state.dart';
///
/// This Cubit handles loading orders, date selection, and tab filtering.
class ViewOrdersCubit extends Cubit<ViewOrdersState> {
ViewOrdersCubit({required GetOrdersUseCase getOrdersUseCase})
ViewOrdersCubit({
required GetOrdersUseCase getOrdersUseCase,
required GetAcceptedApplicationsForDayUseCase getAcceptedAppsUseCase,
})
: _getOrdersUseCase = getOrdersUseCase,
_getAcceptedAppsUseCase = getAcceptedAppsUseCase,
super(ViewOrdersState(selectedDate: DateTime.now())) {
_init();
}
final GetOrdersUseCase _getOrdersUseCase;
final GetAcceptedApplicationsForDayUseCase _getAcceptedAppsUseCase;
void _init() {
updateWeekOffset(0); // Initialize calendar days
loadOrders();
}
/// Loads the list of orders using the [GetOrdersUseCase].
Future<void> loadOrders() async {
Future<void> _loadOrdersForRange({
required DateTime rangeStart,
required DateTime rangeEnd,
required DateTime dayForApps,
}) async {
emit(state.copyWith(status: ViewOrdersStatus.loading));
try {
final List<OrderItem> orders = await _getOrdersUseCase();
emit(state.copyWith(status: ViewOrdersStatus.success, orders: orders));
final List<OrderItem> orders = await _getOrdersUseCase(
OrdersRangeArguments(start: rangeStart, end: rangeEnd),
);
final apps = await _getAcceptedAppsUseCase(
OrdersDayArguments(day: dayForApps),
);
final List<OrderItem> updatedOrders = _applyApplications(orders, apps);
emit(
state.copyWith(
status: ViewOrdersStatus.success,
orders: updatedOrders,
),
);
_updateDerivedState();
} catch (_) {
emit(state.copyWith(status: ViewOrdersStatus.failure));
@@ -35,7 +56,7 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState> {
void selectDate(DateTime date) {
emit(state.copyWith(selectedDate: date));
_updateDerivedState();
_refreshAcceptedApplications(date);
}
void selectFilterTab(String tabId) {
@@ -46,8 +67,25 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState> {
void updateWeekOffset(int offset) {
final int newWeekOffset = state.weekOffset + offset;
final List<DateTime> calendarDays = _calculateCalendarDays(newWeekOffset);
emit(state.copyWith(weekOffset: newWeekOffset, calendarDays: calendarDays));
_updateDerivedState();
final DateTime? selectedDate = state.selectedDate;
final DateTime updatedSelectedDate =
selectedDate != null &&
calendarDays.any((day) => _isSameDay(day, selectedDate))
? selectedDate
: calendarDays.first;
emit(
state.copyWith(
weekOffset: newWeekOffset,
calendarDays: calendarDays,
selectedDate: updatedSelectedDate,
),
);
_loadOrdersForRange(
rangeStart: calendarDays.first,
rangeEnd: calendarDays.last,
dayForApps: updatedSelectedDate,
);
}
void _updateDerivedState() {
@@ -66,6 +104,57 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState> {
);
}
Future<void> _refreshAcceptedApplications(DateTime day) async {
try {
final apps = await _getAcceptedAppsUseCase(
OrdersDayArguments(day: day),
);
final List<OrderItem> updatedOrders =
_applyApplications(state.orders, apps);
emit(state.copyWith(orders: updatedOrders));
_updateDerivedState();
} catch (_) {
// Keep existing data on failure.
}
}
List<OrderItem> _applyApplications(
List<OrderItem> orders,
Map<String, List<Map<String, dynamic>>> apps,
) {
return orders.map((order) {
final confirmed = apps[order.id] ?? const <Map<String, dynamic>>[];
if (confirmed.isEmpty) {
return order;
}
final filled = confirmed.length;
final status =
filled >= order.workersNeeded ? 'filled' : order.status;
return OrderItem(
id: order.id,
title: order.title,
clientName: order.clientName,
status: status,
date: order.date,
startTime: order.startTime,
endTime: order.endTime,
location: order.location,
locationAddress: order.locationAddress,
filled: filled,
workersNeeded: order.workersNeeded,
hourlyRate: order.hourlyRate,
hours: order.hours,
totalValue: order.totalValue,
confirmedApps: confirmed,
);
}).toList();
}
bool _isSameDay(DateTime a, DateTime b) {
return a.year == b.year && a.month == b.month && a.day == b.day;
}
List<DateTime> _calculateCalendarDays(int weekOffset) {
final DateTime now = DateTime.now();
final int jsDay = now.weekday == 7 ? 0 : now.weekday;

View File

@@ -112,12 +112,8 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
? ((order.filled / order.workersNeeded) * 100).round()
: 0;
// Simulation of cost/hours calculation
const double hours = 8.0;
final double cost =
order.hourlyRate *
hours *
(order.filled > 0 ? order.filled : order.workersNeeded);
final double hours = order.hours;
final double cost = order.totalValue;
return Container(
decoration: BoxDecoration(
@@ -730,25 +726,7 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
}
double _calculateTotalCost() {
double total = 0;
for (final Map<String, dynamic> pos in _positions) {
double hours = 8.0;
try {
final List<String> startParts = pos['start_time'].toString().split(':');
final List<String> endParts = pos['end_time'].toString().split(':');
final double startH =
int.parse(startParts[0]) + int.parse(startParts[1]) / 60;
final double endH =
int.parse(endParts[0]) + int.parse(endParts[1]) / 60;
hours = endH - startH;
if (hours < 0) hours += 24;
} catch (_) {}
final double rate =
_selectedVendor?.rates[pos['role']] ?? widget.order.hourlyRate;
total += hours * rate * (pos['count'] as int);
}
return total;
return widget.order.totalValue;
}
@override

View File

@@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'data/repositories/view_orders_repository_impl.dart';
import 'domain/repositories/i_view_orders_repository.dart';
import 'domain/usecases/get_accepted_applications_for_day_use_case.dart';
import 'domain/usecases/get_orders_use_case.dart';
import 'presentation/blocs/view_orders_cubit.dart';
import 'presentation/pages/view_orders_page.dart';
@@ -21,15 +23,22 @@ class ViewOrdersModule extends Module {
// Repositories
i.addLazySingleton<IViewOrdersRepository>(
() => ViewOrdersRepositoryImpl(
orderRepositoryMock: i.get<OrderRepositoryMock>(),
firebaseAuth: firebase.FirebaseAuth.instance,
dataConnect: ExampleConnector.instance,
),
);
// UseCases
i.addLazySingleton(GetOrdersUseCase.new);
i.addLazySingleton(GetAcceptedApplicationsForDayUseCase.new);
// BLoCs
i.addSingleton(ViewOrdersCubit.new);
i.addSingleton(
() => ViewOrdersCubit(
getOrdersUseCase: i.get<GetOrdersUseCase>(),
getAcceptedAppsUseCase: i.get<GetAcceptedApplicationsForDayUseCase>(),
),
);
}
@override

View File

@@ -25,7 +25,6 @@ dependencies:
path: ../../../domain
krow_core:
path: ../../../core
# UI
lucide_icons: ^0.257.0
intl: ^0.20.1