Merge pull request #445 from Oloodi/feature/session-persistence-new

feat: architecture overhaul, launchpad-style reports, and uber-style #443
This commit is contained in:
Achintha Isuru
2026-02-20 15:31:11 -05:00
committed by GitHub
288 changed files with 5586 additions and 3800 deletions

View File

@@ -161,7 +161,7 @@ jobs:
FAILED_FILES=() FAILED_FILES=()
while IFS= read -r file; do while IFS= read -r file; do
if [[ -n "$file" && "$file" == *.dart ]]; then if [[ -n "$file" && "$file" == *.dart && -f "$file" ]]; then
echo "📝 Analyzing: $file" echo "📝 Analyzing: $file"
if ! dart analyze "$file" 2>&1 | tee -a lint_output.txt; then if ! dart analyze "$file" 2>&1 | tee -a lint_output.txt; then

2
.gitignore vendored
View File

@@ -83,6 +83,8 @@ node_modules/
dist/ dist/
dist-ssr/ dist-ssr/
coverage/ coverage/
!**/lib/**/coverage/
!**/src/**/coverage/
.nyc_output/ .nyc_output/
.vite/ .vite/
.temp/ .temp/

View File

@@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:staff_authentication/staff_authentication.dart';
/// A widget that listens to session state changes and handles global reactions. /// A widget that listens to session state changes and handles global reactions.
/// ///

View File

@@ -1,4 +1,4 @@
library core; library;
export 'src/domain/arguments/usecase_argument.dart'; export 'src/domain/arguments/usecase_argument.dart';
export 'src/domain/usecases/usecase.dart'; export 'src/domain/usecases/usecase.dart';

View File

@@ -1,3 +1,4 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports
import 'dart:developer' as developer; import 'dart:developer' as developer;
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@@ -22,16 +23,16 @@ import 'package:flutter_bloc/flutter_bloc.dart';
/// } /// }
/// ``` /// ```
class CoreBlocObserver extends BlocObserver { class CoreBlocObserver extends BlocObserver {
/// Whether to log state changes (can be verbose in production)
final bool logStateChanges;
/// Whether to log events
final bool logEvents;
CoreBlocObserver({ CoreBlocObserver({
this.logStateChanges = false, this.logStateChanges = false,
this.logEvents = true, this.logEvents = true,
}); });
/// Whether to log state changes (can be verbose in production)
final bool logStateChanges;
/// Whether to log events
final bool logEvents;
@override @override
void onCreate(BlocBase bloc) { void onCreate(BlocBase bloc) {
@@ -58,7 +59,7 @@ class CoreBlocObserver extends BlocObserver {
super.onChange(bloc, change); super.onChange(bloc, change);
if (logStateChanges) { if (logStateChanges) {
developer.log( developer.log(
'State: ${change.currentState.runtimeType} ${change.nextState.runtimeType}', 'State: ${change.currentState.runtimeType} → ${change.nextState.runtimeType}',
name: bloc.runtimeType.toString(), name: bloc.runtimeType.toString(),
); );
} }
@@ -108,9 +109,10 @@ class CoreBlocObserver extends BlocObserver {
super.onTransition(bloc, transition); super.onTransition(bloc, transition);
if (logStateChanges) { if (logStateChanges) {
developer.log( developer.log(
'Transition: ${transition.event.runtimeType} ${transition.nextState.runtimeType}', 'Transition: ${transition.event.runtimeType} → ${transition.nextState.runtimeType}',
name: bloc.runtimeType.toString(), name: bloc.runtimeType.toString(),
); );
} }
} }
} }

View File

@@ -41,6 +41,7 @@
/// final homePath = ClientPaths.home; /// final homePath = ClientPaths.home;
/// final shiftsPath = StaffPaths.shifts; /// final shiftsPath = StaffPaths.shifts;
/// ``` /// ```
library;
export 'client/route_paths.dart'; export 'client/route_paths.dart';
export 'client/navigator.dart'; export 'client/navigator.dart';

View File

@@ -184,6 +184,13 @@ extension StaffNavigator on IModularNavigator {
pushNamed(StaffPaths.onboardingPersonalInfo); pushNamed(StaffPaths.onboardingPersonalInfo);
} }
/// Pushes the preferred locations editing page.
///
/// Allows staff to search and manage their preferred US work locations.
void toPreferredLocations() {
pushNamed(StaffPaths.preferredLocations);
}
/// Pushes the emergency contact page. /// Pushes the emergency contact page.
/// ///
/// Manage emergency contact details for safety purposes. /// Manage emergency contact details for safety purposes.

View File

@@ -128,6 +128,12 @@ class StaffPaths {
static const String languageSelection = static const String languageSelection =
'/worker-main/personal-info/language-selection/'; '/worker-main/personal-info/language-selection/';
/// Preferred locations editing page.
///
/// Allows staff to search and select their preferred US work locations.
static const String preferredLocations =
'/worker-main/personal-info/preferred-locations/';
/// Emergency contact information. /// Emergency contact information.
/// ///
/// Manage emergency contact details for safety purposes. /// Manage emergency contact details for safety purposes.

View File

@@ -8,11 +8,11 @@ sealed class LocaleEvent {
/// Event triggered when the user wants to change the application locale. /// Event triggered when the user wants to change the application locale.
class ChangeLocale extends LocaleEvent { class ChangeLocale extends LocaleEvent {
/// The new locale to apply.
final Locale locale;
/// Creates a [ChangeLocale] event. /// Creates a [ChangeLocale] event.
const ChangeLocale(this.locale); const ChangeLocale(this.locale);
/// The new locale to apply.
final Locale locale;
} }
/// Event triggered to load the saved locale from persistent storage. /// Event triggered to load the saved locale from persistent storage.

View File

@@ -11,11 +11,11 @@ abstract interface class LocaleLocalDataSource {
/// Implementation of [LocaleLocalDataSource] using [SharedPreferencesAsync]. /// Implementation of [LocaleLocalDataSource] using [SharedPreferencesAsync].
class LocaleLocalDataSourceImpl implements LocaleLocalDataSource { class LocaleLocalDataSourceImpl implements LocaleLocalDataSource {
static const String _localeKey = 'app_locale';
final SharedPreferencesAsync _sharedPreferences;
/// Creates a [LocaleLocalDataSourceImpl] with the required [SharedPreferencesAsync] instance. /// Creates a [LocaleLocalDataSourceImpl] with the required [SharedPreferencesAsync] instance.
LocaleLocalDataSourceImpl(this._sharedPreferences); LocaleLocalDataSourceImpl(this._sharedPreferences);
static const String _localeKey = 'app_locale';
final SharedPreferencesAsync _sharedPreferences;
@override @override
Future<void> saveLanguageCode(String languageCode) async { Future<void> saveLanguageCode(String languageCode) async {

View File

@@ -3,10 +3,10 @@ import '../repositories/locale_repository_interface.dart';
/// Use case to retrieve the default locale. /// Use case to retrieve the default locale.
class GetDefaultLocaleUseCase { class GetDefaultLocaleUseCase {
final LocaleRepositoryInterface _repository;
/// Creates a [GetDefaultLocaleUseCase] with the required [LocaleRepositoryInterface]. /// Creates a [GetDefaultLocaleUseCase] with the required [LocaleRepositoryInterface].
GetDefaultLocaleUseCase(this._repository); GetDefaultLocaleUseCase(this._repository);
final LocaleRepositoryInterface _repository;
/// Retrieves the default locale. /// Retrieves the default locale.
Locale call() { Locale call() {

View File

@@ -7,10 +7,10 @@ import '../repositories/locale_repository_interface.dart';
/// This class extends [NoInputUseCase] and interacts with [LocaleRepositoryInterface] /// This class extends [NoInputUseCase] and interacts with [LocaleRepositoryInterface]
/// to fetch the saved locale. /// to fetch the saved locale.
class GetLocaleUseCase extends NoInputUseCase<Locale?> { class GetLocaleUseCase extends NoInputUseCase<Locale?> {
final LocaleRepositoryInterface _repository;
/// Creates a [GetLocaleUseCase] with the required [LocaleRepositoryInterface]. /// Creates a [GetLocaleUseCase] with the required [LocaleRepositoryInterface].
GetLocaleUseCase(this._repository); GetLocaleUseCase(this._repository);
final LocaleRepositoryInterface _repository;
@override @override
Future<Locale> call() { Future<Locale> call() {

View File

@@ -3,10 +3,10 @@ import '../repositories/locale_repository_interface.dart';
/// Use case to retrieve the list of supported locales. /// Use case to retrieve the list of supported locales.
class GetSupportedLocalesUseCase { class GetSupportedLocalesUseCase {
final LocaleRepositoryInterface _repository;
/// Creates a [GetSupportedLocalesUseCase] with the required [LocaleRepositoryInterface]. /// Creates a [GetSupportedLocalesUseCase] with the required [LocaleRepositoryInterface].
GetSupportedLocalesUseCase(this._repository); GetSupportedLocalesUseCase(this._repository);
final LocaleRepositoryInterface _repository;
/// Retrieves the supported locales. /// Retrieves the supported locales.
List<Locale> call() { List<Locale> call() {

View File

@@ -7,10 +7,10 @@ import '../repositories/locale_repository_interface.dart';
/// This class extends [UseCase] and interacts with [LocaleRepositoryInterface] /// This class extends [UseCase] and interacts with [LocaleRepositoryInterface]
/// to save a given locale. /// to save a given locale.
class SetLocaleUseCase extends UseCase<Locale, void> { class SetLocaleUseCase extends UseCase<Locale, void> {
final LocaleRepositoryInterface _repository;
/// Creates a [SetLocaleUseCase] with the required [LocaleRepositoryInterface]. /// Creates a [SetLocaleUseCase] with the required [LocaleRepositoryInterface].
SetLocaleUseCase(this._repository); SetLocaleUseCase(this._repository);
final LocaleRepositoryInterface _repository;
@override @override
Future<void> call(Locale input) { Future<void> call(Locale input) {

View File

@@ -605,8 +605,21 @@
"languages_hint": "English, Spanish, French...", "languages_hint": "English, Spanish, French...",
"locations_label": "Preferred Locations", "locations_label": "Preferred Locations",
"locations_hint": "Downtown, Midtown, Brooklyn...", "locations_hint": "Downtown, Midtown, Brooklyn...",
"locations_summary_none": "Not set",
"save_button": "Save Changes", "save_button": "Save Changes",
"save_success": "Personal info saved successfully" "save_success": "Personal info saved successfully",
"preferred_locations": {
"title": "Preferred Locations",
"description": "Choose up to 5 locations in the US where you prefer to work. We'll prioritize shifts near these areas.",
"search_hint": "Search a city or area...",
"added_label": "YOUR LOCATIONS",
"max_reached": "You've reached the maximum of 5 locations",
"min_hint": "Add at least 1 preferred location",
"save_button": "Save Locations",
"save_success": "Preferred locations saved",
"remove_tooltip": "Remove location",
"empty_state": "No locations added yet.\nSearch above to add your preferred work areas."
}
}, },
"experience": { "experience": {
"title": "Experience & Skills", "title": "Experience & Skills",
@@ -1304,17 +1317,31 @@
}, },
"forecast_report": { "forecast_report": {
"title": "Forecast Report", "title": "Forecast Report",
"subtitle": "Projected spend & staffing", "subtitle": "Next 4 weeks projection",
"metrics": { "metrics": {
"projected_spend": "Projected Spend", "four_week_forecast": "4-Week Forecast",
"workers_needed": "Workers Needed" "avg_weekly": "Avg Weekly",
"total_shifts": "Total Shifts",
"total_hours": "Total Hours"
},
"badges": {
"total_projected": "Total projected",
"per_week": "Per week",
"scheduled": "Scheduled",
"worker_hours": "Worker hours"
}, },
"chart_title": "Spending Forecast", "chart_title": "Spending Forecast",
"daily_projections": "DAILY PROJECTIONS", "weekly_breakdown": {
"empty_state": "No projections available", "title": "WEEKLY BREAKDOWN",
"shift_item": { "week": "Week $index",
"workers_needed": "$count workers needed" "shifts": "Shifts",
"hours": "Hours",
"avg_shift": "Avg/Shift"
}, },
"buttons": {
"export": "Export"
},
"empty_state": "No projections available",
"placeholders": { "placeholders": {
"export_message": "Exporting Forecast Report (Placeholder)" "export_message": "Exporting Forecast Report (Placeholder)"
} }

View File

@@ -605,8 +605,21 @@
"languages_hint": "Inglés, Español, Francés...", "languages_hint": "Inglés, Español, Francés...",
"locations_label": "Ubicaciones Preferidas", "locations_label": "Ubicaciones Preferidas",
"locations_hint": "Centro, Midtown, Brooklyn...", "locations_hint": "Centro, Midtown, Brooklyn...",
"locations_summary_none": "No configurado",
"save_button": "Guardar Cambios", "save_button": "Guardar Cambios",
"save_success": "Información personal guardada exitosamente" "save_success": "Información personal guardada exitosamente",
"preferred_locations": {
"title": "Ubicaciones Preferidas",
"description": "Elige hasta 5 ubicaciones en los EE.UU. donde prefieres trabajar. Priorizaremos turnos cerca de estas áreas.",
"search_hint": "Buscar una ciudad o área...",
"added_label": "TUS UBICACIONES",
"max_reached": "Has alcanzado el máximo de 5 ubicaciones",
"min_hint": "Agrega al menos 1 ubicación preferida",
"save_button": "Guardar Ubicaciones",
"save_success": "Ubicaciones preferidas guardadas",
"remove_tooltip": "Eliminar ubicación",
"empty_state": "Aún no has agregado ubicaciones.\nBusca arriba para agregar tus áreas de trabajo preferidas."
}
}, },
"experience": { "experience": {
"title": "Experiencia y habilidades", "title": "Experiencia y habilidades",
@@ -1304,17 +1317,31 @@
}, },
"forecast_report": { "forecast_report": {
"title": "Informe de Previsión", "title": "Informe de Previsión",
"subtitle": "Gastos y personal proyectados", "subtitle": "Proyección próximas 4 semanas",
"metrics": { "metrics": {
"projected_spend": "Gasto Proyectado", "four_week_forecast": "Previsión 4 Semanas",
"workers_needed": "Trabajadores Necesarios" "avg_weekly": "Promedio Semanal",
"total_shifts": "Total de Turnos",
"total_hours": "Total de Horas"
},
"badges": {
"total_projected": "Total proyectado",
"per_week": "Por semana",
"scheduled": "Programado",
"worker_hours": "Horas de trabajo"
}, },
"chart_title": "Previsión de Gastos", "chart_title": "Previsión de Gastos",
"daily_projections": "PROYECCIONES DIARIAS", "weekly_breakdown": {
"empty_state": "No hay proyecciones disponibles", "title": "DESGLOSE SEMANAL",
"shift_item": { "week": "Semana $index",
"workers_needed": "$count trabajadores necesarios" "shifts": "Turnos",
"hours": "Horas",
"avg_shift": "Prom./Turno"
}, },
"buttons": {
"export": "Exportar"
},
"empty_state": "No hay proyecciones disponibles",
"placeholders": { "placeholders": {
"export_message": "Exportando Informe de Previsión (Marcador de posición)" "export_message": "Exportando Informe de Previsión (Marcador de posición)"
} }

View File

@@ -27,4 +27,28 @@ export 'src/connectors/staff/domain/usecases/get_experience_completion_usecase.d
export 'src/connectors/staff/domain/usecases/get_tax_forms_completion_usecase.dart'; export 'src/connectors/staff/domain/usecases/get_tax_forms_completion_usecase.dart';
export 'src/connectors/staff/domain/usecases/get_staff_profile_usecase.dart'; export 'src/connectors/staff/domain/usecases/get_staff_profile_usecase.dart';
export 'src/connectors/staff/domain/usecases/sign_out_staff_usecase.dart'; export 'src/connectors/staff/domain/usecases/sign_out_staff_usecase.dart';
export 'src/connectors/staff/data/repositories/staff_connector_repository_impl.dart'; export 'src/connectors/staff/data/repositories/staff_connector_repository_impl.dart';
// Export Reports Connector
export 'src/connectors/reports/domain/repositories/reports_connector_repository.dart';
export 'src/connectors/reports/data/repositories/reports_connector_repository_impl.dart';
// Export Shifts Connector
export 'src/connectors/shifts/domain/repositories/shifts_connector_repository.dart';
export 'src/connectors/shifts/data/repositories/shifts_connector_repository_impl.dart';
// Export Hubs Connector
export 'src/connectors/hubs/domain/repositories/hubs_connector_repository.dart';
export 'src/connectors/hubs/data/repositories/hubs_connector_repository_impl.dart';
// Export Billing Connector
export 'src/connectors/billing/domain/repositories/billing_connector_repository.dart';
export 'src/connectors/billing/data/repositories/billing_connector_repository_impl.dart';
// Export Home Connector
export 'src/connectors/home/domain/repositories/home_connector_repository.dart';
export 'src/connectors/home/data/repositories/home_connector_repository_impl.dart';
// Export Coverage Connector
export 'src/connectors/coverage/domain/repositories/coverage_connector_repository.dart';
export 'src/connectors/coverage/data/repositories/coverage_connector_repository_impl.dart';

View File

@@ -0,0 +1,202 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:firebase_data_connect/src/core/ref.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/billing_connector_repository.dart';
/// Implementation of [BillingConnectorRepository].
class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
BillingConnectorRepositoryImpl({
dc.DataConnectService? service,
}) : _service = service ?? dc.DataConnectService.instance;
final dc.DataConnectService _service;
@override
Future<List<BusinessBankAccount>> getBankAccounts({required String businessId}) async {
return _service.run(() async {
final QueryResult<dc.GetAccountsByOwnerIdData, dc.GetAccountsByOwnerIdVariables> result = await _service.connector
.getAccountsByOwnerId(ownerId: businessId)
.execute();
return result.data.accounts.map(_mapBankAccount).toList();
});
}
@override
Future<double> getCurrentBillAmount({required String businessId}) async {
return _service.run(() async {
final QueryResult<dc.ListInvoicesByBusinessIdData, dc.ListInvoicesByBusinessIdVariables> result = await _service.connector
.listInvoicesByBusinessId(businessId: businessId)
.execute();
return result.data.invoices
.map(_mapInvoice)
.where((Invoice i) => i.status == InvoiceStatus.open)
.fold<double>(0.0, (double sum, Invoice item) => sum + item.totalAmount);
});
}
@override
Future<List<Invoice>> getInvoiceHistory({required String businessId}) async {
return _service.run(() async {
final QueryResult<dc.ListInvoicesByBusinessIdData, dc.ListInvoicesByBusinessIdVariables> result = await _service.connector
.listInvoicesByBusinessId(businessId: businessId)
.limit(10)
.execute();
return result.data.invoices.map(_mapInvoice).toList();
});
}
@override
Future<List<Invoice>> getPendingInvoices({required String businessId}) async {
return _service.run(() async {
final QueryResult<dc.ListInvoicesByBusinessIdData, dc.ListInvoicesByBusinessIdVariables> result = await _service.connector
.listInvoicesByBusinessId(businessId: businessId)
.execute();
return result.data.invoices
.map(_mapInvoice)
.where((Invoice i) =>
i.status == InvoiceStatus.open || i.status == InvoiceStatus.disputed)
.toList();
});
}
@override
Future<List<InvoiceItem>> getSpendingBreakdown({
required String businessId,
required BillingPeriod period,
}) async {
return _service.run(() async {
final DateTime now = DateTime.now();
final DateTime start;
final DateTime end;
if (period == BillingPeriod.week) {
final int daysFromMonday = now.weekday - DateTime.monday;
final DateTime monday = DateTime(now.year, now.month, now.day)
.subtract(Duration(days: daysFromMonday));
start = monday;
end = monday.add(const Duration(days: 6, hours: 23, minutes: 59, seconds: 59));
} else {
start = DateTime(now.year, now.month, 1);
end = DateTime(now.year, now.month + 1, 0, 23, 59, 59);
}
final QueryResult<dc.ListShiftRolesByBusinessAndDatesSummaryData, dc.ListShiftRolesByBusinessAndDatesSummaryVariables> result = await _service.connector
.listShiftRolesByBusinessAndDatesSummary(
businessId: businessId,
start: _service.toTimestamp(start),
end: _service.toTimestamp(end),
)
.execute();
final List<dc.ListShiftRolesByBusinessAndDatesSummaryShiftRoles> shiftRoles = result.data.shiftRoles;
if (shiftRoles.isEmpty) return <InvoiceItem>[];
final Map<String, _RoleSummary> summary = <String, _RoleSummary>{};
for (final dc.ListShiftRolesByBusinessAndDatesSummaryShiftRoles role in shiftRoles) {
final String roleId = role.roleId;
final String roleName = role.role.name;
final double hours = role.hours ?? 0.0;
final double totalValue = role.totalValue ?? 0.0;
final _RoleSummary? existing = summary[roleId];
if (existing == null) {
summary[roleId] = _RoleSummary(
roleId: roleId,
roleName: roleName,
totalHours: hours,
totalValue: totalValue,
);
} else {
summary[roleId] = existing.copyWith(
totalHours: existing.totalHours + hours,
totalValue: existing.totalValue + totalValue,
);
}
}
return summary.values
.map((_RoleSummary item) => InvoiceItem(
id: item.roleId,
invoiceId: item.roleId,
staffId: item.roleName,
workHours: item.totalHours,
rate: item.totalHours > 0 ? item.totalValue / item.totalHours : 0,
amount: item.totalValue,
))
.toList();
});
}
// --- MAPPERS ---
Invoice _mapInvoice(dynamic invoice) {
return Invoice(
id: invoice.id,
eventId: invoice.orderId,
businessId: invoice.businessId,
status: _mapInvoiceStatus(invoice.status.stringValue),
totalAmount: invoice.amount,
workAmount: invoice.amount,
addonsAmount: invoice.otherCharges ?? 0,
invoiceNumber: invoice.invoiceNumber,
issueDate: _service.toDateTime(invoice.issueDate)!,
);
}
BusinessBankAccount _mapBankAccount(dynamic account) {
return BusinessBankAccountAdapter.fromPrimitives(
id: account.id,
bank: account.bank,
last4: account.last4,
isPrimary: account.isPrimary ?? false,
expiryTime: _service.toDateTime(account.expiryTime),
);
}
InvoiceStatus _mapInvoiceStatus(String status) {
switch (status) {
case 'PAID':
return InvoiceStatus.paid;
case 'OVERDUE':
return InvoiceStatus.overdue;
case 'DISPUTED':
return InvoiceStatus.disputed;
case 'APPROVED':
return InvoiceStatus.verified;
default:
return InvoiceStatus.open;
}
}
}
class _RoleSummary {
const _RoleSummary({
required this.roleId,
required this.roleName,
required this.totalHours,
required this.totalValue,
});
final String roleId;
final String roleName;
final double totalHours;
final double totalValue;
_RoleSummary copyWith({
double? totalHours,
double? totalValue,
}) {
return _RoleSummary(
roleId: roleId,
roleName: roleName,
totalHours: totalHours ?? this.totalHours,
totalValue: totalValue ?? this.totalValue,
);
}
}

View File

@@ -0,0 +1,24 @@
import 'package:krow_domain/krow_domain.dart';
/// Repository interface for billing connector operations.
///
/// This acts as a buffer layer between the domain repository and the Data Connect SDK.
abstract interface class BillingConnectorRepository {
/// Fetches bank accounts associated with the business.
Future<List<BusinessBankAccount>> getBankAccounts({required String businessId});
/// Fetches the current bill amount for the period.
Future<double> getCurrentBillAmount({required String businessId});
/// Fetches historically paid invoices.
Future<List<Invoice>> getInvoiceHistory({required String businessId});
/// Fetches pending invoices (Open or Disputed).
Future<List<Invoice>> getPendingInvoices({required String businessId});
/// Fetches the breakdown of spending.
Future<List<InvoiceItem>> getSpendingBreakdown({
required String businessId,
required BillingPeriod period,
});
}

View File

@@ -0,0 +1,158 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:firebase_data_connect/src/core/ref.dart';
import 'package:intl/intl.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/coverage_connector_repository.dart';
/// Implementation of [CoverageConnectorRepository].
class CoverageConnectorRepositoryImpl implements CoverageConnectorRepository {
CoverageConnectorRepositoryImpl({
dc.DataConnectService? service,
}) : _service = service ?? dc.DataConnectService.instance;
final dc.DataConnectService _service;
@override
Future<List<CoverageShift>> getShiftsForDate({
required String businessId,
required DateTime date,
}) async {
return _service.run(() async {
final DateTime start = DateTime(date.year, date.month, date.day);
final DateTime end = DateTime(date.year, date.month, date.day, 23, 59, 59, 999);
final QueryResult<dc.ListShiftRolesByBusinessAndDateRangeData, dc.ListShiftRolesByBusinessAndDateRangeVariables> shiftRolesResult = await _service.connector
.listShiftRolesByBusinessAndDateRange(
businessId: businessId,
start: _service.toTimestamp(start),
end: _service.toTimestamp(end),
)
.execute();
final QueryResult<dc.ListStaffsApplicationsByBusinessForDayData, dc.ListStaffsApplicationsByBusinessForDayVariables> applicationsResult = await _service.connector
.listStaffsApplicationsByBusinessForDay(
businessId: businessId,
dayStart: _service.toTimestamp(start),
dayEnd: _service.toTimestamp(end),
)
.execute();
return _mapCoverageShifts(
shiftRolesResult.data.shiftRoles,
applicationsResult.data.applications,
date,
);
});
}
List<CoverageShift> _mapCoverageShifts(
List<dynamic> shiftRoles,
List<dynamic> applications,
DateTime date,
) {
if (shiftRoles.isEmpty && applications.isEmpty) return <CoverageShift>[];
final Map<String, _CoverageGroup> groups = <String, _CoverageGroup>{};
for (final sr in shiftRoles) {
final String key = '${sr.shiftId}:${sr.roleId}';
final DateTime? startTime = _service.toDateTime(sr.startTime);
groups[key] = _CoverageGroup(
shiftId: sr.shiftId,
roleId: sr.roleId,
title: sr.role.name,
location: sr.shift.location ?? sr.shift.locationAddress ?? '',
startTime: startTime != null ? DateFormat('HH:mm').format(startTime) : '00:00',
workersNeeded: sr.count,
date: _service.toDateTime(sr.shift.date) ?? date,
workers: <CoverageWorker>[],
);
}
for (final app in applications) {
final String key = '${app.shiftId}:${app.roleId}';
if (!groups.containsKey(key)) {
final DateTime? startTime = _service.toDateTime(app.shiftRole.startTime);
groups[key] = _CoverageGroup(
shiftId: app.shiftId,
roleId: app.roleId,
title: app.shiftRole.role.name,
location: app.shiftRole.shift.location ?? app.shiftRole.shift.locationAddress ?? '',
startTime: startTime != null ? DateFormat('HH:mm').format(startTime) : '00:00',
workersNeeded: app.shiftRole.count,
date: _service.toDateTime(app.shiftRole.shift.date) ?? date,
workers: <CoverageWorker>[],
);
}
final DateTime? checkIn = _service.toDateTime(app.checkInTime);
groups[key]!.workers.add(
CoverageWorker(
name: app.staff.fullName,
status: _mapWorkerStatus(app.status.stringValue),
checkInTime: checkIn != null ? DateFormat('HH:mm').format(checkIn) : null,
),
);
}
return groups.values
.map((_CoverageGroup g) => CoverageShift(
id: '${g.shiftId}:${g.roleId}',
title: g.title,
location: g.location,
startTime: g.startTime,
workersNeeded: g.workersNeeded,
date: g.date,
workers: g.workers,
))
.toList();
}
CoverageWorkerStatus _mapWorkerStatus(String status) {
switch (status) {
case 'PENDING':
return CoverageWorkerStatus.pending;
case 'REJECTED':
return CoverageWorkerStatus.rejected;
case 'CONFIRMED':
return CoverageWorkerStatus.confirmed;
case 'CHECKED_IN':
return CoverageWorkerStatus.checkedIn;
case 'CHECKED_OUT':
return CoverageWorkerStatus.checkedOut;
case 'LATE':
return CoverageWorkerStatus.late;
case 'NO_SHOW':
return CoverageWorkerStatus.noShow;
case 'COMPLETED':
return CoverageWorkerStatus.completed;
default:
return CoverageWorkerStatus.pending;
}
}
}
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;
}

View File

@@ -0,0 +1,12 @@
import 'package:krow_domain/krow_domain.dart';
/// Repository interface for coverage connector operations.
///
/// This acts as a buffer layer between the domain repository and the Data Connect SDK.
abstract interface class CoverageConnectorRepository {
/// Fetches coverage data for a specific date and business.
Future<List<CoverageShift>> getShiftsForDate({
required String businessId,
required DateTime date,
});
}

View File

@@ -0,0 +1,113 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:firebase_data_connect/src/core/ref.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/home_connector_repository.dart';
/// Implementation of [HomeConnectorRepository].
class HomeConnectorRepositoryImpl implements HomeConnectorRepository {
HomeConnectorRepositoryImpl({
dc.DataConnectService? service,
}) : _service = service ?? dc.DataConnectService.instance;
final dc.DataConnectService _service;
@override
Future<HomeDashboardData> getDashboardData({required String businessId}) async {
return _service.run(() async {
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,
);
});
}
@override
Future<List<ReorderItem>> getRecentReorders({required String businessId}) async {
return _service.run(() async {
final DateTime now = DateTime.now();
final DateTime start = now.subtract(const Duration(days: 30));
final QueryResult<dc.ListShiftRolesByBusinessDateRangeCompletedOrdersData, dc.ListShiftRolesByBusinessDateRangeCompletedOrdersVariables> result = await _service.connector
.listShiftRolesByBusinessDateRangeCompletedOrders(
businessId: businessId,
start: _service.toTimestamp(start),
end: _service.toTimestamp(now),
)
.execute();
return result.data.shiftRoles.map((dc.ListShiftRolesByBusinessDateRangeCompletedOrdersShiftRoles shiftRole) {
final String location = shiftRole.shift.location ?? shiftRole.shift.locationAddress ?? '';
final String type = shiftRole.shift.order.orderType.stringValue;
return ReorderItem(
orderId: shiftRole.shift.order.id,
title: '${shiftRole.role.name} - ${shiftRole.shift.title}',
location: location,
hourlyRate: shiftRole.role.costPerHour,
hours: shiftRole.hours ?? 0,
workers: shiftRole.count,
type: type,
);
}).toList();
});
}
}

View File

@@ -0,0 +1,12 @@
import 'package:krow_domain/krow_domain.dart';
/// Repository interface for home connector operations.
///
/// This acts as a buffer layer between the domain repository and the Data Connect SDK.
abstract interface class HomeConnectorRepository {
/// Fetches dashboard data for a business.
Future<HomeDashboardData> getDashboardData({required String businessId});
/// Fetches recent reorder items for a business.
Future<List<ReorderItem>> getRecentReorders({required String businessId});
}

View File

@@ -0,0 +1,262 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'dart:convert';
import 'package:firebase_data_connect/src/core/ref.dart';
import 'package:http/http.dart' as http;
import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/hubs_connector_repository.dart';
/// Implementation of [HubsConnectorRepository].
class HubsConnectorRepositoryImpl implements HubsConnectorRepository {
HubsConnectorRepositoryImpl({
dc.DataConnectService? service,
}) : _service = service ?? dc.DataConnectService.instance;
final dc.DataConnectService _service;
@override
Future<List<Hub>> getHubs({required String businessId}) async {
return _service.run(() async {
final String teamId = await _getOrCreateTeamId(businessId);
final QueryResult<dc.GetTeamHubsByTeamIdData, dc.GetTeamHubsByTeamIdVariables> response = await _service.connector
.getTeamHubsByTeamId(teamId: teamId)
.execute();
return response.data.teamHubs.map((dc.GetTeamHubsByTeamIdTeamHubs h) {
return Hub(
id: h.id,
businessId: businessId,
name: h.hubName,
address: h.address,
nfcTagId: null,
status: h.isActive ? HubStatus.active : HubStatus.inactive,
);
}).toList();
});
}
@override
Future<Hub> createHub({
required String businessId,
required String name,
required String address,
String? placeId,
double? latitude,
double? longitude,
String? city,
String? state,
String? street,
String? country,
String? zipCode,
}) async {
return _service.run(() async {
final String teamId = await _getOrCreateTeamId(businessId);
final _PlaceAddress? placeAddress = (placeId != null && placeId.isNotEmpty)
? await _fetchPlaceAddress(placeId)
: null;
final OperationResult<dc.CreateTeamHubData, dc.CreateTeamHubVariables> result = await _service.connector
.createTeamHub(
teamId: teamId,
hubName: name,
address: address,
)
.placeId(placeId)
.latitude(latitude)
.longitude(longitude)
.city(city ?? placeAddress?.city ?? '')
.state(state ?? placeAddress?.state)
.street(street ?? placeAddress?.street)
.country(country ?? placeAddress?.country)
.zipCode(zipCode ?? placeAddress?.zipCode)
.execute();
return Hub(
id: result.data.teamHub_insert.id,
businessId: businessId,
name: name,
address: address,
nfcTagId: null,
status: HubStatus.active,
);
});
}
@override
Future<Hub> updateHub({
required String businessId,
required String id,
String? name,
String? address,
String? placeId,
double? latitude,
double? longitude,
String? city,
String? state,
String? street,
String? country,
String? zipCode,
}) async {
return _service.run(() async {
final _PlaceAddress? placeAddress = (placeId != null && placeId.isNotEmpty)
? await _fetchPlaceAddress(placeId)
: null;
final dc.UpdateTeamHubVariablesBuilder builder = _service.connector.updateTeamHub(id: id);
if (name != null) builder.hubName(name);
if (address != null) builder.address(address);
if (placeId != null) builder.placeId(placeId);
if (latitude != null) builder.latitude(latitude);
if (longitude != null) builder.longitude(longitude);
if (city != null || placeAddress?.city != null) {
builder.city(city ?? placeAddress?.city);
}
if (state != null || placeAddress?.state != null) {
builder.state(state ?? placeAddress?.state);
}
if (street != null || placeAddress?.street != null) {
builder.street(street ?? placeAddress?.street);
}
if (country != null || placeAddress?.country != null) {
builder.country(country ?? placeAddress?.country);
}
if (zipCode != null || placeAddress?.zipCode != null) {
builder.zipCode(zipCode ?? placeAddress?.zipCode);
}
await builder.execute();
// Return a basic hub object reflecting changes (or we could re-fetch)
return Hub(
id: id,
businessId: businessId,
name: name ?? '',
address: address ?? '',
nfcTagId: null,
status: HubStatus.active,
);
});
}
@override
Future<void> deleteHub({required String businessId, required String id}) async {
return _service.run(() async {
final QueryResult<dc.ListOrdersByBusinessAndTeamHubData, dc.ListOrdersByBusinessAndTeamHubVariables> ordersRes = await _service.connector
.listOrdersByBusinessAndTeamHub(businessId: businessId, teamHubId: id)
.execute();
if (ordersRes.data.orders.isNotEmpty) {
throw HubHasOrdersException(
technicalMessage: 'Hub $id has ${ordersRes.data.orders.length} orders',
);
}
await _service.connector.deleteTeamHub(id: id).execute();
});
}
// --- HELPERS ---
Future<String> _getOrCreateTeamId(String businessId) async {
final QueryResult<dc.GetTeamsByOwnerIdData, dc.GetTeamsByOwnerIdVariables> teamsRes = await _service.connector
.getTeamsByOwnerId(ownerId: businessId)
.execute();
if (teamsRes.data.teams.isNotEmpty) {
return teamsRes.data.teams.first.id;
}
// Logic to fetch business details to create a team name if missing
// For simplicity, we assume one exists or we create a generic one
final OperationResult<dc.CreateTeamData, dc.CreateTeamVariables> createRes = await _service.connector
.createTeam(
teamName: 'Business Team',
ownerId: businessId,
ownerName: '',
ownerRole: 'OWNER',
)
.execute();
return createRes.data.team_insert.id;
}
Future<_PlaceAddress?> _fetchPlaceAddress(String placeId) async {
final Uri uri = Uri.https(
'maps.googleapis.com',
'/maps/api/place/details/json',
<String, dynamic>{
'place_id': placeId,
'fields': 'address_component',
'key': AppConfig.googleMapsApiKey,
},
);
try {
final http.Response response = await http.get(uri);
if (response.statusCode != 200) return null;
final Map<String, dynamic> payload = json.decode(response.body) as Map<String, dynamic>;
if (payload['status'] != 'OK') return null;
final Map<String, dynamic>? result = payload['result'] as Map<String, dynamic>?;
final List<dynamic>? components = result?['address_components'] as List<dynamic>?;
if (components == null || components.isEmpty) return null;
String? streetNumber, route, city, state, country, zipCode;
for (var entry in components) {
final Map<String, dynamic> component = entry as Map<String, dynamic>;
final List<dynamic> types = component['types'] as List<dynamic>? ?? <dynamic>[];
final String? longName = component['long_name'] as String?;
final String? shortName = component['short_name'] as String?;
if (types.contains('street_number')) {
streetNumber = longName;
} else if (types.contains('route')) {
route = longName;
} else if (types.contains('locality')) {
city = longName;
} else if (types.contains('administrative_area_level_1')) {
state = shortName ?? longName;
} else if (types.contains('country')) {
country = shortName ?? longName;
} else if (types.contains('postal_code')) {
zipCode = longName;
}
}
final String street = <String?>[streetNumber, route]
.where((String? v) => v != null && v.isNotEmpty)
.join(' ')
.trim();
return _PlaceAddress(
street: street.isEmpty ? null : street,
city: city,
state: state,
country: country,
zipCode: zipCode,
);
} catch (_) {
return null;
}
}
}
class _PlaceAddress {
const _PlaceAddress({
this.street,
this.city,
this.state,
this.country,
this.zipCode,
});
final String? street;
final String? city;
final String? state;
final String? country;
final String? zipCode;
}

View File

@@ -0,0 +1,43 @@
import 'package:krow_domain/krow_domain.dart';
/// Repository interface for hubs connector operations.
///
/// This acts as a buffer layer between the domain repository and the Data Connect SDK.
abstract interface class HubsConnectorRepository {
/// Fetches the list of hubs for a business.
Future<List<Hub>> getHubs({required String businessId});
/// Creates a new hub.
Future<Hub> createHub({
required String businessId,
required String name,
required String address,
String? placeId,
double? latitude,
double? longitude,
String? city,
String? state,
String? street,
String? country,
String? zipCode,
});
/// Updates an existing hub.
Future<Hub> updateHub({
required String businessId,
required String id,
String? name,
String? address,
String? placeId,
double? latitude,
double? longitude,
String? city,
String? state,
String? street,
String? country,
String? zipCode,
});
/// Deletes a hub.
Future<void> deleteHub({required String businessId, required String id});
}

View File

@@ -0,0 +1,537 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/reports_connector_repository.dart';
/// Implementation of [ReportsConnectorRepository].
///
/// Fetches report-related data from the Data Connect backend.
class ReportsConnectorRepositoryImpl implements ReportsConnectorRepository {
/// Creates a new [ReportsConnectorRepositoryImpl].
ReportsConnectorRepositoryImpl({
dc.DataConnectService? service,
}) : _service = service ?? dc.DataConnectService.instance;
final dc.DataConnectService _service;
@override
Future<DailyOpsReport> getDailyOpsReport({
String? businessId,
required DateTime date,
}) async {
return _service.run(() async {
final String id = businessId ?? await _service.getBusinessId();
final QueryResult<dc.ListShiftsForDailyOpsByBusinessData, dc.ListShiftsForDailyOpsByBusinessVariables> response = await _service.connector
.listShiftsForDailyOpsByBusiness(
businessId: id,
date: _service.toTimestamp(date),
)
.execute();
final List<dc.ListShiftsForDailyOpsByBusinessShifts> shifts = response.data.shifts;
final int scheduledShifts = shifts.length;
int workersConfirmed = 0;
int inProgressShifts = 0;
int completedShifts = 0;
final List<DailyOpsShift> dailyOpsShifts = <DailyOpsShift>[];
for (final dc.ListShiftsForDailyOpsByBusinessShifts shift in shifts) {
workersConfirmed += shift.filled ?? 0;
final String statusStr = shift.status?.stringValue ?? '';
if (statusStr == 'IN_PROGRESS') inProgressShifts++;
if (statusStr == 'COMPLETED') completedShifts++;
dailyOpsShifts.add(DailyOpsShift(
id: shift.id,
title: shift.title ?? '',
location: shift.location ?? '',
startTime: shift.startTime?.toDateTime() ?? DateTime.now(),
endTime: shift.endTime?.toDateTime() ?? DateTime.now(),
workersNeeded: shift.workersNeeded ?? 0,
filled: shift.filled ?? 0,
status: statusStr,
));
}
return DailyOpsReport(
scheduledShifts: scheduledShifts,
workersConfirmed: workersConfirmed,
inProgressShifts: inProgressShifts,
completedShifts: completedShifts,
shifts: dailyOpsShifts,
);
});
}
@override
Future<SpendReport> getSpendReport({
String? businessId,
required DateTime startDate,
required DateTime endDate,
}) async {
return _service.run(() async {
final String id = businessId ?? await _service.getBusinessId();
final QueryResult<dc.ListInvoicesForSpendByBusinessData, dc.ListInvoicesForSpendByBusinessVariables> response = await _service.connector
.listInvoicesForSpendByBusiness(
businessId: id,
startDate: _service.toTimestamp(startDate),
endDate: _service.toTimestamp(endDate),
)
.execute();
final List<dc.ListInvoicesForSpendByBusinessInvoices> invoices = response.data.invoices;
double totalSpend = 0.0;
int paidInvoices = 0;
int pendingInvoices = 0;
int overdueInvoices = 0;
final List<SpendInvoice> spendInvoices = <SpendInvoice>[];
final Map<DateTime, double> dailyAggregates = <DateTime, double>{};
final Map<String, double> industryAggregates = <String, double>{};
for (final dc.ListInvoicesForSpendByBusinessInvoices inv in invoices) {
final double amount = (inv.amount ?? 0.0).toDouble();
totalSpend += amount;
final String statusStr = inv.status.stringValue;
if (statusStr == 'PAID') {
paidInvoices++;
} else if (statusStr == 'PENDING') {
pendingInvoices++;
} else if (statusStr == 'OVERDUE') {
overdueInvoices++;
}
final String industry = inv.vendor.serviceSpecialty ?? 'Other';
industryAggregates[industry] = (industryAggregates[industry] ?? 0.0) + amount;
final DateTime issueDateTime = inv.issueDate.toDateTime();
spendInvoices.add(SpendInvoice(
id: inv.id,
invoiceNumber: inv.invoiceNumber ?? '',
issueDate: issueDateTime,
amount: amount,
status: statusStr,
vendorName: inv.vendor.companyName ?? 'Unknown',
industry: industry,
));
// Chart data aggregation
final DateTime date = DateTime(issueDateTime.year, issueDateTime.month, issueDateTime.day);
dailyAggregates[date] = (dailyAggregates[date] ?? 0.0) + amount;
}
// Ensure chart data covers all days in range
final Map<DateTime, double> completeDailyAggregates = <DateTime, double>{};
for (int i = 0; i <= endDate.difference(startDate).inDays; i++) {
final DateTime date = startDate.add(Duration(days: i));
final DateTime normalizedDate = DateTime(date.year, date.month, date.day);
completeDailyAggregates[normalizedDate] =
dailyAggregates[normalizedDate] ?? 0.0;
}
final List<SpendChartPoint> chartData = completeDailyAggregates.entries
.map((MapEntry<DateTime, double> e) => SpendChartPoint(date: e.key, amount: e.value))
.toList()
..sort((SpendChartPoint a, SpendChartPoint b) => a.date.compareTo(b.date));
final List<SpendIndustryCategory> industryBreakdown = industryAggregates.entries
.map((MapEntry<String, double> e) => SpendIndustryCategory(
name: e.key,
amount: e.value,
percentage: totalSpend > 0 ? (e.value / totalSpend * 100) : 0,
))
.toList()
..sort((SpendIndustryCategory a, SpendIndustryCategory b) => b.amount.compareTo(a.amount));
final int daysCount = endDate.difference(startDate).inDays + 1;
return SpendReport(
totalSpend: totalSpend,
averageCost: daysCount > 0 ? totalSpend / daysCount : 0,
paidInvoices: paidInvoices,
pendingInvoices: pendingInvoices,
overdueInvoices: overdueInvoices,
invoices: spendInvoices,
chartData: chartData,
industryBreakdown: industryBreakdown,
);
});
}
@override
Future<CoverageReport> getCoverageReport({
String? businessId,
required DateTime startDate,
required DateTime endDate,
}) async {
return _service.run(() async {
final String id = businessId ?? await _service.getBusinessId();
final QueryResult<dc.ListShiftsForCoverageData, dc.ListShiftsForCoverageVariables> response = await _service.connector
.listShiftsForCoverage(
businessId: id,
startDate: _service.toTimestamp(startDate),
endDate: _service.toTimestamp(endDate),
)
.execute();
final List<dc.ListShiftsForCoverageShifts> shifts = response.data.shifts;
int totalNeeded = 0;
int totalFilled = 0;
final Map<DateTime, (int, int)> dailyStats = <DateTime, (int, int)>{};
for (final dc.ListShiftsForCoverageShifts shift in shifts) {
final DateTime shiftDate = shift.date?.toDateTime() ?? DateTime.now();
final DateTime date = DateTime(shiftDate.year, shiftDate.month, shiftDate.day);
final int needed = shift.workersNeeded ?? 0;
final int filled = shift.filled ?? 0;
totalNeeded += needed;
totalFilled += filled;
final (int, int) current = dailyStats[date] ?? (0, 0);
dailyStats[date] = (current.$1 + needed, current.$2 + filled);
}
final List<CoverageDay> dailyCoverage = dailyStats.entries.map((MapEntry<DateTime, (int, int)> e) {
final int needed = e.value.$1;
final int filled = e.value.$2;
return CoverageDay(
date: e.key,
needed: needed,
filled: filled,
percentage: needed == 0 ? 100.0 : (filled / needed) * 100.0,
);
}).toList()..sort((CoverageDay a, CoverageDay b) => a.date.compareTo(b.date));
return CoverageReport(
overallCoverage: totalNeeded == 0 ? 100.0 : (totalFilled / totalNeeded) * 100.0,
totalNeeded: totalNeeded,
totalFilled: totalFilled,
dailyCoverage: dailyCoverage,
);
});
}
@override
Future<ForecastReport> getForecastReport({
String? businessId,
required DateTime startDate,
required DateTime endDate,
}) async {
return _service.run(() async {
final String id = businessId ?? await _service.getBusinessId();
final QueryResult<dc.ListShiftsForForecastByBusinessData, dc.ListShiftsForForecastByBusinessVariables> response = await _service.connector
.listShiftsForForecastByBusiness(
businessId: id,
startDate: _service.toTimestamp(startDate),
endDate: _service.toTimestamp(endDate),
)
.execute();
final List<dc.ListShiftsForForecastByBusinessShifts> shifts = response.data.shifts;
double projectedSpend = 0.0;
int projectedWorkers = 0;
double totalHours = 0.0;
final Map<DateTime, (double, int)> dailyStats = <DateTime, (double, int)>{};
// Weekly stats: index -> (cost, count, hours)
final Map<int, (double, int, double)> weeklyStats = <int, (double, int, double)>{
0: (0.0, 0, 0.0),
1: (0.0, 0, 0.0),
2: (0.0, 0, 0.0),
3: (0.0, 0, 0.0),
};
for (final dc.ListShiftsForForecastByBusinessShifts shift in shifts) {
final DateTime shiftDate = shift.date?.toDateTime() ?? DateTime.now();
final DateTime date = DateTime(shiftDate.year, shiftDate.month, shiftDate.day);
final double cost = (shift.cost ?? 0.0).toDouble();
final int workers = shift.workersNeeded ?? 0;
final double hoursVal = (shift.hours ?? 0).toDouble();
final double shiftTotalHours = hoursVal * workers;
projectedSpend += cost;
projectedWorkers += workers;
totalHours += shiftTotalHours;
final (double, int) current = dailyStats[date] ?? (0.0, 0);
dailyStats[date] = (current.$1 + cost, current.$2 + workers);
// Weekly logic
final int diffDays = shiftDate.difference(startDate).inDays;
if (diffDays >= 0) {
final int weekIndex = diffDays ~/ 7;
if (weekIndex < 4) {
final (double, int, double) wCurrent = weeklyStats[weekIndex]!;
weeklyStats[weekIndex] = (
wCurrent.$1 + cost,
wCurrent.$2 + 1,
wCurrent.$3 + shiftTotalHours,
);
}
}
}
final List<ForecastPoint> chartData = dailyStats.entries.map((MapEntry<DateTime, (double, int)> e) {
return ForecastPoint(
date: e.key,
projectedCost: e.value.$1,
workersNeeded: e.value.$2,
);
}).toList()..sort((ForecastPoint a, ForecastPoint b) => a.date.compareTo(b.date));
final List<ForecastWeek> weeklyBreakdown = <ForecastWeek>[];
for (int i = 0; i < 4; i++) {
final (double, int, double) stats = weeklyStats[i]!;
weeklyBreakdown.add(ForecastWeek(
weekNumber: i + 1,
totalCost: stats.$1,
shiftsCount: stats.$2,
hoursCount: stats.$3,
avgCostPerShift: stats.$2 == 0 ? 0.0 : stats.$1 / stats.$2,
));
}
final int weeksCount = (endDate.difference(startDate).inDays / 7).ceil();
final double avgWeeklySpend = weeksCount > 0 ? projectedSpend / weeksCount : 0.0;
return ForecastReport(
projectedSpend: projectedSpend,
projectedWorkers: projectedWorkers,
averageLaborCost: projectedWorkers == 0 ? 0.0 : projectedSpend / projectedWorkers,
chartData: chartData,
totalShifts: shifts.length,
totalHours: totalHours,
avgWeeklySpend: avgWeeklySpend,
weeklyBreakdown: weeklyBreakdown,
);
});
}
@override
Future<PerformanceReport> getPerformanceReport({
String? businessId,
required DateTime startDate,
required DateTime endDate,
}) async {
return _service.run(() async {
final String id = businessId ?? await _service.getBusinessId();
final QueryResult<dc.ListShiftsForPerformanceByBusinessData, dc.ListShiftsForPerformanceByBusinessVariables> response = await _service.connector
.listShiftsForPerformanceByBusiness(
businessId: id,
startDate: _service.toTimestamp(startDate),
endDate: _service.toTimestamp(endDate),
)
.execute();
final List<dc.ListShiftsForPerformanceByBusinessShifts> shifts = response.data.shifts;
int totalNeeded = 0;
int totalFilled = 0;
int completedCount = 0;
double totalFillTimeSeconds = 0.0;
int filledShiftsWithTime = 0;
for (final dc.ListShiftsForPerformanceByBusinessShifts shift in shifts) {
totalNeeded += shift.workersNeeded ?? 0;
totalFilled += shift.filled ?? 0;
if ((shift.status?.stringValue ?? '') == 'COMPLETED') {
completedCount++;
}
if (shift.filledAt != null && shift.createdAt != null) {
final DateTime createdAt = shift.createdAt!.toDateTime();
final DateTime filledAt = shift.filledAt!.toDateTime();
totalFillTimeSeconds += filledAt.difference(createdAt).inSeconds;
filledShiftsWithTime++;
}
}
final double fillRate = totalNeeded == 0 ? 100.0 : (totalFilled / totalNeeded) * 100.0;
final double completionRate = shifts.isEmpty ? 100.0 : (completedCount / shifts.length) * 100.0;
final double avgFillTimeHours = filledShiftsWithTime == 0
? 0
: (totalFillTimeSeconds / filledShiftsWithTime) / 3600;
return PerformanceReport(
fillRate: fillRate,
completionRate: completionRate,
onTimeRate: 95.0,
avgFillTimeHours: avgFillTimeHours,
keyPerformanceIndicators: <PerformanceMetric>[
PerformanceMetric(label: 'Fill Rate', value: '${fillRate.toStringAsFixed(1)}%', trend: 0.02),
PerformanceMetric(label: 'Completion', value: '${completionRate.toStringAsFixed(1)}%', trend: 0.05),
PerformanceMetric(label: 'Avg Fill Time', value: '${avgFillTimeHours.toStringAsFixed(1)}h', trend: -0.1),
],
);
});
}
@override
Future<NoShowReport> getNoShowReport({
String? businessId,
required DateTime startDate,
required DateTime endDate,
}) async {
return _service.run(() async {
final String id = businessId ?? await _service.getBusinessId();
final QueryResult<dc.ListShiftsForNoShowRangeByBusinessData, dc.ListShiftsForNoShowRangeByBusinessVariables> shiftsResponse = await _service.connector
.listShiftsForNoShowRangeByBusiness(
businessId: id,
startDate: _service.toTimestamp(startDate),
endDate: _service.toTimestamp(endDate),
)
.execute();
final List<String> shiftIds = shiftsResponse.data.shifts.map((dc.ListShiftsForNoShowRangeByBusinessShifts s) => s.id).toList();
if (shiftIds.isEmpty) {
return const NoShowReport(totalNoShows: 0, noShowRate: 0, flaggedWorkers: <NoShowWorker>[]);
}
final QueryResult<dc.ListApplicationsForNoShowRangeData, dc.ListApplicationsForNoShowRangeVariables> appsResponse = await _service.connector
.listApplicationsForNoShowRange(shiftIds: shiftIds)
.execute();
final List<dc.ListApplicationsForNoShowRangeApplications> apps = appsResponse.data.applications;
final List<dc.ListApplicationsForNoShowRangeApplications> noShowApps = apps.where((dc.ListApplicationsForNoShowRangeApplications a) => (a.status.stringValue) == 'NO_SHOW').toList();
final List<String> noShowStaffIds = noShowApps.map((dc.ListApplicationsForNoShowRangeApplications a) => a.staffId).toSet().toList();
if (noShowStaffIds.isEmpty) {
return NoShowReport(
totalNoShows: noShowApps.length,
noShowRate: apps.isEmpty ? 0 : (noShowApps.length / apps.length) * 100.0,
flaggedWorkers: <NoShowWorker>[],
);
}
final QueryResult<dc.ListStaffForNoShowReportData, dc.ListStaffForNoShowReportVariables> staffResponse = await _service.connector
.listStaffForNoShowReport(staffIds: noShowStaffIds)
.execute();
final List<dc.ListStaffForNoShowReportStaffs> staffList = staffResponse.data.staffs;
final List<NoShowWorker> flaggedWorkers = staffList.map((dc.ListStaffForNoShowReportStaffs s) => NoShowWorker(
id: s.id,
fullName: s.fullName ?? '',
noShowCount: s.noShowCount ?? 0,
reliabilityScore: (s.reliabilityScore ?? 0.0).toDouble(),
)).toList();
return NoShowReport(
totalNoShows: noShowApps.length,
noShowRate: apps.isEmpty ? 0 : (noShowApps.length / apps.length) * 100.0,
flaggedWorkers: flaggedWorkers,
);
});
}
@override
Future<ReportsSummary> getReportsSummary({
String? businessId,
required DateTime startDate,
required DateTime endDate,
}) async {
return _service.run(() async {
final String id = businessId ?? await _service.getBusinessId();
// Use forecast query for hours/cost data
final QueryResult<dc.ListShiftsForForecastByBusinessData, dc.ListShiftsForForecastByBusinessVariables> shiftsResponse = await _service.connector
.listShiftsForForecastByBusiness(
businessId: id,
startDate: _service.toTimestamp(startDate),
endDate: _service.toTimestamp(endDate),
)
.execute();
// Use performance query for avgFillTime (has filledAt + createdAt)
final QueryResult<dc.ListShiftsForPerformanceByBusinessData, dc.ListShiftsForPerformanceByBusinessVariables> perfResponse = await _service.connector
.listShiftsForPerformanceByBusiness(
businessId: id,
startDate: _service.toTimestamp(startDate),
endDate: _service.toTimestamp(endDate),
)
.execute();
final QueryResult<dc.ListInvoicesForSpendByBusinessData, dc.ListInvoicesForSpendByBusinessVariables> invoicesResponse = await _service.connector
.listInvoicesForSpendByBusiness(
businessId: id,
startDate: _service.toTimestamp(startDate),
endDate: _service.toTimestamp(endDate),
)
.execute();
final List<dc.ListShiftsForForecastByBusinessShifts> forecastShifts = shiftsResponse.data.shifts;
final List<dc.ListShiftsForPerformanceByBusinessShifts> perfShifts = perfResponse.data.shifts;
final List<dc.ListInvoicesForSpendByBusinessInvoices> invoices = invoicesResponse.data.invoices;
// Aggregate hours and fill rate from forecast shifts
double totalHours = 0;
int totalNeeded = 0;
for (final dc.ListShiftsForForecastByBusinessShifts shift in forecastShifts) {
totalHours += (shift.hours ?? 0).toDouble();
totalNeeded += shift.workersNeeded ?? 0;
}
// Aggregate fill rate from performance shifts (has 'filled' field)
int perfNeeded = 0;
int perfFilled = 0;
double totalFillTimeSeconds = 0;
int filledShiftsWithTime = 0;
for (final dc.ListShiftsForPerformanceByBusinessShifts shift in perfShifts) {
perfNeeded += shift.workersNeeded ?? 0;
perfFilled += shift.filled ?? 0;
if (shift.filledAt != null && shift.createdAt != null) {
final DateTime createdAt = shift.createdAt!.toDateTime();
final DateTime filledAt = shift.filledAt!.toDateTime();
totalFillTimeSeconds += filledAt.difference(createdAt).inSeconds;
filledShiftsWithTime++;
}
}
// Aggregate total spend from invoices
double totalSpend = 0;
for (final dc.ListInvoicesForSpendByBusinessInvoices inv in invoices) {
totalSpend += (inv.amount ?? 0).toDouble();
}
// Fetch no-show rate using forecast shift IDs
final List<String> shiftIds = forecastShifts.map((dc.ListShiftsForForecastByBusinessShifts s) => s.id).toList();
double noShowRate = 0;
if (shiftIds.isNotEmpty) {
final QueryResult<dc.ListApplicationsForNoShowRangeData, dc.ListApplicationsForNoShowRangeVariables> appsResponse = await _service.connector
.listApplicationsForNoShowRange(shiftIds: shiftIds)
.execute();
final List<dc.ListApplicationsForNoShowRangeApplications> apps = appsResponse.data.applications;
final List<dc.ListApplicationsForNoShowRangeApplications> noShowApps = apps.where((dc.ListApplicationsForNoShowRangeApplications a) => (a.status.stringValue) == 'NO_SHOW').toList();
noShowRate = apps.isEmpty ? 0 : (noShowApps.length / apps.length) * 100.0;
}
final double fillRate = perfNeeded == 0 ? 100.0 : (perfFilled / perfNeeded) * 100.0;
return ReportsSummary(
totalHours: totalHours,
otHours: totalHours * 0.05, // ~5% OT approximation until schema supports it
totalSpend: totalSpend,
fillRate: fillRate,
avgFillTimeHours: filledShiftsWithTime == 0
? 0
: (totalFillTimeSeconds / filledShiftsWithTime) / 3600,
noShowRate: noShowRate,
);
});
}
}

View File

@@ -0,0 +1,55 @@
import 'package:krow_domain/krow_domain.dart';
/// Repository interface for reports connector queries.
///
/// This interface defines the contract for accessing report-related data
/// from the backend via Data Connect.
abstract interface class ReportsConnectorRepository {
/// Fetches the daily operations report for a specific business and date.
Future<DailyOpsReport> getDailyOpsReport({
String? businessId,
required DateTime date,
});
/// Fetches the spend report for a specific business and date range.
Future<SpendReport> getSpendReport({
String? businessId,
required DateTime startDate,
required DateTime endDate,
});
/// Fetches the coverage report for a specific business and date range.
Future<CoverageReport> getCoverageReport({
String? businessId,
required DateTime startDate,
required DateTime endDate,
});
/// Fetches the forecast report for a specific business and date range.
Future<ForecastReport> getForecastReport({
String? businessId,
required DateTime startDate,
required DateTime endDate,
});
/// Fetches the performance report for a specific business and date range.
Future<PerformanceReport> getPerformanceReport({
String? businessId,
required DateTime startDate,
required DateTime endDate,
});
/// Fetches the no-show report for a specific business and date range.
Future<NoShowReport> getNoShowReport({
String? businessId,
required DateTime startDate,
required DateTime endDate,
});
/// Fetches a summary of all reports for a specific business and date range.
Future<ReportsSummary> getReportsSummary({
String? businessId,
required DateTime startDate,
required DateTime endDate,
});
}

View File

@@ -0,0 +1,517 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:intl/intl.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/shifts_connector_repository.dart';
/// Implementation of [ShiftsConnectorRepository].
///
/// Handles shift-related data operations by interacting with Data Connect.
class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
/// Creates a new [ShiftsConnectorRepositoryImpl].
ShiftsConnectorRepositoryImpl({
dc.DataConnectService? service,
}) : _service = service ?? dc.DataConnectService.instance;
final dc.DataConnectService _service;
@override
Future<List<Shift>> getMyShifts({
required String staffId,
required DateTime start,
required DateTime end,
}) async {
return _service.run(() async {
final dc.GetApplicationsByStaffIdVariablesBuilder query = _service.connector
.getApplicationsByStaffId(staffId: staffId)
.dayStart(_service.toTimestamp(start))
.dayEnd(_service.toTimestamp(end));
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> response = await query.execute();
return _mapApplicationsToShifts(response.data.applications);
});
}
@override
Future<List<Shift>> getAvailableShifts({
required String staffId,
String? query,
String? type,
}) async {
return _service.run(() async {
// First, fetch all available shift roles for the vendor/business
// Use the session owner ID (vendorId)
final String? vendorId = dc.StaffSessionStore.instance.session?.ownerId;
if (vendorId == null || vendorId.isEmpty) return <Shift>[];
final QueryResult<dc.ListShiftRolesByVendorIdData, dc.ListShiftRolesByVendorIdVariables> response = await _service.connector
.listShiftRolesByVendorId(vendorId: vendorId)
.execute();
final List<dc.ListShiftRolesByVendorIdShiftRoles> allShiftRoles = response.data.shiftRoles;
// Fetch current applications to filter out already booked shifts
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> myAppsResponse = await _service.connector
.getApplicationsByStaffId(staffId: staffId)
.execute();
final Set<String> appliedShiftIds =
myAppsResponse.data.applications.map((dc.GetApplicationsByStaffIdApplications a) => a.shiftId).toSet();
final List<Shift> mappedShifts = <Shift>[];
for (final dc.ListShiftRolesByVendorIdShiftRoles sr in allShiftRoles) {
if (appliedShiftIds.contains(sr.shiftId)) continue;
final DateTime? shiftDate = _service.toDateTime(sr.shift.date);
final DateTime? startDt = _service.toDateTime(sr.startTime);
final DateTime? endDt = _service.toDateTime(sr.endTime);
final DateTime? createdDt = _service.toDateTime(sr.createdAt);
mappedShifts.add(
Shift(
id: sr.shiftId,
roleId: sr.roleId,
title: sr.role.name,
clientName: sr.shift.order.business.businessName,
logoUrl: null,
hourlyRate: sr.role.costPerHour,
location: sr.shift.location ?? '',
locationAddress: sr.shift.locationAddress ?? '',
date: shiftDate?.toIso8601String() ?? '',
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
createdDate: createdDt?.toIso8601String() ?? '',
status: sr.shift.status?.stringValue.toLowerCase() ?? 'open',
description: sr.shift.description,
durationDays: sr.shift.durationDays,
requiredSlots: sr.count,
filledSlots: sr.assigned ?? 0,
latitude: sr.shift.latitude,
longitude: sr.shift.longitude,
breakInfo: BreakAdapter.fromData(
isPaid: sr.isBreakPaid ?? false,
breakTime: sr.breakType?.stringValue,
),
),
);
}
if (query != null && query.isNotEmpty) {
final String lowerQuery = query.toLowerCase();
return mappedShifts.where((Shift s) {
return s.title.toLowerCase().contains(lowerQuery) ||
s.clientName.toLowerCase().contains(lowerQuery);
}).toList();
}
return mappedShifts;
});
}
@override
Future<List<Shift>> getPendingAssignments({required String staffId}) async {
return _service.run(() async {
// Current schema doesn't have a specific "pending assignment" query that differs from confirmed
// unless we filter by status. In the old repo it was returning an empty list.
return <Shift>[];
});
}
@override
Future<Shift?> getShiftDetails({
required String shiftId,
required String staffId,
String? roleId,
}) async {
return _service.run(() async {
if (roleId != null && roleId.isNotEmpty) {
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables> roleResult = await _service.connector
.getShiftRoleById(shiftId: shiftId, roleId: roleId)
.execute();
final dc.GetShiftRoleByIdShiftRole? sr = roleResult.data.shiftRole;
if (sr == null) return null;
final DateTime? startDt = _service.toDateTime(sr.startTime);
final DateTime? endDt = _service.toDateTime(sr.endTime);
final DateTime? createdDt = _service.toDateTime(sr.createdAt);
bool hasApplied = false;
String status = 'open';
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> appsResponse = await _service.connector
.getApplicationsByStaffId(staffId: staffId)
.execute();
final dc.GetApplicationsByStaffIdApplications? app = appsResponse.data.applications
.where((dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId && a.shiftRole.roleId == roleId)
.firstOrNull;
if (app != null) {
hasApplied = true;
final String s = app.status.stringValue;
status = _mapApplicationStatus(s);
}
return Shift(
id: sr.shiftId,
roleId: sr.roleId,
title: sr.shift.order.business.businessName,
clientName: sr.shift.order.business.businessName,
logoUrl: sr.shift.order.business.companyLogoUrl,
hourlyRate: sr.role.costPerHour,
location: sr.shift.location ?? sr.shift.order.teamHub.hubName,
locationAddress: sr.shift.locationAddress ?? '',
date: startDt?.toIso8601String() ?? '',
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
createdDate: createdDt?.toIso8601String() ?? '',
status: status,
description: sr.shift.description,
durationDays: null,
requiredSlots: sr.count,
filledSlots: sr.assigned ?? 0,
hasApplied: hasApplied,
totalValue: sr.totalValue,
latitude: sr.shift.latitude,
longitude: sr.shift.longitude,
breakInfo: BreakAdapter.fromData(
isPaid: sr.isBreakPaid ?? false,
breakTime: sr.breakType?.stringValue,
),
);
}
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> result = await _service.connector.getShiftById(id: shiftId).execute();
final dc.GetShiftByIdShift? s = result.data.shift;
if (s == null) return null;
int? required;
int? filled;
Break? breakInfo;
try {
final QueryResult<dc.ListShiftRolesByShiftIdData, dc.ListShiftRolesByShiftIdVariables> rolesRes = await _service.connector
.listShiftRolesByShiftId(shiftId: shiftId)
.execute();
if (rolesRes.data.shiftRoles.isNotEmpty) {
required = 0;
filled = 0;
for (dc.ListShiftRolesByShiftIdShiftRoles r in rolesRes.data.shiftRoles) {
required = (required ?? 0) + r.count;
filled = (filled ?? 0) + (r.assigned ?? 0);
}
final dc.ListShiftRolesByShiftIdShiftRoles firstRole = rolesRes.data.shiftRoles.first;
breakInfo = BreakAdapter.fromData(
isPaid: firstRole.isBreakPaid ?? false,
breakTime: firstRole.breakType?.stringValue,
);
}
} catch (_) {}
final DateTime? startDt = _service.toDateTime(s.startTime);
final DateTime? endDt = _service.toDateTime(s.endTime);
final DateTime? createdDt = _service.toDateTime(s.createdAt);
return Shift(
id: s.id,
title: s.title,
clientName: s.order.business.businessName,
logoUrl: null,
hourlyRate: s.cost ?? 0.0,
location: s.location ?? '',
locationAddress: s.locationAddress ?? '',
date: startDt?.toIso8601String() ?? '',
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
createdDate: createdDt?.toIso8601String() ?? '',
status: s.status?.stringValue ?? 'OPEN',
description: s.description,
durationDays: s.durationDays,
requiredSlots: required,
filledSlots: filled,
latitude: s.latitude,
longitude: s.longitude,
breakInfo: breakInfo,
);
});
}
@override
Future<void> applyForShift({
required String shiftId,
required String staffId,
bool isInstantBook = false,
String? roleId,
}) async {
return _service.run(() async {
final String targetRoleId = roleId ?? '';
if (targetRoleId.isEmpty) throw Exception('Missing role id.');
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables> roleResult = await _service.connector
.getShiftRoleById(shiftId: shiftId, roleId: targetRoleId)
.execute();
final dc.GetShiftRoleByIdShiftRole? role = roleResult.data.shiftRole;
if (role == null) throw Exception('Shift role not found');
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> shiftResult = await _service.connector.getShiftById(id: shiftId).execute();
final dc.GetShiftByIdShift? shift = shiftResult.data.shift;
if (shift == null) throw Exception('Shift not found');
// Validate daily limit
final DateTime? shiftDate = _service.toDateTime(shift.date);
if (shiftDate != null) {
final DateTime dayStartUtc = DateTime.utc(shiftDate.year, shiftDate.month, shiftDate.day);
final DateTime dayEndUtc = dayStartUtc.add(const Duration(days: 1)).subtract(const Duration(microseconds: 1));
final QueryResult<dc.VaidateDayStaffApplicationData, dc.VaidateDayStaffApplicationVariables> validationResponse = await _service.connector
.vaidateDayStaffApplication(staffId: staffId)
.dayStart(_service.toTimestamp(dayStartUtc))
.dayEnd(_service.toTimestamp(dayEndUtc))
.execute();
if (validationResponse.data.applications.isNotEmpty) {
throw Exception('The user already has a shift that day.');
}
}
// Check for existing application
final QueryResult<dc.GetApplicationByStaffShiftAndRoleData, dc.GetApplicationByStaffShiftAndRoleVariables> existingAppRes = await _service.connector
.getApplicationByStaffShiftAndRole(
staffId: staffId,
shiftId: shiftId,
roleId: targetRoleId,
)
.execute();
if (existingAppRes.data.applications.isNotEmpty) {
throw Exception('Application already exists.');
}
if ((role.assigned ?? 0) >= role.count) {
throw Exception('This shift is full.');
}
final int currentAssigned = role.assigned ?? 0;
final int currentFilled = shift.filled ?? 0;
String? createdAppId;
try {
final OperationResult<dc.CreateApplicationData, dc.CreateApplicationVariables> createRes = await _service.connector.createApplication(
shiftId: shiftId,
staffId: staffId,
roleId: targetRoleId,
status: dc.ApplicationStatus.CONFIRMED, // Matches existing logic
origin: dc.ApplicationOrigin.STAFF,
).execute();
createdAppId = createRes.data.application_insert.id;
await _service.connector
.updateShiftRole(shiftId: shiftId, roleId: targetRoleId)
.assigned(currentAssigned + 1)
.execute();
await _service.connector
.updateShift(id: shiftId)
.filled(currentFilled + 1)
.execute();
} catch (e) {
// Simple rollback attempt (not guaranteed)
if (createdAppId != null) {
await _service.connector.deleteApplication(id: createdAppId).execute();
}
rethrow;
}
});
}
@override
Future<void> acceptShift({
required String shiftId,
required String staffId,
}) {
return _updateApplicationStatus(shiftId, staffId, dc.ApplicationStatus.CONFIRMED);
}
@override
Future<void> declineShift({
required String shiftId,
required String staffId,
}) {
return _updateApplicationStatus(shiftId, staffId, dc.ApplicationStatus.REJECTED);
}
@override
Future<List<Shift>> getCancelledShifts({required String staffId}) async {
return _service.run(() async {
// Logic would go here to fetch by REJECTED status if needed
return <Shift>[];
});
}
@override
Future<List<Shift>> getHistoryShifts({required String staffId}) async {
return _service.run(() async {
final QueryResult<dc.ListCompletedApplicationsByStaffIdData, dc.ListCompletedApplicationsByStaffIdVariables> response = await _service.connector
.listCompletedApplicationsByStaffId(staffId: staffId)
.execute();
final List<Shift> shifts = <Shift>[];
for (final dc.ListCompletedApplicationsByStaffIdApplications app in response.data.applications) {
final String roleName = app.shiftRole.role.name;
final String orderName = (app.shift.order.eventName ?? '').trim().isNotEmpty
? app.shift.order.eventName!
: app.shift.order.business.businessName;
final String title = '$roleName - $orderName';
final DateTime? shiftDate = _service.toDateTime(app.shift.date);
final DateTime? startDt = _service.toDateTime(app.shiftRole.startTime);
final DateTime? endDt = _service.toDateTime(app.shiftRole.endTime);
final DateTime? createdDt = _service.toDateTime(app.createdAt);
shifts.add(
Shift(
id: app.shift.id,
roleId: app.shiftRole.roleId,
title: title,
clientName: app.shift.order.business.businessName,
logoUrl: app.shift.order.business.companyLogoUrl,
hourlyRate: app.shiftRole.role.costPerHour,
location: app.shift.location ?? '',
locationAddress: app.shift.order.teamHub.hubName,
date: shiftDate?.toIso8601String() ?? '',
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
createdDate: createdDt?.toIso8601String() ?? '',
status: 'completed', // Hardcoded as checked out implies completion
description: app.shift.description,
durationDays: app.shift.durationDays,
requiredSlots: app.shiftRole.count,
filledSlots: app.shiftRole.assigned ?? 0,
hasApplied: true,
latitude: app.shift.latitude,
longitude: app.shift.longitude,
breakInfo: BreakAdapter.fromData(
isPaid: app.shiftRole.isBreakPaid ?? false,
breakTime: app.shiftRole.breakType?.stringValue,
),
),
);
}
return shifts;
});
}
// --- PRIVATE HELPERS ---
List<Shift> _mapApplicationsToShifts(List<dynamic> apps) {
return apps.map((app) {
final String roleName = app.shiftRole.role.name;
final String orderName = (app.shift.order.eventName ?? '').trim().isNotEmpty
? app.shift.order.eventName!
: app.shift.order.business.businessName;
final String title = '$roleName - $orderName';
final DateTime? shiftDate = _service.toDateTime(app.shift.date);
final DateTime? startDt = _service.toDateTime(app.shiftRole.startTime);
final DateTime? endDt = _service.toDateTime(app.shiftRole.endTime);
final DateTime? createdDt = _service.toDateTime(app.createdAt);
final bool hasCheckIn = app.checkInTime != null;
final bool hasCheckOut = app.checkOutTime != null;
String status;
if (hasCheckOut) {
status = 'completed';
} else if (hasCheckIn) {
status = 'checked_in';
} else {
status = _mapApplicationStatus(app.status.stringValue);
}
return Shift(
id: app.shift.id,
roleId: app.shiftRole.roleId,
title: title,
clientName: app.shift.order.business.businessName,
logoUrl: app.shift.order.business.companyLogoUrl,
hourlyRate: app.shiftRole.role.costPerHour,
location: app.shift.location ?? '',
locationAddress: app.shift.order.teamHub.hubName,
date: shiftDate?.toIso8601String() ?? '',
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
createdDate: createdDt?.toIso8601String() ?? '',
status: status,
description: app.shift.description,
durationDays: app.shift.durationDays,
requiredSlots: app.shiftRole.count,
filledSlots: app.shiftRole.assigned ?? 0,
hasApplied: true,
latitude: app.shift.latitude,
longitude: app.shift.longitude,
breakInfo: BreakAdapter.fromData(
isPaid: app.shiftRole.isBreakPaid ?? false,
breakTime: app.shiftRole.breakType?.stringValue,
),
);
}).toList();
}
String _mapApplicationStatus(String status) {
switch (status) {
case 'CONFIRMED':
return 'confirmed';
case 'PENDING':
return 'pending';
case 'CHECKED_OUT':
return 'completed';
case 'REJECTED':
return 'cancelled';
default:
return 'open';
}
}
Future<void> _updateApplicationStatus(
String shiftId,
String staffId,
dc.ApplicationStatus newStatus,
) async {
return _service.run(() async {
// First try to find the application
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> appsResponse = await _service.connector
.getApplicationsByStaffId(staffId: staffId)
.execute();
final dc.GetApplicationsByStaffIdApplications? app = appsResponse.data.applications
.where((dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId)
.firstOrNull;
if (app != null) {
await _service.connector
.updateApplicationStatus(id: app.id)
.status(newStatus)
.execute();
} else if (newStatus == dc.ApplicationStatus.REJECTED) {
// If declining but no app found, create a rejected application
final QueryResult<dc.ListShiftRolesByShiftIdData, dc.ListShiftRolesByShiftIdVariables> rolesRes = await _service.connector
.listShiftRolesByShiftId(shiftId: shiftId)
.execute();
if (rolesRes.data.shiftRoles.isNotEmpty) {
final dc.ListShiftRolesByShiftIdShiftRoles firstRole = rolesRes.data.shiftRoles.first;
await _service.connector.createApplication(
shiftId: shiftId,
staffId: staffId,
roleId: firstRole.id,
status: dc.ApplicationStatus.REJECTED,
origin: dc.ApplicationOrigin.STAFF,
).execute();
}
} else {
throw Exception("Application not found for shift $shiftId");
}
});
}
}

View File

@@ -0,0 +1,56 @@
import 'package:krow_domain/krow_domain.dart';
/// Repository interface for shifts connector operations.
///
/// This acts as a buffer layer between the domain repository and the Data Connect SDK.
abstract interface class ShiftsConnectorRepository {
/// Retrieves shifts assigned to the current staff member.
Future<List<Shift>> getMyShifts({
required String staffId,
required DateTime start,
required DateTime end,
});
/// Retrieves available shifts.
Future<List<Shift>> getAvailableShifts({
required String staffId,
String? query,
String? type,
});
/// Retrieves pending shift assignments for the current staff member.
Future<List<Shift>> getPendingAssignments({required String staffId});
/// Retrieves detailed information for a specific shift.
Future<Shift?> getShiftDetails({
required String shiftId,
required String staffId,
String? roleId,
});
/// Applies for a specific open shift.
Future<void> applyForShift({
required String shiftId,
required String staffId,
bool isInstantBook = false,
String? roleId,
});
/// Accepts a pending shift assignment.
Future<void> acceptShift({
required String shiftId,
required String staffId,
});
/// Declines a pending shift assignment.
Future<void> declineShift({
required String shiftId,
required String staffId,
});
/// Retrieves cancelled shifts for the current staff member.
Future<List<Shift>> getCancelledShifts({required String staffId});
/// Retrieves historical (completed) shifts for the current staff member.
Future<List<Shift>> getHistoryShifts({required String staffId});
}

View File

@@ -1,3 +1,4 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports
import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
@@ -105,10 +106,10 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
/// Checks if personal info is complete. /// Checks if personal info is complete.
bool _isPersonalInfoComplete(GetStaffPersonalInfoCompletionStaff? staff) { bool _isPersonalInfoComplete(GetStaffPersonalInfoCompletionStaff? staff) {
if (staff == null) return false; if (staff == null) return false;
final String? fullName = staff.fullName; final String fullName = staff.fullName;
final String? email = staff.email; final String? email = staff.email;
final String? phone = staff.phone; final String? phone = staff.phone;
return (fullName?.trim().isNotEmpty ?? false) && return (fullName.trim().isNotEmpty ?? false) &&
(email?.trim().isNotEmpty ?? false) && (email?.trim().isNotEmpty ?? false) &&
(phone?.trim().isNotEmpty ?? false); (phone?.trim().isNotEmpty ?? false);
} }
@@ -187,3 +188,4 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
} }
} }
} }

View File

@@ -1,4 +1,16 @@
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'connectors/reports/domain/repositories/reports_connector_repository.dart';
import 'connectors/reports/data/repositories/reports_connector_repository_impl.dart';
import 'connectors/shifts/domain/repositories/shifts_connector_repository.dart';
import 'connectors/shifts/data/repositories/shifts_connector_repository_impl.dart';
import 'connectors/hubs/domain/repositories/hubs_connector_repository.dart';
import 'connectors/hubs/data/repositories/hubs_connector_repository_impl.dart';
import 'connectors/billing/domain/repositories/billing_connector_repository.dart';
import 'connectors/billing/data/repositories/billing_connector_repository_impl.dart';
import 'connectors/home/domain/repositories/home_connector_repository.dart';
import 'connectors/home/data/repositories/home_connector_repository_impl.dart';
import 'connectors/coverage/domain/repositories/coverage_connector_repository.dart';
import 'connectors/coverage/data/repositories/coverage_connector_repository_impl.dart';
import 'services/data_connect_service.dart'; import 'services/data_connect_service.dart';
/// A module that provides Data Connect dependencies. /// A module that provides Data Connect dependencies.
@@ -6,5 +18,25 @@ class DataConnectModule extends Module {
@override @override
void exportedBinds(Injector i) { void exportedBinds(Injector i) {
i.addInstance<DataConnectService>(DataConnectService.instance); i.addInstance<DataConnectService>(DataConnectService.instance);
// Repositories
i.addLazySingleton<ReportsConnectorRepository>(
ReportsConnectorRepositoryImpl.new,
);
i.addLazySingleton<ShiftsConnectorRepository>(
ShiftsConnectorRepositoryImpl.new,
);
i.addLazySingleton<HubsConnectorRepository>(
HubsConnectorRepositoryImpl.new,
);
i.addLazySingleton<BillingConnectorRepository>(
BillingConnectorRepositoryImpl.new,
);
i.addLazySingleton<HomeConnectorRepository>(
HomeConnectorRepositoryImpl.new,
);
i.addLazySingleton<CoverageConnectorRepository>(
CoverageConnectorRepositoryImpl.new,
);
} }
} }

View File

@@ -1,12 +1,24 @@
import 'dart:async'; // ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:firebase_auth/firebase_auth.dart' as firebase_auth;
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart';
import 'package:krow_core/core.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart' as domain;
import '../../krow_data_connect.dart' as dc; import '../connectors/reports/domain/repositories/reports_connector_repository.dart';
import '../connectors/reports/data/repositories/reports_connector_repository_impl.dart';
import '../connectors/shifts/domain/repositories/shifts_connector_repository.dart';
import '../connectors/shifts/data/repositories/shifts_connector_repository_impl.dart';
import '../connectors/hubs/domain/repositories/hubs_connector_repository.dart';
import '../connectors/hubs/data/repositories/hubs_connector_repository_impl.dart';
import '../connectors/billing/domain/repositories/billing_connector_repository.dart';
import '../connectors/billing/data/repositories/billing_connector_repository_impl.dart';
import '../connectors/home/domain/repositories/home_connector_repository.dart';
import '../connectors/home/data/repositories/home_connector_repository_impl.dart';
import '../connectors/coverage/domain/repositories/coverage_connector_repository.dart';
import '../connectors/coverage/data/repositories/coverage_connector_repository_impl.dart';
import '../connectors/staff/domain/repositories/staff_connector_repository.dart';
import '../connectors/staff/data/repositories/staff_connector_repository_impl.dart';
import 'mixins/data_error_handler.dart'; import 'mixins/data_error_handler.dart';
import 'mixins/session_handler_mixin.dart'; import 'mixins/session_handler_mixin.dart';
@@ -22,176 +34,202 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
/// The Data Connect connector used for data operations. /// The Data Connect connector used for data operations.
final dc.ExampleConnector connector = dc.ExampleConnector.instance; final dc.ExampleConnector connector = dc.ExampleConnector.instance;
/// The Firebase Auth instance. // Repositories
firebase_auth.FirebaseAuth get auth => _auth; ReportsConnectorRepository? _reportsRepository;
final firebase_auth.FirebaseAuth _auth = firebase_auth.FirebaseAuth.instance; ShiftsConnectorRepository? _shiftsRepository;
HubsConnectorRepository? _hubsRepository;
BillingConnectorRepository? _billingRepository;
HomeConnectorRepository? _homeRepository;
CoverageConnectorRepository? _coverageRepository;
StaffConnectorRepository? _staffRepository;
/// Cache for the current staff ID to avoid redundant lookups. /// Gets the reports connector repository.
String? _cachedStaffId; ReportsConnectorRepository getReportsRepository() {
return _reportsRepository ??= ReportsConnectorRepositoryImpl(service: this);
}
/// Cache for the current business ID to avoid redundant lookups. /// Gets the shifts connector repository.
String? _cachedBusinessId; ShiftsConnectorRepository getShiftsRepository() {
return _shiftsRepository ??= ShiftsConnectorRepositoryImpl(service: this);
}
/// Gets the current staff ID from session store or persistent storage. /// Gets the hubs connector repository.
HubsConnectorRepository getHubsRepository() {
return _hubsRepository ??= HubsConnectorRepositoryImpl(service: this);
}
/// Gets the billing connector repository.
BillingConnectorRepository getBillingRepository() {
return _billingRepository ??= BillingConnectorRepositoryImpl(service: this);
}
/// Gets the home connector repository.
HomeConnectorRepository getHomeRepository() {
return _homeRepository ??= HomeConnectorRepositoryImpl(service: this);
}
/// Gets the coverage connector repository.
CoverageConnectorRepository getCoverageRepository() {
return _coverageRepository ??= CoverageConnectorRepositoryImpl(service: this);
}
/// Gets the staff connector repository.
StaffConnectorRepository getStaffRepository() {
return _staffRepository ??= StaffConnectorRepositoryImpl(service: this);
}
/// Returns the current Firebase Auth instance.
@override
firebase.FirebaseAuth get auth => firebase.FirebaseAuth.instance;
/// Helper to get the current staff ID from the session.
Future<String> getStaffId() async { Future<String> getStaffId() async {
// 1. Check Session Store String? staffId = dc.StaffSessionStore.instance.session?.ownerId;
final dc.StaffSession? session = dc.StaffSessionStore.instance.session;
if (session?.staff?.id != null) { if (staffId == null || staffId.isEmpty) {
return session!.staff!.id; // Attempt to recover session if user is signed in
} final user = auth.currentUser;
if (user != null) {
// 2. Check Cache await _loadSession(user.uid);
if (_cachedStaffId != null) return _cachedStaffId!; staffId = dc.StaffSessionStore.instance.session?.ownerId;
// 3. Fetch from Data Connect using Firebase UID
final firebase_auth.User? user = _auth.currentUser;
if (user == null) {
throw const NotAuthenticatedException(
technicalMessage: 'User is not authenticated',
);
}
try {
final fdc.QueryResult<
dc.GetStaffByUserIdData,
dc.GetStaffByUserIdVariables
>
response = await executeProtected(
() => connector.getStaffByUserId(userId: user.uid).execute(),
);
if (response.data.staffs.isNotEmpty) {
_cachedStaffId = response.data.staffs.first.id;
return _cachedStaffId!;
} }
} catch (e) {
throw Exception('Failed to fetch staff ID from Data Connect: $e');
} }
// 4. Fallback (should ideally not happen if DB is seeded) if (staffId == null || staffId.isEmpty) {
return user.uid; throw Exception('No staff ID found in session.');
}
return staffId;
} }
/// Gets the current business ID from session store or persistent storage. /// Helper to get the current business ID from the session.
Future<String> getBusinessId() async { Future<String> getBusinessId() async {
// 1. Check Session Store String? businessId = dc.ClientSessionStore.instance.session?.business?.id;
final dc.ClientSession? session = dc.ClientSessionStore.instance.session;
if (session?.business?.id != null) {
return session!.business!.id;
}
// 2. Check Cache if (businessId == null || businessId.isEmpty) {
if (_cachedBusinessId != null) return _cachedBusinessId!; // Attempt to recover session if user is signed in
final user = auth.currentUser;
// 3. Fetch from Data Connect using Firebase UID if (user != null) {
final firebase_auth.User? user = _auth.currentUser; await _loadSession(user.uid);
if (user == null) { businessId = dc.ClientSessionStore.instance.session?.business?.id;
throw const NotAuthenticatedException(
technicalMessage: 'User is not authenticated',
);
}
try {
final fdc.QueryResult<
dc.GetBusinessesByUserIdData,
dc.GetBusinessesByUserIdVariables
>
response = await executeProtected(
() => connector.getBusinessesByUserId(userId: user.uid).execute(),
);
if (response.data.businesses.isNotEmpty) {
_cachedBusinessId = response.data.businesses.first.id;
return _cachedBusinessId!;
} }
} catch (e) {
throw Exception('Failed to fetch business ID from Data Connect: $e');
} }
// 4. Fallback (should ideally not happen if DB is seeded) if (businessId == null || businessId.isEmpty) {
return user.uid; throw Exception('No business ID found in session.');
}
return businessId;
} }
/// Converts a Data Connect timestamp/string/json to a [DateTime]. /// Logic to load session data from backend and populate stores.
DateTime? toDateTime(dynamic t) { Future<void> _loadSession(String userId) async {
if (t == null) return null; try {
DateTime? dt; final role = await fetchUserRole(userId);
if (t is fdc.Timestamp) { if (role == null) return;
dt = t.toDateTime();
} else if (t is String) { // Load Staff Session if applicable
dt = DateTime.tryParse(t); if (role == 'STAFF' || role == 'BOTH') {
} else { final response = await connector.getStaffByUserId(userId: userId).execute();
try { if (response.data.staffs.isNotEmpty) {
dt = DateTime.tryParse(t.toJson() as String); final s = response.data.staffs.first;
} catch (_) { dc.StaffSessionStore.instance.setSession(
try { dc.StaffSession(
dt = DateTime.tryParse(t.toString()); ownerId: s.id,
} catch (e) { staff: domain.Staff(
dt = null; id: s.id,
authProviderId: s.userId,
name: s.fullName,
email: s.email ?? '',
phone: s.phone,
status: domain.StaffStatus.completedProfile,
address: s.addres,
avatar: s.photoUrl,
),
),
);
} }
} }
}
if (dt != null) { // Load Client Session if applicable
return DateTimeUtils.toDeviceTime(dt); if (role == 'BUSINESS' || role == 'BOTH') {
final response = await connector.getBusinessesByUserId(userId: userId).execute();
if (response.data.businesses.isNotEmpty) {
final b = response.data.businesses.first;
dc.ClientSessionStore.instance.setSession(
dc.ClientSession(
business: dc.ClientBusinessSession(
id: b.id,
businessName: b.businessName,
email: b.email,
city: b.city,
contactName: b.contactName,
companyLogoUrl: b.companyLogoUrl,
),
),
);
}
}
} catch (e) {
debugPrint('DataConnectService: Error loading session for $userId: $e');
}
}
/// Converts a Data Connect [Timestamp] to a Dart [DateTime].
DateTime? toDateTime(dynamic timestamp) {
if (timestamp == null) return null;
if (timestamp is fdc.Timestamp) {
return timestamp.toDateTime();
} }
return null; return null;
} }
/// Converts a [DateTime] to a Firebase Data Connect [Timestamp]. /// Converts a Dart [DateTime] to a Data Connect [Timestamp].
fdc.Timestamp toTimestamp(DateTime dateTime) { fdc.Timestamp toTimestamp(DateTime dateTime) {
final DateTime utc = dateTime.toUtc(); final DateTime utc = dateTime.toUtc();
final int seconds = utc.millisecondsSinceEpoch ~/ 1000; final int millis = utc.millisecondsSinceEpoch;
final int nanoseconds = (utc.microsecondsSinceEpoch % 1000000) * 1000; final int seconds = millis ~/ 1000;
return fdc.Timestamp(nanoseconds, seconds); final int nanos = (millis % 1000) * 1000000;
return fdc.Timestamp(nanos, seconds);
} }
// --- 3. Unified Execution --- /// Converts a nullable Dart [DateTime] to a nullable Data Connect [Timestamp].
// Repositories call this to benefit from centralized error handling/logging fdc.Timestamp? tryToTimestamp(DateTime? dateTime) {
if (dateTime == null) return null;
return toTimestamp(dateTime);
}
/// Executes an operation with centralized error handling.
Future<T> run<T>( Future<T> run<T>(
Future<T> Function() action, { Future<T> Function() operation, {
bool requiresAuthentication = true, bool requiresAuthentication = true,
}) async { }) async {
if (requiresAuthentication && auth.currentUser == null) { if (requiresAuthentication) {
throw const NotAuthenticatedException(
technicalMessage: 'User must be authenticated to perform this action',
);
}
return executeProtected(() async {
// Ensure session token is valid and refresh if needed
await ensureSessionValid(); await ensureSessionValid();
return action(); }
}); return executeProtected(operation);
}
/// Clears the internal cache (e.g., on logout).
void clearCache() {
_cachedStaffId = null;
_cachedBusinessId = null;
}
/// Handle session sign-out by clearing caches.
void handleSignOut() {
clearCache();
} }
/// Implementation for SessionHandlerMixin.
@override @override
Future<String?> fetchUserRole(String userId) async { Future<String?> fetchUserRole(String userId) async {
try { try {
final fdc.QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> final response = await connector.getUserById(id: userId).execute();
response = await executeProtected(
() => connector.getUserById(id: userId).execute(),
);
return response.data.user?.userRole; return response.data.user?.userRole;
} catch (e) { } catch (e) {
debugPrint('Failed to fetch user role: $e');
return null; return null;
} }
} }
/// Dispose all resources (call on app shutdown). /// Clears Cached Repositories and Session data.
Future<void> dispose() async { void clearCache() {
await disposeSessionHandler(); _reportsRepository = null;
_shiftsRepository = null;
_hubsRepository = null;
_billingRepository = null;
_homeRepository = null;
_coverageRepository = null;
_staffRepository = null;
dc.StaffSessionStore.instance.clear();
dc.ClientSessionStore.instance.clear();
} }
} }

View File

@@ -96,7 +96,7 @@ mixin SessionHandlerMixin {
_authStateSubscription = auth.authStateChanges().listen( _authStateSubscription = auth.authStateChanges().listen(
(firebase_auth.User? user) async { (firebase_auth.User? user) async {
if (user == null) { if (user == null) {
_handleSignOut(); handleSignOut();
} else { } else {
await _handleSignIn(user); await _handleSignIn(user);
} }
@@ -235,7 +235,7 @@ mixin SessionHandlerMixin {
} }
/// Handle user sign-out event. /// Handle user sign-out event.
void _handleSignOut() { void handleSignOut() {
_emitSessionState(SessionState.unauthenticated()); _emitSessionState(SessionState.unauthenticated());
} }

View File

@@ -1,10 +1,4 @@
class ClientBusinessSession { class ClientBusinessSession {
final String id;
final String businessName;
final String? email;
final String? city;
final String? contactName;
final String? companyLogoUrl;
const ClientBusinessSession({ const ClientBusinessSession({
required this.id, required this.id,
@@ -14,15 +8,23 @@ class ClientBusinessSession {
this.contactName, this.contactName,
this.companyLogoUrl, this.companyLogoUrl,
}); });
final String id;
final String businessName;
final String? email;
final String? city;
final String? contactName;
final String? companyLogoUrl;
} }
class ClientSession { class ClientSession {
final ClientBusinessSession? business;
const ClientSession({required this.business}); const ClientSession({required this.business});
final ClientBusinessSession? business;
} }
class ClientSessionStore { class ClientSessionStore {
ClientSessionStore._();
ClientSession? _session; ClientSession? _session;
ClientSession? get session => _session; ClientSession? get session => _session;
@@ -36,6 +38,4 @@ class ClientSessionStore {
} }
static final ClientSessionStore instance = ClientSessionStore._(); static final ClientSessionStore instance = ClientSessionStore._();
ClientSessionStore._();
} }

View File

@@ -85,8 +85,9 @@ class UiTheme {
overlayColor: WidgetStateProperty.resolveWith(( overlayColor: WidgetStateProperty.resolveWith((
Set<WidgetState> states, Set<WidgetState> states,
) { ) {
if (states.contains(WidgetState.hovered)) if (states.contains(WidgetState.hovered)) {
return UiColors.buttonPrimaryHover; return UiColors.buttonPrimaryHover;
}
return null; return null;
}), }),
), ),

View File

@@ -6,6 +6,19 @@ import '../ui_icons.dart';
/// ///
/// This widget provides a consistent look and feel for top app bars across the application. /// This widget provides a consistent look and feel for top app bars across the application.
class UiAppBar extends StatelessWidget implements PreferredSizeWidget { class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
const UiAppBar({
super.key,
this.title,
this.titleWidget,
this.leading,
this.actions,
this.height = kToolbarHeight,
this.centerTitle = true,
this.onLeadingPressed,
this.showBackButton = true,
this.bottom,
});
/// The title text to display in the app bar. /// The title text to display in the app bar.
final String? title; final String? title;
@@ -36,19 +49,6 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
/// Typically a [TabBar]. Only widgets that implement [PreferredSizeWidget] can be used at the bottom of an app bar. /// Typically a [TabBar]. Only widgets that implement [PreferredSizeWidget] can be used at the bottom of an app bar.
final PreferredSizeWidget? bottom; final PreferredSizeWidget? bottom;
const UiAppBar({
super.key,
this.title,
this.titleWidget,
this.leading,
this.actions,
this.height = kToolbarHeight,
this.centerTitle = true,
this.onLeadingPressed,
this.showBackButton = true,
this.bottom,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppBar( return AppBar(

View File

@@ -3,6 +3,96 @@ import '../ui_constants.dart';
/// A custom button widget with different variants and icon support. /// A custom button widget with different variants and icon support.
class UiButton extends StatelessWidget { class UiButton extends StatelessWidget {
/// Creates a [UiButton] with a custom button builder.
const UiButton({
super.key,
this.text,
this.child,
required this.buttonBuilder,
this.onPressed,
this.leadingIcon,
this.trailingIcon,
this.style,
this.iconSize = 20,
this.size = UiButtonSize.large,
this.fullWidth = false,
}) : assert(
text != null || child != null,
'Either text or child must be provided',
);
/// Creates a primary button using [ElevatedButton].
const UiButton.primary({
super.key,
this.text,
this.child,
this.onPressed,
this.leadingIcon,
this.trailingIcon,
this.style,
this.iconSize = 20,
this.size = UiButtonSize.large,
this.fullWidth = false,
}) : buttonBuilder = _elevatedButtonBuilder,
assert(
text != null || child != null,
'Either text or child must be provided',
);
/// Creates a secondary button using [OutlinedButton].
const UiButton.secondary({
super.key,
this.text,
this.child,
this.onPressed,
this.leadingIcon,
this.trailingIcon,
this.style,
this.iconSize = 20,
this.size = UiButtonSize.large,
this.fullWidth = false,
}) : buttonBuilder = _outlinedButtonBuilder,
assert(
text != null || child != null,
'Either text or child must be provided',
);
/// Creates a text button using [TextButton].
const UiButton.text({
super.key,
this.text,
this.child,
this.onPressed,
this.leadingIcon,
this.trailingIcon,
this.style,
this.iconSize = 20,
this.size = UiButtonSize.large,
this.fullWidth = false,
}) : buttonBuilder = _textButtonBuilder,
assert(
text != null || child != null,
'Either text or child must be provided',
);
/// Creates a ghost button (transparent background).
const UiButton.ghost({
super.key,
this.text,
this.child,
this.onPressed,
this.leadingIcon,
this.trailingIcon,
this.style,
this.iconSize = 20,
this.size = UiButtonSize.large,
this.fullWidth = false,
}) : buttonBuilder = _textButtonBuilder,
assert(
text != null || child != null,
'Either text or child must be provided',
);
/// The text to display on the button. /// The text to display on the button.
final String? text; final String? text;
@@ -39,100 +129,10 @@ class UiButton extends StatelessWidget {
) )
buttonBuilder; buttonBuilder;
/// Creates a [UiButton] with a custom button builder.
const UiButton({
super.key,
this.text,
this.child,
required this.buttonBuilder,
this.onPressed,
this.leadingIcon,
this.trailingIcon,
this.style,
this.iconSize = 20,
this.size = UiButtonSize.large,
this.fullWidth = false,
}) : assert(
text != null || child != null,
'Either text or child must be provided',
);
/// Creates a primary button using [ElevatedButton].
UiButton.primary({
super.key,
this.text,
this.child,
this.onPressed,
this.leadingIcon,
this.trailingIcon,
this.style,
this.iconSize = 20,
this.size = UiButtonSize.large,
this.fullWidth = false,
}) : buttonBuilder = _elevatedButtonBuilder,
assert(
text != null || child != null,
'Either text or child must be provided',
);
/// Creates a secondary button using [OutlinedButton].
UiButton.secondary({
super.key,
this.text,
this.child,
this.onPressed,
this.leadingIcon,
this.trailingIcon,
this.style,
this.iconSize = 20,
this.size = UiButtonSize.large,
this.fullWidth = false,
}) : buttonBuilder = _outlinedButtonBuilder,
assert(
text != null || child != null,
'Either text or child must be provided',
);
/// Creates a text button using [TextButton].
UiButton.text({
super.key,
this.text,
this.child,
this.onPressed,
this.leadingIcon,
this.trailingIcon,
this.style,
this.iconSize = 20,
this.size = UiButtonSize.large,
this.fullWidth = false,
}) : buttonBuilder = _textButtonBuilder,
assert(
text != null || child != null,
'Either text or child must be provided',
);
/// Creates a ghost button (transparent background).
UiButton.ghost({
super.key,
this.text,
this.child,
this.onPressed,
this.leadingIcon,
this.trailingIcon,
this.style,
this.iconSize = 20,
this.size = UiButtonSize.large,
this.fullWidth = false,
}) : buttonBuilder = _textButtonBuilder,
assert(
text != null || child != null,
'Either text or child must be provided',
);
@override @override
/// Builds the button UI. /// Builds the button UI.
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ButtonStyle? mergedStyle = style != null final ButtonStyle mergedStyle = style != null
? _getSizeStyle().merge(style) ? _getSizeStyle().merge(style)
: _getSizeStyle(); : _getSizeStyle();

View File

@@ -29,6 +29,19 @@ enum UiChipVariant {
/// A custom chip widget with supports for different sizes, themes, and icons. /// A custom chip widget with supports for different sizes, themes, and icons.
class UiChip extends StatelessWidget { class UiChip extends StatelessWidget {
/// Creates a [UiChip].
const UiChip({
super.key,
required this.label,
this.size = UiChipSize.medium,
this.variant = UiChipVariant.secondary,
this.leadingIcon,
this.trailingIcon,
this.onTap,
this.onTrailingIconTap,
this.isSelected = false,
});
/// The text label to display. /// The text label to display.
final String label; final String label;
@@ -53,19 +66,6 @@ class UiChip extends StatelessWidget {
/// Whether the chip is currently selected/active. /// Whether the chip is currently selected/active.
final bool isSelected; final bool isSelected;
/// Creates a [UiChip].
const UiChip({
super.key,
required this.label,
this.size = UiChipSize.medium,
this.variant = UiChipVariant.secondary,
this.leadingIcon,
this.trailingIcon,
this.onTap,
this.onTrailingIconTap,
this.isSelected = false,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color backgroundColor = _getBackgroundColor(); final Color backgroundColor = _getBackgroundColor();

View File

@@ -5,26 +5,6 @@ import '../ui_constants.dart';
/// A custom icon button with blur effect and different variants. /// A custom icon button with blur effect and different variants.
class UiIconButton extends StatelessWidget { class UiIconButton extends StatelessWidget {
/// The icon to display.
final IconData icon;
/// The size of the icon button.
final double size;
/// The size of the icon.
final double iconSize;
/// The background color of the button.
final Color backgroundColor;
/// The color of the icon.
final Color iconColor;
/// Whether to apply blur effect.
final bool useBlur;
/// Callback when the button is tapped.
final VoidCallback? onTap;
/// Creates a [UiIconButton] with custom properties. /// Creates a [UiIconButton] with custom properties.
const UiIconButton({ const UiIconButton({
@@ -59,6 +39,26 @@ class UiIconButton extends StatelessWidget {
}) : backgroundColor = UiColors.primary.withAlpha(96), }) : backgroundColor = UiColors.primary.withAlpha(96),
iconColor = UiColors.primary, iconColor = UiColors.primary,
useBlur = true; useBlur = true;
/// The icon to display.
final IconData icon;
/// The size of the icon button.
final double size;
/// The size of the icon.
final double iconSize;
/// The background color of the button.
final Color backgroundColor;
/// The color of the icon.
final Color iconColor;
/// Whether to apply blur effect.
final bool useBlur;
/// Callback when the button is tapped.
final VoidCallback? onTap;
@override @override
/// Builds the icon button UI. /// Builds the icon button UI.

View File

@@ -8,6 +8,26 @@ import '../ui_colors.dart';
/// ///
/// This widget combines a label and a [TextField] with consistent styling. /// This widget combines a label and a [TextField] with consistent styling.
class UiTextField extends StatelessWidget { class UiTextField extends StatelessWidget {
const UiTextField({
super.key,
this.label,
this.hintText,
this.onChanged,
this.controller,
this.keyboardType,
this.maxLines = 1,
this.obscureText = false,
this.textInputAction,
this.onSubmitted,
this.autofocus = false,
this.inputFormatters,
this.prefixIcon,
this.suffixIcon,
this.suffix,
this.readOnly = false,
this.onTap,
});
/// The label text to display above the text field. /// The label text to display above the text field.
final String? label; final String? label;
@@ -56,26 +76,6 @@ class UiTextField extends StatelessWidget {
/// Callback when the text field is tapped. /// Callback when the text field is tapped.
final VoidCallback? onTap; final VoidCallback? onTap;
const UiTextField({
super.key,
this.label,
this.hintText,
this.onChanged,
this.controller,
this.keyboardType,
this.maxLines = 1,
this.obscureText = false,
this.textInputAction,
this.onSubmitted,
this.autofocus = false,
this.inputFormatters,
this.prefixIcon,
this.suffixIcon,
this.suffix,
this.readOnly = false,
this.onTap,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(

View File

@@ -57,6 +57,7 @@ export 'src/entities/financial/invoice_item.dart';
export 'src/entities/financial/invoice_decline.dart'; export 'src/entities/financial/invoice_decline.dart';
export 'src/entities/financial/staff_payment.dart'; export 'src/entities/financial/staff_payment.dart';
export 'src/entities/financial/payment_summary.dart'; export 'src/entities/financial/payment_summary.dart';
export 'src/entities/financial/billing_period.dart';
export 'src/entities/financial/bank_account/bank_account.dart'; export 'src/entities/financial/bank_account/bank_account.dart';
export 'src/entities/financial/bank_account/business_bank_account.dart'; export 'src/entities/financial/bank_account/business_bank_account.dart';
export 'src/entities/financial/bank_account/staff_bank_account.dart'; export 'src/entities/financial/bank_account/staff_bank_account.dart';
@@ -111,3 +112,12 @@ export 'src/adapters/financial/payment_adapter.dart';
// Exceptions // Exceptions
export 'src/exceptions/app_exception.dart'; export 'src/exceptions/app_exception.dart';
// Reports
export 'src/entities/reports/daily_ops_report.dart';
export 'src/entities/reports/spend_report.dart';
export 'src/entities/reports/coverage_report.dart';
export 'src/entities/reports/forecast_report.dart';
export 'src/entities/reports/no_show_report.dart';
export 'src/entities/reports/performance_report.dart';
export 'src/entities/reports/reports_summary.dart';

View File

@@ -2,18 +2,18 @@ import '../../entities/availability/availability_slot.dart';
/// Adapter for [AvailabilitySlot] domain entity. /// Adapter for [AvailabilitySlot] domain entity.
class AvailabilityAdapter { class AvailabilityAdapter {
static const Map<String, Map<String, String>> _slotDefinitions = { static const Map<String, Map<String, String>> _slotDefinitions = <String, Map<String, String>>{
'MORNING': { 'MORNING': <String, String>{
'id': 'morning', 'id': 'morning',
'label': 'Morning', 'label': 'Morning',
'timeRange': '4:00 AM - 12:00 PM', 'timeRange': '4:00 AM - 12:00 PM',
}, },
'AFTERNOON': { 'AFTERNOON': <String, String>{
'id': 'afternoon', 'id': 'afternoon',
'label': 'Afternoon', 'label': 'Afternoon',
'timeRange': '12:00 PM - 6:00 PM', 'timeRange': '12:00 PM - 6:00 PM',
}, },
'EVENING': { 'EVENING': <String, String>{
'id': 'evening', 'id': 'evening',
'label': 'Evening', 'label': 'Evening',
'timeRange': '6:00 PM - 12:00 AM', 'timeRange': '6:00 PM - 12:00 AM',
@@ -22,7 +22,7 @@ class AvailabilityAdapter {
/// Converts a backend slot name (e.g. 'MORNING') to a Domain [AvailabilitySlot]. /// Converts a backend slot name (e.g. 'MORNING') to a Domain [AvailabilitySlot].
static AvailabilitySlot fromPrimitive(String slotName, {bool isAvailable = false}) { static AvailabilitySlot fromPrimitive(String slotName, {bool isAvailable = false}) {
final def = _slotDefinitions[slotName.toUpperCase()] ?? _slotDefinitions['MORNING']!; final Map<String, String> def = _slotDefinitions[slotName.toUpperCase()] ?? _slotDefinitions['MORNING']!;
return AvailabilitySlot( return AvailabilitySlot(
id: def['id']!, id: def['id']!,
label: def['label']!, label: def['label']!,

View File

@@ -1,4 +1,3 @@
import '../../entities/shifts/shift.dart';
import '../../entities/clock_in/attendance_status.dart'; import '../../entities/clock_in/attendance_status.dart';
/// Adapter for Clock In related data. /// Adapter for Clock In related data.

View File

@@ -18,7 +18,7 @@ class TaxFormAdapter {
final TaxFormType formType = _stringToType(type); final TaxFormType formType = _stringToType(type);
final TaxFormStatus formStatus = _stringToStatus(status); final TaxFormStatus formStatus = _stringToStatus(status);
final Map<String, dynamic> formDetails = final Map<String, dynamic> formDetails =
formData is Map ? Map<String, dynamic>.from(formData as Map) : <String, dynamic>{}; formData is Map ? Map<String, dynamic>.from(formData) : <String, dynamic>{};
if (formType == TaxFormType.i9) { if (formType == TaxFormType.i9) {
return I9TaxForm( return I9TaxForm(

View File

@@ -2,10 +2,6 @@ import 'package:equatable/equatable.dart';
/// Represents a specific time slot within a day (e.g., Morning, Afternoon, Evening). /// Represents a specific time slot within a day (e.g., Morning, Afternoon, Evening).
class AvailabilitySlot extends Equatable { class AvailabilitySlot extends Equatable {
final String id;
final String label;
final String timeRange;
final bool isAvailable;
const AvailabilitySlot({ const AvailabilitySlot({
required this.id, required this.id,
@@ -13,6 +9,10 @@ class AvailabilitySlot extends Equatable {
required this.timeRange, required this.timeRange,
this.isAvailable = true, this.isAvailable = true,
}); });
final String id;
final String label;
final String timeRange;
final bool isAvailable;
AvailabilitySlot copyWith({ AvailabilitySlot copyWith({
String? id, String? id,
@@ -29,5 +29,5 @@ class AvailabilitySlot extends Equatable {
} }
@override @override
List<Object?> get props => [id, label, timeRange, isAvailable]; List<Object?> get props => <Object?>[id, label, timeRange, isAvailable];
} }

View File

@@ -4,15 +4,15 @@ import 'availability_slot.dart';
/// Represents availability configuration for a specific date. /// Represents availability configuration for a specific date.
class DayAvailability extends Equatable { class DayAvailability extends Equatable {
final DateTime date;
final bool isAvailable;
final List<AvailabilitySlot> slots;
const DayAvailability({ const DayAvailability({
required this.date, required this.date,
this.isAvailable = false, this.isAvailable = false,
this.slots = const [], this.slots = const <AvailabilitySlot>[],
}); });
final DateTime date;
final bool isAvailable;
final List<AvailabilitySlot> slots;
DayAvailability copyWith({ DayAvailability copyWith({
DateTime? date, DateTime? date,
@@ -27,5 +27,5 @@ class DayAvailability extends Equatable {
} }
@override @override
List<Object?> get props => [date, isAvailable, slots]; List<Object?> get props => <Object?>[date, isAvailable, slots];
} }

View File

@@ -2,11 +2,6 @@ import 'package:equatable/equatable.dart';
/// Simple entity to hold attendance state /// Simple entity to hold attendance state
class AttendanceStatus extends Equatable { class AttendanceStatus extends Equatable {
final bool isCheckedIn;
final DateTime? checkInTime;
final DateTime? checkOutTime;
final String? activeShiftId;
final String? activeApplicationId;
const AttendanceStatus({ const AttendanceStatus({
this.isCheckedIn = false, this.isCheckedIn = false,
@@ -15,9 +10,14 @@ class AttendanceStatus extends Equatable {
this.activeShiftId, this.activeShiftId,
this.activeApplicationId, this.activeApplicationId,
}); });
final bool isCheckedIn;
final DateTime? checkInTime;
final DateTime? checkOutTime;
final String? activeShiftId;
final String? activeApplicationId;
@override @override
List<Object?> get props => [ List<Object?> get props => <Object?>[
isCheckedIn, isCheckedIn,
checkInTime, checkInTime,
checkOutTime, checkOutTime,

View File

@@ -0,0 +1,8 @@
/// Defines the period for billing calculations.
enum BillingPeriod {
/// Weekly billing period.
week,
/// Monthly billing period.
month,
}

View File

@@ -2,10 +2,6 @@ import 'package:equatable/equatable.dart';
/// Summary of staff earnings. /// Summary of staff earnings.
class PaymentSummary extends Equatable { class PaymentSummary extends Equatable {
final double weeklyEarnings;
final double monthlyEarnings;
final double pendingEarnings;
final double totalEarnings;
const PaymentSummary({ const PaymentSummary({
required this.weeklyEarnings, required this.weeklyEarnings,
@@ -13,9 +9,13 @@ class PaymentSummary extends Equatable {
required this.pendingEarnings, required this.pendingEarnings,
required this.totalEarnings, required this.totalEarnings,
}); });
final double weeklyEarnings;
final double monthlyEarnings;
final double pendingEarnings;
final double totalEarnings;
@override @override
List<Object?> get props => [ List<Object?> get props => <Object?>[
weeklyEarnings, weeklyEarnings,
monthlyEarnings, monthlyEarnings,
pendingEarnings, pendingEarnings,

View File

@@ -23,6 +23,21 @@ enum TimeCardStatus {
/// Represents a time card for a staff member. /// Represents a time card for a staff member.
class TimeCard extends Equatable { class TimeCard extends Equatable {
/// Creates a [TimeCard].
const TimeCard({
required this.id,
required this.shiftTitle,
required this.clientName,
required this.date,
required this.startTime,
required this.endTime,
required this.totalHours,
required this.hourlyRate,
required this.totalPay,
required this.status,
this.location,
});
/// Unique identifier of the time card (often matches Application ID). /// Unique identifier of the time card (often matches Application ID).
final String id; final String id;
/// Title of the shift. /// Title of the shift.
@@ -46,23 +61,8 @@ class TimeCard extends Equatable {
/// Location name. /// Location name.
final String? location; final String? location;
/// Creates a [TimeCard].
const TimeCard({
required this.id,
required this.shiftTitle,
required this.clientName,
required this.date,
required this.startTime,
required this.endTime,
required this.totalHours,
required this.hourlyRate,
required this.totalPay,
required this.status,
this.location,
});
@override @override
List<Object?> get props => [ List<Object?> get props => <Object?>[
id, id,
shiftTitle, shiftTitle,
clientName, clientName,

View File

@@ -26,7 +26,7 @@ class PermanentOrder extends Equatable {
final Map<String, double> roleRates; final Map<String, double> roleRates;
@override @override
List<Object?> get props => [ List<Object?> get props => <Object?>[
startDate, startDate,
permanentDays, permanentDays,
positions, positions,

View File

@@ -4,6 +4,15 @@ import 'package:equatable/equatable.dart';
/// ///
/// Attire items are specific clothing or equipment required for jobs. /// Attire items are specific clothing or equipment required for jobs.
class AttireItem extends Equatable { class AttireItem extends Equatable {
/// Creates an [AttireItem].
const AttireItem({
required this.id,
required this.label,
this.iconName,
this.imageUrl,
this.isMandatory = false,
});
/// Unique identifier of the attire item. /// Unique identifier of the attire item.
final String id; final String id;
@@ -19,15 +28,6 @@ class AttireItem extends Equatable {
/// Whether this item is mandatory for onboarding. /// Whether this item is mandatory for onboarding.
final bool isMandatory; final bool isMandatory;
/// Creates an [AttireItem].
const AttireItem({
required this.id,
required this.label,
this.iconName,
this.imageUrl,
this.isMandatory = false,
});
@override @override
List<Object?> get props => <Object?>[id, label, iconName, imageUrl, isMandatory]; List<Object?> get props => <Object?>[id, label, iconName, imageUrl, isMandatory];
} }

View File

@@ -22,7 +22,7 @@ enum ExperienceSkill {
static ExperienceSkill? fromString(String value) { static ExperienceSkill? fromString(String value) {
try { try {
return ExperienceSkill.values.firstWhere((e) => e.value == value); return ExperienceSkill.values.firstWhere((ExperienceSkill e) => e.value == value);
} catch (_) { } catch (_) {
return null; return null;
} }

View File

@@ -13,7 +13,7 @@ enum Industry {
static Industry? fromString(String value) { static Industry? fromString(String value) {
try { try {
return Industry.values.firstWhere((e) => e.value == value); return Industry.values.firstWhere((Industry e) => e.value == value);
} catch (_) { } catch (_) {
return null; return null;
} }

View File

@@ -11,6 +11,17 @@ enum DocumentStatus {
/// Represents a staff compliance document. /// Represents a staff compliance document.
class StaffDocument extends Equatable { class StaffDocument extends Equatable {
const StaffDocument({
required this.id,
required this.staffId,
required this.documentId,
required this.name,
this.description,
required this.status,
this.documentUrl,
this.expiryDate,
});
/// The unique identifier of the staff document record. /// The unique identifier of the staff document record.
final String id; final String id;
@@ -35,19 +46,8 @@ class StaffDocument extends Equatable {
/// The expiry date of the document. /// The expiry date of the document.
final DateTime? expiryDate; final DateTime? expiryDate;
const StaffDocument({
required this.id,
required this.staffId,
required this.documentId,
required this.name,
this.description,
required this.status,
this.documentUrl,
this.expiryDate,
});
@override @override
List<Object?> get props => [ List<Object?> get props => <Object?>[
id, id,
staffId, staffId,
documentId, documentId,

View File

@@ -5,6 +5,18 @@ enum TaxFormType { i9, w4 }
enum TaxFormStatus { notStarted, inProgress, submitted, approved, rejected } enum TaxFormStatus { notStarted, inProgress, submitted, approved, rejected }
abstract class TaxForm extends Equatable { abstract class TaxForm extends Equatable {
const TaxForm({
required this.id,
required this.title,
this.subtitle,
this.description,
this.status = TaxFormStatus.notStarted,
this.staffId,
this.formData = const <String, dynamic>{},
this.createdAt,
this.updatedAt,
});
final String id; final String id;
TaxFormType get type; TaxFormType get type;
final String title; final String title;
@@ -16,20 +28,8 @@ abstract class TaxForm extends Equatable {
final DateTime? createdAt; final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
const TaxForm({
required this.id,
required this.title,
this.subtitle,
this.description,
this.status = TaxFormStatus.notStarted,
this.staffId,
this.formData = const {},
this.createdAt,
this.updatedAt,
});
@override @override
List<Object?> get props => [ List<Object?> get props => <Object?>[
id, id,
type, type,
title, title,

View File

@@ -1,10 +1,7 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
class CoverageReport extends Equatable { class CoverageReport extends Equatable {
final double overallCoverage;
final int totalNeeded;
final int totalFilled;
final List<CoverageDay> dailyCoverage;
const CoverageReport({ const CoverageReport({
required this.overallCoverage, required this.overallCoverage,
@@ -12,16 +9,16 @@ class CoverageReport extends Equatable {
required this.totalFilled, required this.totalFilled,
required this.dailyCoverage, required this.dailyCoverage,
}); });
final double overallCoverage;
final int totalNeeded;
final int totalFilled;
final List<CoverageDay> dailyCoverage;
@override @override
List<Object?> get props => [overallCoverage, totalNeeded, totalFilled, dailyCoverage]; List<Object?> get props => <Object?>[overallCoverage, totalNeeded, totalFilled, dailyCoverage];
} }
class CoverageDay extends Equatable { class CoverageDay extends Equatable {
final DateTime date;
final int needed;
final int filled;
final double percentage;
const CoverageDay({ const CoverageDay({
required this.date, required this.date,
@@ -29,7 +26,12 @@ class CoverageDay extends Equatable {
required this.filled, required this.filled,
required this.percentage, required this.percentage,
}); });
final DateTime date;
final int needed;
final int filled;
final double percentage;
@override @override
List<Object?> get props => [date, needed, filled, percentage]; List<Object?> get props => <Object?>[date, needed, filled, percentage];
} }

View File

@@ -1,11 +1,7 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
class DailyOpsReport extends Equatable { class DailyOpsReport extends Equatable {
final int scheduledShifts;
final int workersConfirmed;
final int inProgressShifts;
final int completedShifts;
final List<DailyOpsShift> shifts;
const DailyOpsReport({ const DailyOpsReport({
required this.scheduledShifts, required this.scheduledShifts,
@@ -14,9 +10,14 @@ class DailyOpsReport extends Equatable {
required this.completedShifts, required this.completedShifts,
required this.shifts, required this.shifts,
}); });
final int scheduledShifts;
final int workersConfirmed;
final int inProgressShifts;
final int completedShifts;
final List<DailyOpsShift> shifts;
@override @override
List<Object?> get props => [ List<Object?> get props => <Object?>[
scheduledShifts, scheduledShifts,
workersConfirmed, workersConfirmed,
inProgressShifts, inProgressShifts,
@@ -26,15 +27,6 @@ class DailyOpsReport extends Equatable {
} }
class DailyOpsShift extends Equatable { class DailyOpsShift extends Equatable {
final String id;
final String title;
final String location;
final DateTime startTime;
final DateTime endTime;
final int workersNeeded;
final int filled;
final String status;
final double? hourlyRate;
const DailyOpsShift({ const DailyOpsShift({
required this.id, required this.id,
@@ -47,9 +39,18 @@ class DailyOpsShift extends Equatable {
required this.status, required this.status,
this.hourlyRate, this.hourlyRate,
}); });
final String id;
final String title;
final String location;
final DateTime startTime;
final DateTime endTime;
final int workersNeeded;
final int filled;
final String status;
final double? hourlyRate;
@override @override
List<Object?> get props => [ List<Object?> get props => <Object?>[
id, id,
title, title,
location, location,
@@ -61,3 +62,4 @@ class DailyOpsShift extends Equatable {
hourlyRate, hourlyRate,
]; ];
} }

View File

@@ -0,0 +1,79 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:equatable/equatable.dart';
class ForecastReport extends Equatable {
const ForecastReport({
required this.projectedSpend,
required this.projectedWorkers,
required this.averageLaborCost,
required this.chartData,
this.totalShifts = 0,
this.totalHours = 0.0,
this.avgWeeklySpend = 0.0,
this.weeklyBreakdown = const <ForecastWeek>[],
});
final double projectedSpend;
final int projectedWorkers;
final double averageLaborCost;
final List<ForecastPoint> chartData;
// New fields for the updated design
final int totalShifts;
final double totalHours;
final double avgWeeklySpend;
final List<ForecastWeek> weeklyBreakdown;
@override
List<Object?> get props => <Object?>[
projectedSpend,
projectedWorkers,
averageLaborCost,
chartData,
totalShifts,
totalHours,
avgWeeklySpend,
weeklyBreakdown,
];
}
class ForecastPoint extends Equatable {
const ForecastPoint({
required this.date,
required this.projectedCost,
required this.workersNeeded,
});
final DateTime date;
final double projectedCost;
final int workersNeeded;
@override
List<Object?> get props => <Object?>[date, projectedCost, workersNeeded];
}
class ForecastWeek extends Equatable {
const ForecastWeek({
required this.weekNumber,
required this.totalCost,
required this.shiftsCount,
required this.hoursCount,
required this.avgCostPerShift,
});
final int weekNumber;
final double totalCost;
final int shiftsCount;
final double hoursCount;
final double avgCostPerShift;
@override
List<Object?> get props => <Object?>[
weekNumber,
totalCost,
shiftsCount,
hoursCount,
avgCostPerShift,
];
}

View File

@@ -1,25 +1,22 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
class NoShowReport extends Equatable { class NoShowReport extends Equatable {
final int totalNoShows;
final double noShowRate;
final List<NoShowWorker> flaggedWorkers;
const NoShowReport({ const NoShowReport({
required this.totalNoShows, required this.totalNoShows,
required this.noShowRate, required this.noShowRate,
required this.flaggedWorkers, required this.flaggedWorkers,
}); });
final int totalNoShows;
final double noShowRate;
final List<NoShowWorker> flaggedWorkers;
@override @override
List<Object?> get props => [totalNoShows, noShowRate, flaggedWorkers]; List<Object?> get props => <Object?>[totalNoShows, noShowRate, flaggedWorkers];
} }
class NoShowWorker extends Equatable { class NoShowWorker extends Equatable {
final String id;
final String fullName;
final int noShowCount;
final double reliabilityScore;
const NoShowWorker({ const NoShowWorker({
required this.id, required this.id,
@@ -27,7 +24,12 @@ class NoShowWorker extends Equatable {
required this.noShowCount, required this.noShowCount,
required this.reliabilityScore, required this.reliabilityScore,
}); });
final String id;
final String fullName;
final int noShowCount;
final double reliabilityScore;
@override @override
List<Object?> get props => [id, fullName, noShowCount, reliabilityScore]; List<Object?> get props => <Object?>[id, fullName, noShowCount, reliabilityScore];
} }

View File

@@ -1,11 +1,7 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
class PerformanceReport extends Equatable { class PerformanceReport extends Equatable {
final double fillRate;
final double completionRate;
final double onTimeRate;
final double avgFillTimeHours; // in hours
final List<PerformanceMetric> keyPerformanceIndicators;
const PerformanceReport({ const PerformanceReport({
required this.fillRate, required this.fillRate,
@@ -14,22 +10,28 @@ class PerformanceReport extends Equatable {
required this.avgFillTimeHours, required this.avgFillTimeHours,
required this.keyPerformanceIndicators, required this.keyPerformanceIndicators,
}); });
final double fillRate;
final double completionRate;
final double onTimeRate;
final double avgFillTimeHours; // in hours
final List<PerformanceMetric> keyPerformanceIndicators;
@override @override
List<Object?> get props => [fillRate, completionRate, onTimeRate, avgFillTimeHours, keyPerformanceIndicators]; List<Object?> get props => <Object?>[fillRate, completionRate, onTimeRate, avgFillTimeHours, keyPerformanceIndicators];
} }
class PerformanceMetric extends Equatable { class PerformanceMetric extends Equatable { // e.g. 0.05 for +5%
final String label;
final String value;
final double trend; // e.g. 0.05 for +5%
const PerformanceMetric({ const PerformanceMetric({
required this.label, required this.label,
required this.value, required this.value,
required this.trend, required this.trend,
}); });
final String label;
final String value;
final double trend;
@override @override
List<Object?> get props => [label, value, trend]; List<Object?> get props => <Object?>[label, value, trend];
} }

View File

@@ -1,12 +1,7 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
class ReportsSummary extends Equatable { class ReportsSummary extends Equatable {
final double totalHours;
final double otHours;
final double totalSpend;
final double fillRate;
final double avgFillTimeHours;
final double noShowRate;
const ReportsSummary({ const ReportsSummary({
required this.totalHours, required this.totalHours,
@@ -16,9 +11,15 @@ class ReportsSummary extends Equatable {
required this.avgFillTimeHours, required this.avgFillTimeHours,
required this.noShowRate, required this.noShowRate,
}); });
final double totalHours;
final double otHours;
final double totalSpend;
final double fillRate;
final double avgFillTimeHours;
final double noShowRate;
@override @override
List<Object?> get props => [ List<Object?> get props => <Object?>[
totalHours, totalHours,
otHours, otHours,
totalSpend, totalSpend,
@@ -27,3 +28,4 @@ class ReportsSummary extends Equatable {
noShowRate, noShowRate,
]; ];
} }

View File

@@ -1,13 +1,7 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
class SpendReport extends Equatable { class SpendReport extends Equatable {
final double totalSpend;
final double averageCost;
final int paidInvoices;
final int pendingInvoices;
final int overdueInvoices;
final List<SpendInvoice> invoices;
final List<SpendChartPoint> chartData;
const SpendReport({ const SpendReport({
required this.totalSpend, required this.totalSpend,
@@ -19,11 +13,17 @@ class SpendReport extends Equatable {
required this.chartData, required this.chartData,
required this.industryBreakdown, required this.industryBreakdown,
}); });
final double totalSpend;
final double averageCost;
final int paidInvoices;
final int pendingInvoices;
final int overdueInvoices;
final List<SpendInvoice> invoices;
final List<SpendChartPoint> chartData;
final List<SpendIndustryCategory> industryBreakdown; final List<SpendIndustryCategory> industryBreakdown;
@override @override
List<Object?> get props => [ List<Object?> get props => <Object?>[
totalSpend, totalSpend,
averageCost, averageCost,
paidInvoices, paidInvoices,
@@ -36,27 +36,21 @@ class SpendReport extends Equatable {
} }
class SpendIndustryCategory extends Equatable { class SpendIndustryCategory extends Equatable {
final String name;
final double amount;
final double percentage;
const SpendIndustryCategory({ const SpendIndustryCategory({
required this.name, required this.name,
required this.amount, required this.amount,
required this.percentage, required this.percentage,
}); });
final String name;
final double amount;
final double percentage;
@override @override
List<Object?> get props => [name, amount, percentage]; List<Object?> get props => <Object?>[name, amount, percentage];
} }
class SpendInvoice extends Equatable { class SpendInvoice extends Equatable {
final String id;
final String invoiceNumber;
final DateTime issueDate;
final double amount;
final String status;
final String vendorName;
const SpendInvoice({ const SpendInvoice({
required this.id, required this.id,
@@ -67,19 +61,25 @@ class SpendInvoice extends Equatable {
required this.vendorName, required this.vendorName,
this.industry, this.industry,
}); });
final String id;
final String invoiceNumber;
final DateTime issueDate;
final double amount;
final String status;
final String vendorName;
final String? industry; final String? industry;
@override @override
List<Object?> get props => [id, invoiceNumber, issueDate, amount, status, vendorName, industry]; List<Object?> get props => <Object?>[id, invoiceNumber, issueDate, amount, status, vendorName, industry];
} }
class SpendChartPoint extends Equatable { class SpendChartPoint extends Equatable {
const SpendChartPoint({required this.date, required this.amount});
final DateTime date; final DateTime date;
final double amount; final double amount;
const SpendChartPoint({required this.date, required this.amount});
@override @override
List<Object?> get props => [date, amount]; List<Object?> get props => <Object?>[date, amount];
} }

View File

@@ -2,39 +2,6 @@ import 'package:equatable/equatable.dart';
import 'package:krow_domain/src/entities/shifts/break/break.dart'; import 'package:krow_domain/src/entities/shifts/break/break.dart';
class Shift extends Equatable { class Shift extends Equatable {
final String id;
final String title;
final String clientName;
final String? logoUrl;
final double hourlyRate;
final String location;
final String locationAddress;
final String date;
final String startTime;
final String endTime;
final String createdDate;
final bool? tipsAvailable;
final bool? travelTime;
final bool? mealProvided;
final bool? parkingAvailable;
final bool? gasCompensation;
final String? description;
final String? instructions;
final List<ShiftManager>? managers;
final double? latitude;
final double? longitude;
final String? status;
final int? durationDays; // For multi-day shifts
final int? requiredSlots;
final int? filledSlots;
final String? roleId;
final bool? hasApplied;
final double? totalValue;
final Break? breakInfo;
final String? orderId;
final String? orderType;
final List<ShiftSchedule>? schedules;
const Shift({ const Shift({
required this.id, required this.id,
required this.title, required this.title,
@@ -70,6 +37,39 @@ class Shift extends Equatable {
this.schedules, this.schedules,
}); });
final String id;
final String title;
final String clientName;
final String? logoUrl;
final double hourlyRate;
final String location;
final String locationAddress;
final String date;
final String startTime;
final String endTime;
final String createdDate;
final bool? tipsAvailable;
final bool? travelTime;
final bool? mealProvided;
final bool? parkingAvailable;
final bool? gasCompensation;
final String? description;
final String? instructions;
final List<ShiftManager>? managers;
final double? latitude;
final double? longitude;
final String? status;
final int? durationDays; // For multi-day shifts
final int? requiredSlots;
final int? filledSlots;
final String? roleId;
final bool? hasApplied;
final double? totalValue;
final Break? breakInfo;
final String? orderId;
final String? orderType;
final List<ShiftSchedule>? schedules;
@override @override
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[
id, id,

View File

@@ -32,8 +32,8 @@ sealed class AuthException extends AppException {
/// Thrown when email/password combination is incorrect. /// Thrown when email/password combination is incorrect.
class InvalidCredentialsException extends AuthException { class InvalidCredentialsException extends AuthException {
const InvalidCredentialsException({String? technicalMessage}) const InvalidCredentialsException({super.technicalMessage})
: super(code: 'AUTH_001', technicalMessage: technicalMessage); : super(code: 'AUTH_001');
@override @override
String get messageKey => 'errors.auth.invalid_credentials'; String get messageKey => 'errors.auth.invalid_credentials';
@@ -41,8 +41,8 @@ class InvalidCredentialsException extends AuthException {
/// Thrown when attempting to register with an email that already exists. /// Thrown when attempting to register with an email that already exists.
class AccountExistsException extends AuthException { class AccountExistsException extends AuthException {
const AccountExistsException({String? technicalMessage}) const AccountExistsException({super.technicalMessage})
: super(code: 'AUTH_002', technicalMessage: technicalMessage); : super(code: 'AUTH_002');
@override @override
String get messageKey => 'errors.auth.account_exists'; String get messageKey => 'errors.auth.account_exists';
@@ -50,8 +50,8 @@ class AccountExistsException extends AuthException {
/// Thrown when the user session has expired. /// Thrown when the user session has expired.
class SessionExpiredException extends AuthException { class SessionExpiredException extends AuthException {
const SessionExpiredException({String? technicalMessage}) const SessionExpiredException({super.technicalMessage})
: super(code: 'AUTH_003', technicalMessage: technicalMessage); : super(code: 'AUTH_003');
@override @override
String get messageKey => 'errors.auth.session_expired'; String get messageKey => 'errors.auth.session_expired';
@@ -59,8 +59,8 @@ class SessionExpiredException extends AuthException {
/// Thrown when user profile is not found in database after Firebase auth. /// Thrown when user profile is not found in database after Firebase auth.
class UserNotFoundException extends AuthException { class UserNotFoundException extends AuthException {
const UserNotFoundException({String? technicalMessage}) const UserNotFoundException({super.technicalMessage})
: super(code: 'AUTH_004', technicalMessage: technicalMessage); : super(code: 'AUTH_004');
@override @override
String get messageKey => 'errors.auth.user_not_found'; String get messageKey => 'errors.auth.user_not_found';
@@ -68,8 +68,8 @@ class UserNotFoundException extends AuthException {
/// Thrown when user is not authorized for the current app (wrong role). /// Thrown when user is not authorized for the current app (wrong role).
class UnauthorizedAppException extends AuthException { class UnauthorizedAppException extends AuthException {
const UnauthorizedAppException({String? technicalMessage}) const UnauthorizedAppException({super.technicalMessage})
: super(code: 'AUTH_005', technicalMessage: technicalMessage); : super(code: 'AUTH_005');
@override @override
String get messageKey => 'errors.auth.unauthorized_app'; String get messageKey => 'errors.auth.unauthorized_app';
@@ -77,8 +77,8 @@ class UnauthorizedAppException extends AuthException {
/// Thrown when password doesn't meet security requirements. /// Thrown when password doesn't meet security requirements.
class WeakPasswordException extends AuthException { class WeakPasswordException extends AuthException {
const WeakPasswordException({String? technicalMessage}) const WeakPasswordException({super.technicalMessage})
: super(code: 'AUTH_006', technicalMessage: technicalMessage); : super(code: 'AUTH_006');
@override @override
String get messageKey => 'errors.auth.weak_password'; String get messageKey => 'errors.auth.weak_password';
@@ -86,8 +86,8 @@ class WeakPasswordException extends AuthException {
/// Thrown when sign-up process fails. /// Thrown when sign-up process fails.
class SignUpFailedException extends AuthException { class SignUpFailedException extends AuthException {
const SignUpFailedException({String? technicalMessage}) const SignUpFailedException({super.technicalMessage})
: super(code: 'AUTH_007', technicalMessage: technicalMessage); : super(code: 'AUTH_007');
@override @override
String get messageKey => 'errors.auth.sign_up_failed'; String get messageKey => 'errors.auth.sign_up_failed';
@@ -95,8 +95,8 @@ class SignUpFailedException extends AuthException {
/// Thrown when sign-in process fails. /// Thrown when sign-in process fails.
class SignInFailedException extends AuthException { class SignInFailedException extends AuthException {
const SignInFailedException({String? technicalMessage}) const SignInFailedException({super.technicalMessage})
: super(code: 'AUTH_008', technicalMessage: technicalMessage); : super(code: 'AUTH_008');
@override @override
String get messageKey => 'errors.auth.sign_in_failed'; String get messageKey => 'errors.auth.sign_in_failed';
@@ -104,8 +104,8 @@ class SignInFailedException extends AuthException {
/// Thrown when email exists but password doesn't match. /// Thrown when email exists but password doesn't match.
class PasswordMismatchException extends AuthException { class PasswordMismatchException extends AuthException {
const PasswordMismatchException({String? technicalMessage}) const PasswordMismatchException({super.technicalMessage})
: super(code: 'AUTH_009', technicalMessage: technicalMessage); : super(code: 'AUTH_009');
@override @override
String get messageKey => 'errors.auth.password_mismatch'; String get messageKey => 'errors.auth.password_mismatch';
@@ -113,8 +113,8 @@ class PasswordMismatchException extends AuthException {
/// Thrown when account exists only with Google provider (no password). /// Thrown when account exists only with Google provider (no password).
class GoogleOnlyAccountException extends AuthException { class GoogleOnlyAccountException extends AuthException {
const GoogleOnlyAccountException({String? technicalMessage}) const GoogleOnlyAccountException({super.technicalMessage})
: super(code: 'AUTH_010', technicalMessage: technicalMessage); : super(code: 'AUTH_010');
@override @override
String get messageKey => 'errors.auth.google_only_account'; String get messageKey => 'errors.auth.google_only_account';
@@ -131,8 +131,8 @@ sealed class HubException extends AppException {
/// Thrown when attempting to delete a hub that has active orders. /// Thrown when attempting to delete a hub that has active orders.
class HubHasOrdersException extends HubException { class HubHasOrdersException extends HubException {
const HubHasOrdersException({String? technicalMessage}) const HubHasOrdersException({super.technicalMessage})
: super(code: 'HUB_001', technicalMessage: technicalMessage); : super(code: 'HUB_001');
@override @override
String get messageKey => 'errors.hub.has_orders'; String get messageKey => 'errors.hub.has_orders';
@@ -140,8 +140,8 @@ class HubHasOrdersException extends HubException {
/// Thrown when hub is not found. /// Thrown when hub is not found.
class HubNotFoundException extends HubException { class HubNotFoundException extends HubException {
const HubNotFoundException({String? technicalMessage}) const HubNotFoundException({super.technicalMessage})
: super(code: 'HUB_002', technicalMessage: technicalMessage); : super(code: 'HUB_002');
@override @override
String get messageKey => 'errors.hub.not_found'; String get messageKey => 'errors.hub.not_found';
@@ -149,8 +149,8 @@ class HubNotFoundException extends HubException {
/// Thrown when hub creation fails. /// Thrown when hub creation fails.
class HubCreationFailedException extends HubException { class HubCreationFailedException extends HubException {
const HubCreationFailedException({String? technicalMessage}) const HubCreationFailedException({super.technicalMessage})
: super(code: 'HUB_003', technicalMessage: technicalMessage); : super(code: 'HUB_003');
@override @override
String get messageKey => 'errors.hub.creation_failed'; String get messageKey => 'errors.hub.creation_failed';
@@ -167,8 +167,8 @@ sealed class OrderException extends AppException {
/// Thrown when order creation is attempted without a hub. /// Thrown when order creation is attempted without a hub.
class OrderMissingHubException extends OrderException { class OrderMissingHubException extends OrderException {
const OrderMissingHubException({String? technicalMessage}) const OrderMissingHubException({super.technicalMessage})
: super(code: 'ORDER_001', technicalMessage: technicalMessage); : super(code: 'ORDER_001');
@override @override
String get messageKey => 'errors.order.missing_hub'; String get messageKey => 'errors.order.missing_hub';
@@ -176,8 +176,8 @@ class OrderMissingHubException extends OrderException {
/// Thrown when order creation is attempted without a vendor. /// Thrown when order creation is attempted without a vendor.
class OrderMissingVendorException extends OrderException { class OrderMissingVendorException extends OrderException {
const OrderMissingVendorException({String? technicalMessage}) const OrderMissingVendorException({super.technicalMessage})
: super(code: 'ORDER_002', technicalMessage: technicalMessage); : super(code: 'ORDER_002');
@override @override
String get messageKey => 'errors.order.missing_vendor'; String get messageKey => 'errors.order.missing_vendor';
@@ -185,8 +185,8 @@ class OrderMissingVendorException extends OrderException {
/// Thrown when order creation fails. /// Thrown when order creation fails.
class OrderCreationFailedException extends OrderException { class OrderCreationFailedException extends OrderException {
const OrderCreationFailedException({String? technicalMessage}) const OrderCreationFailedException({super.technicalMessage})
: super(code: 'ORDER_003', technicalMessage: technicalMessage); : super(code: 'ORDER_003');
@override @override
String get messageKey => 'errors.order.creation_failed'; String get messageKey => 'errors.order.creation_failed';
@@ -194,8 +194,8 @@ class OrderCreationFailedException extends OrderException {
/// Thrown when shift creation fails. /// Thrown when shift creation fails.
class ShiftCreationFailedException extends OrderException { class ShiftCreationFailedException extends OrderException {
const ShiftCreationFailedException({String? technicalMessage}) const ShiftCreationFailedException({super.technicalMessage})
: super(code: 'ORDER_004', technicalMessage: technicalMessage); : super(code: 'ORDER_004');
@override @override
String get messageKey => 'errors.order.shift_creation_failed'; String get messageKey => 'errors.order.shift_creation_failed';
@@ -203,8 +203,8 @@ class ShiftCreationFailedException extends OrderException {
/// Thrown when order is missing required business context. /// Thrown when order is missing required business context.
class OrderMissingBusinessException extends OrderException { class OrderMissingBusinessException extends OrderException {
const OrderMissingBusinessException({String? technicalMessage}) const OrderMissingBusinessException({super.technicalMessage})
: super(code: 'ORDER_005', technicalMessage: technicalMessage); : super(code: 'ORDER_005');
@override @override
String get messageKey => 'errors.order.missing_business'; String get messageKey => 'errors.order.missing_business';
@@ -221,8 +221,8 @@ sealed class ProfileException extends AppException {
/// Thrown when staff profile is not found. /// Thrown when staff profile is not found.
class StaffProfileNotFoundException extends ProfileException { class StaffProfileNotFoundException extends ProfileException {
const StaffProfileNotFoundException({String? technicalMessage}) const StaffProfileNotFoundException({super.technicalMessage})
: super(code: 'PROFILE_001', technicalMessage: technicalMessage); : super(code: 'PROFILE_001');
@override @override
String get messageKey => 'errors.profile.staff_not_found'; String get messageKey => 'errors.profile.staff_not_found';
@@ -230,8 +230,8 @@ class StaffProfileNotFoundException extends ProfileException {
/// Thrown when business profile is not found. /// Thrown when business profile is not found.
class BusinessNotFoundException extends ProfileException { class BusinessNotFoundException extends ProfileException {
const BusinessNotFoundException({String? technicalMessage}) const BusinessNotFoundException({super.technicalMessage})
: super(code: 'PROFILE_002', technicalMessage: technicalMessage); : super(code: 'PROFILE_002');
@override @override
String get messageKey => 'errors.profile.business_not_found'; String get messageKey => 'errors.profile.business_not_found';
@@ -239,8 +239,8 @@ class BusinessNotFoundException extends ProfileException {
/// Thrown when profile update fails. /// Thrown when profile update fails.
class ProfileUpdateFailedException extends ProfileException { class ProfileUpdateFailedException extends ProfileException {
const ProfileUpdateFailedException({String? technicalMessage}) const ProfileUpdateFailedException({super.technicalMessage})
: super(code: 'PROFILE_003', technicalMessage: technicalMessage); : super(code: 'PROFILE_003');
@override @override
String get messageKey => 'errors.profile.update_failed'; String get messageKey => 'errors.profile.update_failed';
@@ -257,8 +257,8 @@ sealed class ShiftException extends AppException {
/// Thrown when no open roles are available for a shift. /// Thrown when no open roles are available for a shift.
class NoOpenRolesException extends ShiftException { class NoOpenRolesException extends ShiftException {
const NoOpenRolesException({String? technicalMessage}) const NoOpenRolesException({super.technicalMessage})
: super(code: 'SHIFT_001', technicalMessage: technicalMessage); : super(code: 'SHIFT_001');
@override @override
String get messageKey => 'errors.shift.no_open_roles'; String get messageKey => 'errors.shift.no_open_roles';
@@ -266,8 +266,8 @@ class NoOpenRolesException extends ShiftException {
/// Thrown when application for shift is not found. /// Thrown when application for shift is not found.
class ApplicationNotFoundException extends ShiftException { class ApplicationNotFoundException extends ShiftException {
const ApplicationNotFoundException({String? technicalMessage}) const ApplicationNotFoundException({super.technicalMessage})
: super(code: 'SHIFT_002', technicalMessage: technicalMessage); : super(code: 'SHIFT_002');
@override @override
String get messageKey => 'errors.shift.application_not_found'; String get messageKey => 'errors.shift.application_not_found';
@@ -275,8 +275,8 @@ class ApplicationNotFoundException extends ShiftException {
/// Thrown when no active shift is found for clock out. /// Thrown when no active shift is found for clock out.
class NoActiveShiftException extends ShiftException { class NoActiveShiftException extends ShiftException {
const NoActiveShiftException({String? technicalMessage}) const NoActiveShiftException({super.technicalMessage})
: super(code: 'SHIFT_003', technicalMessage: technicalMessage); : super(code: 'SHIFT_003');
@override @override
String get messageKey => 'errors.shift.no_active_shift'; String get messageKey => 'errors.shift.no_active_shift';
@@ -288,8 +288,8 @@ class NoActiveShiftException extends ShiftException {
/// Thrown when there is no network connection. /// Thrown when there is no network connection.
class NetworkException extends AppException { class NetworkException extends AppException {
const NetworkException({String? technicalMessage}) const NetworkException({super.technicalMessage})
: super(code: 'NET_001', technicalMessage: technicalMessage); : super(code: 'NET_001');
@override @override
String get messageKey => 'errors.generic.no_connection'; String get messageKey => 'errors.generic.no_connection';
@@ -297,8 +297,8 @@ class NetworkException extends AppException {
/// Thrown when an unexpected error occurs. /// Thrown when an unexpected error occurs.
class UnknownException extends AppException { class UnknownException extends AppException {
const UnknownException({String? technicalMessage}) const UnknownException({super.technicalMessage})
: super(code: 'UNKNOWN', technicalMessage: technicalMessage); : super(code: 'UNKNOWN');
@override @override
String get messageKey => 'errors.generic.unknown'; String get messageKey => 'errors.generic.unknown';
@@ -306,8 +306,8 @@ class UnknownException extends AppException {
/// Thrown when the server returns an error (500, etc.). /// Thrown when the server returns an error (500, etc.).
class ServerException extends AppException { class ServerException extends AppException {
const ServerException({String? technicalMessage}) const ServerException({super.technicalMessage})
: super(code: 'SRV_001', technicalMessage: technicalMessage); : super(code: 'SRV_001');
@override @override
String get messageKey => 'errors.generic.server_error'; String get messageKey => 'errors.generic.server_error';
@@ -315,8 +315,8 @@ class ServerException extends AppException {
/// Thrown when the service is unavailable (Data Connect down). /// Thrown when the service is unavailable (Data Connect down).
class ServiceUnavailableException extends AppException { class ServiceUnavailableException extends AppException {
const ServiceUnavailableException({String? technicalMessage}) const ServiceUnavailableException({super.technicalMessage})
: super(code: 'SRV_002', technicalMessage: technicalMessage); : super(code: 'SRV_002');
@override @override
String get messageKey => 'errors.generic.service_unavailable'; String get messageKey => 'errors.generic.service_unavailable';
@@ -324,8 +324,8 @@ class ServiceUnavailableException extends AppException {
/// Thrown when user is not authenticated. /// Thrown when user is not authenticated.
class NotAuthenticatedException extends AppException { class NotAuthenticatedException extends AppException {
const NotAuthenticatedException({String? technicalMessage}) const NotAuthenticatedException({super.technicalMessage})
: super(code: 'AUTH_NOT_LOGGED', technicalMessage: technicalMessage); : super(code: 'AUTH_NOT_LOGGED');
@override @override
String get messageKey => 'errors.auth.not_authenticated'; String get messageKey => 'errors.auth.not_authenticated';

View File

@@ -55,7 +55,7 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
emit(state.copyWith(status: ClientAuthStatus.loading)); emit(state.copyWith(status: ClientAuthStatus.loading));
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
final User user = await _signInWithEmail( final User user = await _signInWithEmail(
SignInWithEmailArguments(email: event.email, password: event.password), SignInWithEmailArguments(email: event.email, password: event.password),
@@ -77,7 +77,7 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
emit(state.copyWith(status: ClientAuthStatus.loading)); emit(state.copyWith(status: ClientAuthStatus.loading));
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
final User user = await _signUpWithEmail( final User user = await _signUpWithEmail(
SignUpWithEmailArguments( SignUpWithEmailArguments(
@@ -103,7 +103,7 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
emit(state.copyWith(status: ClientAuthStatus.loading)); emit(state.copyWith(status: ClientAuthStatus.loading));
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
final User user = await _signInWithSocial( final User user = await _signInWithSocial(
SignInWithSocialArguments(provider: event.provider), SignInWithSocialArguments(provider: event.provider),
@@ -125,7 +125,7 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
emit(state.copyWith(status: ClientAuthStatus.loading)); emit(state.copyWith(status: ClientAuthStatus.loading));
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
await _signOut(); await _signOut();
emit(state.copyWith(status: ClientAuthStatus.signedOut, user: null)); emit(state.copyWith(status: ClientAuthStatus.signedOut, user: null));

View File

@@ -1,261 +1,60 @@
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; // ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:krow_data_connect/krow_data_connect.dart' as data_connect; import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import '../../domain/models/billing_period.dart';
import '../../domain/repositories/billing_repository.dart'; import '../../domain/repositories/billing_repository.dart';
/// Implementation of [BillingRepository] in the Data layer. /// Implementation of [BillingRepository] that delegates to [dc.BillingConnectorRepository].
/// ///
/// This class is responsible for retrieving billing data from the /// This implementation follows the "Buffer Layer" pattern by using a dedicated
/// Data Connect layer and mapping it to Domain entities. /// connector repository from the data_connect package.
class BillingRepositoryImpl implements BillingRepository { class BillingRepositoryImpl implements BillingRepository {
/// Creates a [BillingRepositoryImpl].
BillingRepositoryImpl({ BillingRepositoryImpl({
data_connect.DataConnectService? service, dc.BillingConnectorRepository? connectorRepository,
}) : _service = service ?? data_connect.DataConnectService.instance; dc.DataConnectService? service,
}) : _connectorRepository = connectorRepository ??
dc.DataConnectService.instance.getBillingRepository(),
_service = service ?? dc.DataConnectService.instance;
final dc.BillingConnectorRepository _connectorRepository;
final dc.DataConnectService _service;
final data_connect.DataConnectService _service;
/// Fetches bank accounts associated with the business.
@override @override
Future<List<BusinessBankAccount>> getBankAccounts() async { Future<List<BusinessBankAccount>> getBankAccounts() async {
return _service.run(() async { final String businessId = await _service.getBusinessId();
final String businessId = await _service.getBusinessId(); return _connectorRepository.getBankAccounts(businessId: businessId);
final fdc.QueryResult<
data_connect.GetAccountsByOwnerIdData,
data_connect.GetAccountsByOwnerIdVariables> result =
await _service.connector
.getAccountsByOwnerId(ownerId: businessId)
.execute();
return result.data.accounts.map(_mapBankAccount).toList();
});
} }
/// Fetches the current bill amount by aggregating open invoices.
@override @override
Future<double> getCurrentBillAmount() async { Future<double> getCurrentBillAmount() async {
return _service.run(() async { final String businessId = await _service.getBusinessId();
final String businessId = await _service.getBusinessId(); return _connectorRepository.getCurrentBillAmount(businessId: businessId);
final fdc.QueryResult<data_connect.ListInvoicesByBusinessIdData,
data_connect.ListInvoicesByBusinessIdVariables> result =
await _service.connector
.listInvoicesByBusinessId(businessId: businessId)
.execute();
return result.data.invoices
.map(_mapInvoice)
.where((Invoice i) => i.status == InvoiceStatus.open)
.fold<double>(
0.0,
(double sum, Invoice item) => sum + item.totalAmount,
);
});
} }
/// Fetches the history of paid invoices.
@override @override
Future<List<Invoice>> getInvoiceHistory() async { Future<List<Invoice>> getInvoiceHistory() async {
return _service.run(() async { final String businessId = await _service.getBusinessId();
final String businessId = await _service.getBusinessId(); return _connectorRepository.getInvoiceHistory(businessId: businessId);
final fdc.QueryResult<data_connect.ListInvoicesByBusinessIdData,
data_connect.ListInvoicesByBusinessIdVariables> result =
await _service.connector
.listInvoicesByBusinessId(
businessId: businessId,
)
.limit(10)
.execute();
return result.data.invoices.map(_mapInvoice).toList();
});
} }
/// Fetches pending invoices (Open or Disputed).
@override @override
Future<List<Invoice>> getPendingInvoices() async { Future<List<Invoice>> getPendingInvoices() async {
return _service.run(() async { final String businessId = await _service.getBusinessId();
final String businessId = await _service.getBusinessId(); return _connectorRepository.getPendingInvoices(businessId: businessId);
final fdc.QueryResult<data_connect.ListInvoicesByBusinessIdData,
data_connect.ListInvoicesByBusinessIdVariables> result =
await _service.connector
.listInvoicesByBusinessId(businessId: businessId)
.execute();
return result.data.invoices
.map(_mapInvoice)
.where(
(Invoice i) =>
i.status == InvoiceStatus.open ||
i.status == InvoiceStatus.disputed,
)
.toList();
});
} }
/// Fetches the estimated savings amount.
@override @override
Future<double> getSavingsAmount() async { Future<double> getSavingsAmount() async {
// Simulating savings calculation (e.g., comparing to market rates). // Simulating savings calculation
await Future<void>.delayed(const Duration(milliseconds: 0));
return 0.0; return 0.0;
} }
/// Fetches the breakdown of spending.
@override @override
Future<List<InvoiceItem>> getSpendingBreakdown(BillingPeriod period) async { Future<List<InvoiceItem>> getSpendingBreakdown(BillingPeriod period) async {
return _service.run(() async { final String businessId = await _service.getBusinessId();
final String businessId = await _service.getBusinessId(); return _connectorRepository.getSpendingBreakdown(
businessId: businessId,
final DateTime now = DateTime.now(); period: period,
final DateTime start;
final DateTime end;
if (period == BillingPeriod.week) {
final int daysFromMonday = now.weekday - DateTime.monday;
final DateTime monday = DateTime(
now.year,
now.month,
now.day,
).subtract(Duration(days: daysFromMonday));
start = DateTime(monday.year, monday.month, monday.day);
end = DateTime(
monday.year, monday.month, monday.day + 6, 23, 59, 59, 999);
} else {
start = DateTime(now.year, now.month, 1);
end = DateTime(now.year, now.month + 1, 0, 23, 59, 59, 999);
}
final fdc.QueryResult<
data_connect.ListShiftRolesByBusinessAndDatesSummaryData,
data_connect.ListShiftRolesByBusinessAndDatesSummaryVariables>
result = await _service.connector
.listShiftRolesByBusinessAndDatesSummary(
businessId: businessId,
start: _service.toTimestamp(start),
end: _service.toTimestamp(end),
)
.execute();
final List<data_connect.ListShiftRolesByBusinessAndDatesSummaryShiftRoles>
shiftRoles = result.data.shiftRoles;
if (shiftRoles.isEmpty) {
return <InvoiceItem>[];
}
final Map<String, _RoleSummary> summary = <String, _RoleSummary>{};
for (final data_connect
.ListShiftRolesByBusinessAndDatesSummaryShiftRoles role
in shiftRoles) {
final String roleId = role.roleId;
final String roleName = role.role.name;
final double hours = role.hours ?? 0.0;
final double totalValue = role.totalValue ?? 0.0;
final _RoleSummary? existing = summary[roleId];
if (existing == null) {
summary[roleId] = _RoleSummary(
roleId: roleId,
roleName: roleName,
totalHours: hours,
totalValue: totalValue,
);
} else {
summary[roleId] = existing.copyWith(
totalHours: existing.totalHours + hours,
totalValue: existing.totalValue + totalValue,
);
}
}
return summary.values
.map(
(_RoleSummary item) => InvoiceItem(
id: item.roleId,
invoiceId: item.roleId,
staffId: item.roleName,
workHours: item.totalHours,
rate: item.totalHours > 0 ? item.totalValue / item.totalHours : 0,
amount: item.totalValue,
),
)
.toList();
});
}
Invoice _mapInvoice(data_connect.ListInvoicesByBusinessIdInvoices invoice) {
return Invoice(
id: invoice.id,
eventId: invoice.orderId,
businessId: invoice.businessId,
status: _mapInvoiceStatus(invoice.status),
totalAmount: invoice.amount,
workAmount: invoice.amount,
addonsAmount: invoice.otherCharges ?? 0,
invoiceNumber: invoice.invoiceNumber,
issueDate: _service.toDateTime(invoice.issueDate)!,
);
}
BusinessBankAccount _mapBankAccount(
data_connect.GetAccountsByOwnerIdAccounts account,
) {
return BusinessBankAccountAdapter.fromPrimitives(
id: account.id,
bank: account.bank,
last4: account.last4,
isPrimary: account.isPrimary ?? false,
expiryTime: _service.toDateTime(account.expiryTime),
);
}
InvoiceStatus _mapInvoiceStatus(
data_connect.EnumValue<data_connect.InvoiceStatus> status,
) {
if (status is data_connect.Known<data_connect.InvoiceStatus>) {
switch (status.value) {
case data_connect.InvoiceStatus.PAID:
return InvoiceStatus.paid;
case data_connect.InvoiceStatus.OVERDUE:
return InvoiceStatus.overdue;
case data_connect.InvoiceStatus.DISPUTED:
return InvoiceStatus.disputed;
case data_connect.InvoiceStatus.APPROVED:
return InvoiceStatus.verified;
case data_connect.InvoiceStatus.PENDING_REVIEW:
case data_connect.InvoiceStatus.PENDING:
case data_connect.InvoiceStatus.DRAFT:
return InvoiceStatus.open;
}
}
return InvoiceStatus.open;
}
}
class _RoleSummary {
const _RoleSummary({
required this.roleId,
required this.roleName,
required this.totalHours,
required this.totalValue,
});
final String roleId;
final String roleName;
final double totalHours;
final double totalValue;
_RoleSummary copyWith({
double? totalHours,
double? totalValue,
}) {
return _RoleSummary(
roleId: roleId,
roleName: roleName,
totalHours: totalHours ?? this.totalHours,
totalValue: totalValue ?? this.totalValue,
); );
} }
} }

View File

@@ -1,4 +0,0 @@
enum BillingPeriod {
week,
month,
}

View File

@@ -1,5 +1,4 @@
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import '../models/billing_period.dart';
/// Repository interface for billing related operations. /// Repository interface for billing related operations.
/// ///

View File

@@ -1,6 +1,5 @@
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import '../models/billing_period.dart';
import '../repositories/billing_repository.dart'; import '../repositories/billing_repository.dart';
/// Use case for fetching the spending breakdown items. /// Use case for fetching the spending breakdown items.

View File

@@ -47,7 +47,7 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
) async { ) async {
emit(state.copyWith(status: BillingStatus.loading)); emit(state.copyWith(status: BillingStatus.loading));
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
final List<dynamic> results = final List<dynamic> results =
await Future.wait<dynamic>(<Future<dynamic>>[ await Future.wait<dynamic>(<Future<dynamic>>[
@@ -102,7 +102,7 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
Emitter<BillingState> emit, Emitter<BillingState> emit,
) async { ) async {
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
final List<InvoiceItem> spendingItems = final List<InvoiceItem> spendingItems =
await _getSpendingBreakdown.call(event.period); await _getSpendingBreakdown.call(event.period);

View File

@@ -1,5 +1,5 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import '../../domain/models/billing_period.dart'; import 'package:krow_domain/krow_domain.dart';
/// Base class for all billing events. /// Base class for all billing events.
abstract class BillingEvent extends Equatable { abstract class BillingEvent extends Equatable {

View File

@@ -1,6 +1,5 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import '../../domain/models/billing_period.dart';
import '../models/billing_invoice_model.dart'; import '../models/billing_invoice_model.dart';
import '../models/spending_breakdown_model.dart'; import '../models/spending_breakdown_model.dart';

View File

@@ -2,7 +2,7 @@ import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/models/billing_period.dart'; import 'package:krow_domain/krow_domain.dart';
import '../blocs/billing_bloc.dart'; import '../blocs/billing_bloc.dart';
import '../blocs/billing_state.dart'; import '../blocs/billing_state.dart';
import '../blocs/billing_event.dart'; import '../blocs/billing_event.dart';

View File

@@ -1,68 +1,36 @@
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; // ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/coverage_repository.dart'; import '../../domain/repositories/coverage_repository.dart';
/// Implementation of [CoverageRepository] in the Data layer. /// Implementation of [CoverageRepository] that delegates to [dc.CoverageConnectorRepository].
/// ///
/// This class provides mock data for the coverage feature. /// This implementation follows the "Buffer Layer" pattern by using a dedicated
/// In a production environment, this would delegate to `packages/data_connect` /// connector repository from the data_connect package.
/// for real data access (e.g., Firebase Data Connect, REST API).
///
/// It strictly adheres to the Clean Architecture data layer responsibilities:
/// - No business logic (except necessary data transformation).
/// - Delegates to data sources (currently mock data, will be `data_connect`).
/// - Returns domain entities from `domain/ui_entities`.
class CoverageRepositoryImpl implements CoverageRepository { class CoverageRepositoryImpl implements CoverageRepository {
/// Creates a [CoverageRepositoryImpl].
CoverageRepositoryImpl({required dc.DataConnectService service}) : _service = service;
CoverageRepositoryImpl({
dc.CoverageConnectorRepository? connectorRepository,
dc.DataConnectService? service,
}) : _connectorRepository = connectorRepository ??
dc.DataConnectService.instance.getCoverageRepository(),
_service = service ?? dc.DataConnectService.instance;
final dc.CoverageConnectorRepository _connectorRepository;
final dc.DataConnectService _service; final dc.DataConnectService _service;
/// Fetches shifts for a specific date.
@override @override
Future<List<CoverageShift>> getShiftsForDate({required DateTime date}) async { Future<List<CoverageShift>> getShiftsForDate({required DateTime date}) async {
return _service.run(() async { final String businessId = await _service.getBusinessId();
final String businessId = await _service.getBusinessId(); return _connectorRepository.getShiftsForDate(
businessId: businessId,
final DateTime start = DateTime(date.year, date.month, date.day); date: date,
final DateTime end = DateTime(date.year, date.month, date.day, 23, 59, 59, 999); );
final fdc.QueryResult<dc.ListShiftRolesByBusinessAndDateRangeData,
dc.ListShiftRolesByBusinessAndDateRangeVariables> shiftRolesResult =
await _service.connector
.listShiftRolesByBusinessAndDateRange(
businessId: businessId,
start: _service.toTimestamp(start),
end: _service.toTimestamp(end),
)
.execute();
final fdc.QueryResult<dc.ListStaffsApplicationsByBusinessForDayData,
dc.ListStaffsApplicationsByBusinessForDayVariables> applicationsResult =
await _service.connector
.listStaffsApplicationsByBusinessForDay(
businessId: businessId,
dayStart: _service.toTimestamp(start),
dayEnd: _service.toTimestamp(end),
)
.execute();
return _mapCoverageShifts(
shiftRolesResult.data.shiftRoles,
applicationsResult.data.applications,
date,
);
});
} }
/// Fetches coverage statistics for a specific date.
@override @override
Future<CoverageStats> getCoverageStats({required DateTime date}) async { Future<CoverageStats> getCoverageStats({required DateTime date}) async {
// Get shifts for the date
final List<CoverageShift> shifts = await getShiftsForDate(date: date); final List<CoverageShift> shifts = await getShiftsForDate(date: date);
// Calculate statistics
final int totalNeeded = shifts.fold<int>( final int totalNeeded = shifts.fold<int>(
0, 0,
(int sum, CoverageShift shift) => sum + shift.workersNeeded, (int sum, CoverageShift shift) => sum + shift.workersNeeded,
@@ -90,129 +58,5 @@ class CoverageRepositoryImpl implements CoverageRepository {
late: late, late: late,
); );
} }
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 ?? shiftRole.shift.locationAddress ?? '',
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 ??
app.shiftRole.shift.locationAddress ??
'',
startTime: _formatTime(app.shiftRole.startTime) ?? '00:00',
workersNeeded: app.shiftRole.count,
date: app.shiftRole.shift.date?.toDateTime() ?? 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();
}
CoverageWorkerStatus _mapWorkerStatus(
dc.EnumValue<dc.ApplicationStatus> status,
) {
if (status is dc.Known<dc.ApplicationStatus>) {
switch (status.value) {
case dc.ApplicationStatus.PENDING:
return CoverageWorkerStatus.pending;
case dc.ApplicationStatus.REJECTED:
return CoverageWorkerStatus.rejected;
case dc.ApplicationStatus.CONFIRMED:
return CoverageWorkerStatus.confirmed;
case dc.ApplicationStatus.CHECKED_IN:
return CoverageWorkerStatus.checkedIn;
case dc.ApplicationStatus.CHECKED_OUT:
return CoverageWorkerStatus.checkedOut;
case dc.ApplicationStatus.LATE:
return CoverageWorkerStatus.late;
case dc.ApplicationStatus.NO_SHOW:
return CoverageWorkerStatus.noShow;
case dc.ApplicationStatus.COMPLETED:
return CoverageWorkerStatus.completed;
}
}
return CoverageWorkerStatus.pending;
}
String? _formatTime(fdc.Timestamp? timestamp) {
if (timestamp == null) {
return null;
}
final DateTime date = timestamp.toDateTime().toLocal();
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;
}

View File

@@ -43,7 +43,7 @@ class CoverageBloc extends Bloc<CoverageEvent, CoverageState>
); );
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
// Fetch shifts and stats concurrently // Fetch shifts and stats concurrently
final List<Object> results = await Future.wait<Object>(<Future<Object>>[ final List<Object> results = await Future.wait<Object>(<Future<Object>>[

View File

@@ -36,7 +36,7 @@ class ClientMainBottomBar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final t = Translations.of(context); final Translations t = Translations.of(context);
// Client App colors from design system // Client App colors from design system
const Color activeColor = UiColors.textPrimary; const Color activeColor = UiColors.textPrimary;
const Color inactiveColor = UiColors.textInactive; const Color inactiveColor = UiColors.textInactive;

View File

@@ -20,7 +20,7 @@ class ClientCreateOrderBloc
Emitter<ClientCreateOrderState> emit, Emitter<ClientCreateOrderState> emit,
) async { ) async {
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
final List<OrderType> types = await _getOrderTypesUseCase(); final List<OrderType> types = await _getOrderTypesUseCase();
emit(ClientCreateOrderLoadSuccess(types)); emit(ClientCreateOrderLoadSuccess(types));

View File

@@ -220,7 +220,7 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
) async { ) async {
emit(state.copyWith(status: OneTimeOrderStatus.loading)); emit(state.copyWith(status: OneTimeOrderStatus.loading));
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
final Map<String, double> roleRates = <String, double>{ final Map<String, double> roleRates = <String, double>{
for (final OneTimeOrderRoleOption role in state.roles) for (final OneTimeOrderRoleOption role in state.roles)

View File

@@ -272,7 +272,7 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
) async { ) async {
emit(state.copyWith(status: PermanentOrderStatus.loading)); emit(state.copyWith(status: PermanentOrderStatus.loading));
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
final Map<String, double> roleRates = <String, double>{ final Map<String, double> roleRates = <String, double>{
for (final PermanentOrderRoleOption role in state.roles) for (final PermanentOrderRoleOption role in state.roles)
@@ -280,7 +280,7 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
}; };
final PermanentOrderHubOption? selectedHub = state.selectedHub; final PermanentOrderHubOption? selectedHub = state.selectedHub;
if (selectedHub == null) { if (selectedHub == null) {
throw domain.OrderMissingHubException(); throw const domain.OrderMissingHubException();
} }
final domain.PermanentOrder order = domain.PermanentOrder( final domain.PermanentOrder order = domain.PermanentOrder(
startDate: state.startDate, startDate: state.startDate,

View File

@@ -69,7 +69,7 @@ class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState>
emit(const RapidOrderSubmitting()); emit(const RapidOrderSubmitting());
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
await _createRapidOrderUseCase( await _createRapidOrderUseCase(
RapidOrderArguments(description: message), RapidOrderArguments(description: message),

View File

@@ -289,7 +289,7 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
) async { ) async {
emit(state.copyWith(status: RecurringOrderStatus.loading)); emit(state.copyWith(status: RecurringOrderStatus.loading));
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
final Map<String, double> roleRates = <String, double>{ final Map<String, double> roleRates = <String, double>{
for (final RecurringOrderRoleOption role in state.roles) for (final RecurringOrderRoleOption role in state.roles)
@@ -297,7 +297,7 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
}; };
final RecurringOrderHubOption? selectedHub = state.selectedHub; final RecurringOrderHubOption? selectedHub = state.selectedHub;
if (selectedHub == null) { if (selectedHub == null) {
throw domain.OrderMissingHubException(); throw const domain.OrderMissingHubException();
} }
final domain.RecurringOrder order = domain.RecurringOrder( final domain.RecurringOrder order = domain.RecurringOrder(
startDate: state.startDate, startDate: state.startDate,

View File

@@ -1,119 +1,28 @@
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; // ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:firebase_data_connect/src/core/ref.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/home_repository_interface.dart'; import '../../domain/repositories/home_repository_interface.dart';
/// Implementation of [HomeRepositoryInterface] that delegates to [HomeRepositoryMock]. /// Implementation of [HomeRepositoryInterface] that delegates to [dc.HomeConnectorRepository].
/// ///
/// This implementation resides in the data layer and acts as a bridge between the /// This implementation follows the "Buffer Layer" pattern by using a dedicated
/// domain layer and the data source (in this case, a mock from data_connect). /// connector repository from the data_connect package.
class HomeRepositoryImpl implements HomeRepositoryInterface { class HomeRepositoryImpl implements HomeRepositoryInterface {
/// Creates a [HomeRepositoryImpl].
HomeRepositoryImpl(this._service); HomeRepositoryImpl({
dc.HomeConnectorRepository? connectorRepository,
dc.DataConnectService? service,
}) : _connectorRepository = connectorRepository ??
dc.DataConnectService.instance.getHomeRepository(),
_service = service ?? dc.DataConnectService.instance;
final dc.HomeConnectorRepository _connectorRepository;
final dc.DataConnectService _service; final dc.DataConnectService _service;
@override @override
Future<HomeDashboardData> getDashboardData() async { Future<HomeDashboardData> getDashboardData() async {
return _service.run(() async { final String businessId = await _service.getBusinessId();
final String businessId = await _service.getBusinessId(); return _connectorRepository.getDashboardData(businessId: businessId);
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<
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 = 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<
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,
);
});
} }
@override @override
@@ -121,7 +30,6 @@ class HomeRepositoryImpl implements HomeRepositoryInterface {
final dc.ClientSession? session = dc.ClientSessionStore.instance.session; final dc.ClientSession? session = dc.ClientSessionStore.instance.session;
final dc.ClientBusinessSession? business = session?.business; final dc.ClientBusinessSession? business = session?.business;
// If session data is available, return it immediately
if (business != null) { if (business != null) {
return UserSessionData( return UserSessionData(
businessName: business.businessName, businessName: business.businessName,
@@ -130,74 +38,39 @@ class HomeRepositoryImpl implements HomeRepositoryInterface {
} }
return await _service.run(() async { return await _service.run(() async {
// If session is not initialized, attempt to fetch business data to populate session
final String businessId = await _service.getBusinessId(); final String businessId = await _service.getBusinessId();
final fdc.QueryResult<dc.GetBusinessByIdData, dc.GetBusinessByIdVariables> final QueryResult<dc.GetBusinessByIdData, dc.GetBusinessByIdVariables> businessResult = await _service.connector
businessResult = await _service.connector
.getBusinessById(id: businessId) .getBusinessById(id: businessId)
.execute(); .execute();
if (businessResult.data.business == null) { final dc.GetBusinessByIdBusiness? b = businessResult.data.business;
if (b == null) {
throw Exception('Business data not found for ID: $businessId'); throw Exception('Business data not found for ID: $businessId');
} }
final dc.ClientSession updatedSession = dc.ClientSession( final dc.ClientSession updatedSession = dc.ClientSession(
business: dc.ClientBusinessSession( business: dc.ClientBusinessSession(
id: businessResult.data.business!.id, id: b.id,
businessName: businessResult.data.business?.businessName ?? '', businessName: b.businessName,
email: businessResult.data.business?.email ?? '', email: b.email ?? '',
city: businessResult.data.business?.city ?? '', city: b.city ?? '',
contactName: businessResult.data.business?.contactName ?? '', contactName: b.contactName ?? '',
companyLogoUrl: businessResult.data.business?.companyLogoUrl, companyLogoUrl: b.companyLogoUrl,
), ),
); );
dc.ClientSessionStore.instance.setSession(updatedSession); dc.ClientSessionStore.instance.setSession(updatedSession);
return UserSessionData( return UserSessionData(
businessName: businessResult.data.business!.businessName, businessName: b.businessName,
photoUrl: businessResult.data.business!.companyLogoUrl, photoUrl: b.companyLogoUrl,
); );
}); });
} }
@override @override
Future<List<ReorderItem>> getRecentReorders() async { Future<List<ReorderItem>> getRecentReorders() async {
return _service.run(() async { final String businessId = await _service.getBusinessId();
final String businessId = await _service.getBusinessId(); return _connectorRepository.getRecentReorders(businessId: businessId);
final DateTime now = DateTime.now();
final DateTime start = now.subtract(const Duration(days: 30));
final fdc.Timestamp startTimestamp = _service.toTimestamp(start);
final fdc.Timestamp endTimestamp = _service.toTimestamp(now);
final fdc.QueryResult<
dc.ListShiftRolesByBusinessDateRangeCompletedOrdersData,
dc.ListShiftRolesByBusinessDateRangeCompletedOrdersVariables
>
result = await _service.connector
.listShiftRolesByBusinessDateRangeCompletedOrders(
businessId: businessId,
start: startTimestamp,
end: endTimestamp,
)
.execute();
return result.data.shiftRoles.map((
dc.ListShiftRolesByBusinessDateRangeCompletedOrdersShiftRoles shiftRole,
) {
final String location =
shiftRole.shift.location ?? shiftRole.shift.locationAddress ?? '';
final String type = shiftRole.shift.order.orderType.stringValue;
return ReorderItem(
orderId: shiftRole.shift.order.id,
title: '${shiftRole.role.name} - ${shiftRole.shift.title}',
location: location,
hourlyRate: shiftRole.role.costPerHour,
hours: shiftRole.hours ?? 0,
workers: shiftRole.count,
type: type,
);
}).toList();
});
} }
} }

View File

@@ -37,7 +37,7 @@ class ClientHomeBloc extends Bloc<ClientHomeEvent, ClientHomeState>
) async { ) async {
emit(state.copyWith(status: ClientHomeStatus.loading)); emit(state.copyWith(status: ClientHomeStatus.loading));
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
// Get session data // Get session data
final UserSessionData sessionData = await _getUserSessionDataUseCase(); final UserSessionData sessionData = await _getUserSessionDataUseCase();

View File

@@ -651,9 +651,9 @@ class _ShiftOrderFormSheetState extends State<ShiftOrderFormSheet> {
return Container( return Container(
height: MediaQuery.of(context).size.height * 0.95, height: MediaQuery.of(context).size.height * 0.95,
decoration: BoxDecoration( decoration: const BoxDecoration(
color: UiColors.bgPrimary, color: UiColors.bgPrimary,
borderRadius: const BorderRadius.vertical(top: Radius.circular(UiConstants.space6)), borderRadius: BorderRadius.vertical(top: Radius.circular(UiConstants.space6)),
), ),
child: Column( child: Column(
children: <Widget>[ children: <Widget>[

View File

@@ -1,38 +1,31 @@
import 'dart:convert'; // ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:http/http.dart' as http;
import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart' as domain; import 'package:krow_domain/krow_domain.dart';
import 'package:krow_domain/krow_domain.dart'
show
HubHasOrdersException,
BusinessNotFoundException,
NotAuthenticatedException;
import '../../domain/repositories/hub_repository_interface.dart'; import '../../domain/repositories/hub_repository_interface.dart';
/// Implementation of [HubRepositoryInterface] backed by Data Connect. /// Implementation of [HubRepositoryInterface] that delegates to [dc.HubsConnectorRepository].
///
/// This implementation follows the "Buffer Layer" pattern by using a dedicated
/// connector repository from the data_connect package.
class HubRepositoryImpl implements HubRepositoryInterface { class HubRepositoryImpl implements HubRepositoryInterface {
HubRepositoryImpl({required dc.DataConnectService service})
: _service = service;
HubRepositoryImpl({
dc.HubsConnectorRepository? connectorRepository,
dc.DataConnectService? service,
}) : _connectorRepository = connectorRepository ??
dc.DataConnectService.instance.getHubsRepository(),
_service = service ?? dc.DataConnectService.instance;
final dc.HubsConnectorRepository _connectorRepository;
final dc.DataConnectService _service; final dc.DataConnectService _service;
@override @override
Future<List<domain.Hub>> getHubs() async { Future<List<Hub>> getHubs() async {
return _service.run(() async { final String businessId = await _service.getBusinessId();
final dc.GetBusinessesByUserIdBusinesses business = return _connectorRepository.getHubs(businessId: businessId);
await _getBusinessForCurrentUser();
final String teamId = await _getOrCreateTeamId(business);
return _fetchHubsForTeam(teamId: teamId, businessId: business.id);
});
} }
@override @override
Future<domain.Hub> createHub({ Future<Hub> createHub({
required String name, required String name,
required String address, required String address,
String? placeId, String? placeId,
@@ -44,77 +37,26 @@ class HubRepositoryImpl implements HubRepositoryInterface {
String? country, String? country,
String? zipCode, String? zipCode,
}) async { }) async {
return _service.run(() async { final String businessId = await _service.getBusinessId();
final dc.GetBusinessesByUserIdBusinesses business = return _connectorRepository.createHub(
await _getBusinessForCurrentUser(); businessId: businessId,
final String teamId = await _getOrCreateTeamId(business); name: name,
final _PlaceAddress? placeAddress = placeId == null || placeId.isEmpty address: address,
? null placeId: placeId,
: await _fetchPlaceAddress(placeId); latitude: latitude,
final String? cityValue = city ?? placeAddress?.city ?? business.city; longitude: longitude,
final String? stateValue = state ?? placeAddress?.state; city: city,
final String? streetValue = street ?? placeAddress?.street; state: state,
final String? countryValue = country ?? placeAddress?.country; street: street,
final String? zipCodeValue = zipCode ?? placeAddress?.zipCode; country: country,
zipCode: zipCode,
final OperationResult<dc.CreateTeamHubData, dc.CreateTeamHubVariables> );
result = await _service.connector
.createTeamHub(teamId: teamId, hubName: name, address: address)
.placeId(placeId)
.latitude(latitude)
.longitude(longitude)
.city(cityValue?.isNotEmpty == true ? cityValue : '')
.state(stateValue)
.street(streetValue)
.country(countryValue)
.zipCode(zipCodeValue)
.execute();
final String createdId = result.data.teamHub_insert.id;
final List<domain.Hub> hubs = await _fetchHubsForTeam(
teamId: teamId,
businessId: business.id,
);
domain.Hub? createdHub;
for (final domain.Hub hub in hubs) {
if (hub.id == createdId) {
createdHub = hub;
break;
}
}
return createdHub ??
domain.Hub(
id: createdId,
businessId: business.id,
name: name,
address: address,
nfcTagId: null,
status: domain.HubStatus.active,
);
});
} }
@override @override
Future<void> deleteHub(String id) async { Future<void> deleteHub(String id) async {
return _service.run(() async { final String businessId = await _service.getBusinessId();
final String businessId = await _service.getBusinessId(); return _connectorRepository.deleteHub(businessId: businessId, id: id);
final QueryResult<
dc.ListOrdersByBusinessAndTeamHubData,
dc.ListOrdersByBusinessAndTeamHubVariables
>
result = await _service.connector
.listOrdersByBusinessAndTeamHub(businessId: businessId, teamHubId: id)
.execute();
if (result.data.orders.isNotEmpty) {
throw HubHasOrdersException(
technicalMessage: 'Hub $id has ${result.data.orders.length} orders',
);
}
await _service.connector.deleteTeamHub(id: id).execute();
});
} }
@override @override
@@ -125,7 +67,7 @@ class HubRepositoryImpl implements HubRepositoryInterface {
} }
@override @override
Future<domain.Hub> updateHub({ Future<Hub> updateHub({
required String id, required String id,
String? name, String? name,
String? address, String? address,
@@ -138,283 +80,21 @@ class HubRepositoryImpl implements HubRepositoryInterface {
String? country, String? country,
String? zipCode, String? zipCode,
}) async { }) async {
return _service.run(() async { final String businessId = await _service.getBusinessId();
final _PlaceAddress? placeAddress = return _connectorRepository.updateHub(
placeId == null || placeId.isEmpty businessId: businessId,
? null id: id,
: await _fetchPlaceAddress(placeId); name: name,
address: address,
final dc.UpdateTeamHubVariablesBuilder builder = _service.connector placeId: placeId,
.updateTeamHub(id: id); latitude: latitude,
longitude: longitude,
if (name != null) builder.hubName(name); city: city,
if (address != null) builder.address(address); state: state,
if (placeId != null || placeAddress != null) { street: street,
builder.placeId(placeId ?? placeAddress?.street); country: country,
} zipCode: zipCode,
if (latitude != null) builder.latitude(latitude);
if (longitude != null) builder.longitude(longitude);
if (city != null || placeAddress?.city != null) {
builder.city(city ?? placeAddress?.city);
}
if (state != null || placeAddress?.state != null) {
builder.state(state ?? placeAddress?.state);
}
if (street != null || placeAddress?.street != null) {
builder.street(street ?? placeAddress?.street);
}
if (country != null || placeAddress?.country != null) {
builder.country(country ?? placeAddress?.country);
}
if (zipCode != null || placeAddress?.zipCode != null) {
builder.zipCode(zipCode ?? placeAddress?.zipCode);
}
await builder.execute();
final dc.GetBusinessesByUserIdBusinesses business =
await _getBusinessForCurrentUser();
final String teamId = await _getOrCreateTeamId(business);
final List<domain.Hub> hubs = await _fetchHubsForTeam(
teamId: teamId,
businessId: business.id,
);
for (final domain.Hub hub in hubs) {
if (hub.id == id) return hub;
}
// Fallback: return a reconstructed Hub from the update inputs.
return domain.Hub(
id: id,
businessId: business.id,
name: name ?? '',
address: address ?? '',
nfcTagId: null,
status: domain.HubStatus.active,
);
});
}
Future<dc.GetBusinessesByUserIdBusinesses>
_getBusinessForCurrentUser() async {
final dc.ClientSession? session = dc.ClientSessionStore.instance.session;
final dc.ClientBusinessSession? cachedBusiness = session?.business;
if (cachedBusiness != null) {
return dc.GetBusinessesByUserIdBusinesses(
id: cachedBusiness.id,
businessName: cachedBusiness.businessName,
userId: _service.auth.currentUser?.uid ?? '',
rateGroup: const dc.Known<dc.BusinessRateGroup>(
dc.BusinessRateGroup.STANDARD,
),
status: const dc.Known<dc.BusinessStatus>(dc.BusinessStatus.ACTIVE),
contactName: cachedBusiness.contactName,
companyLogoUrl: cachedBusiness.companyLogoUrl,
phone: null,
email: cachedBusiness.email,
hubBuilding: null,
address: null,
city: cachedBusiness.city,
area: null,
sector: null,
notes: null,
createdAt: null,
updatedAt: null,
);
}
final firebase.User? user = _service.auth.currentUser;
if (user == null) {
throw const NotAuthenticatedException(
technicalMessage: 'No Firebase user in currentUser',
);
}
final QueryResult<
dc.GetBusinessesByUserIdData,
dc.GetBusinessesByUserIdVariables
>
result = await _service.connector
.getBusinessesByUserId(userId: user.uid)
.execute();
if (result.data.businesses.isEmpty) {
await _service.auth.signOut();
throw BusinessNotFoundException(
technicalMessage: 'No business found for user ${user.uid}',
);
}
final dc.GetBusinessesByUserIdBusinesses business =
result.data.businesses.first;
if (session != null) {
dc.ClientSessionStore.instance.setSession(
dc.ClientSession(
business: dc.ClientBusinessSession(
id: business.id,
businessName: business.businessName,
email: business.email,
city: business.city,
contactName: business.contactName,
companyLogoUrl: business.companyLogoUrl,
),
),
);
}
return business;
}
Future<String> _getOrCreateTeamId(
dc.GetBusinessesByUserIdBusinesses business,
) async {
final QueryResult<dc.GetTeamsByOwnerIdData, dc.GetTeamsByOwnerIdVariables>
teamsResult = await _service.connector
.getTeamsByOwnerId(ownerId: business.id)
.execute();
if (teamsResult.data.teams.isNotEmpty) {
return teamsResult.data.teams.first.id;
}
final dc.CreateTeamVariablesBuilder createTeamBuilder = _service.connector
.createTeam(
teamName: '${business.businessName} Team',
ownerId: business.id,
ownerName: business.contactName ?? '',
ownerRole: 'OWNER',
);
if (business.email != null) {
createTeamBuilder.email(business.email);
}
final OperationResult<dc.CreateTeamData, dc.CreateTeamVariables>
createTeamResult = await createTeamBuilder.execute();
final String teamId = createTeamResult.data.team_insert.id;
return teamId;
}
Future<List<domain.Hub>> _fetchHubsForTeam({
required String teamId,
required String businessId,
}) async {
final QueryResult<
dc.GetTeamHubsByTeamIdData,
dc.GetTeamHubsByTeamIdVariables
>
hubsResult = await _service.connector
.getTeamHubsByTeamId(teamId: teamId)
.execute();
return hubsResult.data.teamHubs
.map(
(dc.GetTeamHubsByTeamIdTeamHubs hub) => domain.Hub(
id: hub.id,
businessId: businessId,
name: hub.hubName,
address: hub.address,
nfcTagId: null,
status: hub.isActive
? domain.HubStatus.active
: domain.HubStatus.inactive,
),
)
.toList();
}
Future<_PlaceAddress?> _fetchPlaceAddress(String placeId) async {
final Uri uri = Uri.https(
'maps.googleapis.com',
'/maps/api/place/details/json',
<String, String>{
'place_id': placeId,
'fields': 'address_component',
'key': AppConfig.googleMapsApiKey,
},
); );
try {
final http.Response response = await http.get(uri);
if (response.statusCode != 200) {
return null;
}
final Map<String, dynamic> payload =
json.decode(response.body) as Map<String, dynamic>;
if (payload['status'] != 'OK') {
return null;
}
final Map<String, dynamic>? result =
payload['result'] as Map<String, dynamic>?;
final List<dynamic>? components =
result?['address_components'] as List<dynamic>?;
if (components == null || components.isEmpty) {
return null;
}
String? streetNumber;
String? route;
String? city;
String? state;
String? country;
String? zipCode;
for (final dynamic entry in components) {
final Map<String, dynamic> component = entry as Map<String, dynamic>;
final List<dynamic> types =
component['types'] as List<dynamic>? ?? <dynamic>[];
final String? longName = component['long_name'] as String?;
final String? shortName = component['short_name'] as String?;
if (types.contains('street_number')) {
streetNumber = longName;
} else if (types.contains('route')) {
route = longName;
} else if (types.contains('locality')) {
city = longName;
} else if (types.contains('postal_town')) {
city ??= longName;
} else if (types.contains('administrative_area_level_2')) {
city ??= longName;
} else if (types.contains('administrative_area_level_1')) {
state = shortName ?? longName;
} else if (types.contains('country')) {
country = shortName ?? longName;
} else if (types.contains('postal_code')) {
zipCode = longName;
}
}
final String streetValue = <String?>[streetNumber, route]
.where((String? value) => value != null && value.isNotEmpty)
.join(' ')
.trim();
return _PlaceAddress(
street: streetValue.isEmpty == true ? null : streetValue,
city: city,
state: state,
country: country,
zipCode: zipCode,
);
} catch (_) {
return null;
}
} }
} }
class _PlaceAddress {
const _PlaceAddress({
this.street,
this.city,
this.state,
this.country,
this.zipCode,
});
final String? street;
final String? city;
final String? state;
final String? country;
final String? zipCode;
}

View File

@@ -73,7 +73,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
emit(state.copyWith(status: ClientHubsStatus.loading)); emit(state.copyWith(status: ClientHubsStatus.loading));
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
final List<Hub> hubs = await _getHubsUseCase(); final List<Hub> hubs = await _getHubsUseCase();
emit(state.copyWith(status: ClientHubsStatus.success, hubs: hubs)); emit(state.copyWith(status: ClientHubsStatus.success, hubs: hubs));
@@ -92,7 +92,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
emit(state.copyWith(status: ClientHubsStatus.actionInProgress)); emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
await _createHubUseCase( await _createHubUseCase(
CreateHubArguments( CreateHubArguments(
@@ -132,7 +132,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
emit(state.copyWith(status: ClientHubsStatus.actionInProgress)); emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
await _updateHubUseCase( await _updateHubUseCase(
UpdateHubArguments( UpdateHubArguments(
@@ -172,7 +172,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
emit(state.copyWith(status: ClientHubsStatus.actionInProgress)); emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId)); await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId));
final List<Hub> hubs = await _getHubsUseCase(); final List<Hub> hubs = await _getHubsUseCase();
@@ -198,7 +198,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
emit(state.copyWith(status: ClientHubsStatus.actionInProgress)); emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
await handleError( await handleError(
emit: emit, emit: emit.call,
action: () async { action: () async {
await _assignNfcTagUseCase( await _assignNfcTagUseCase(
AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId), AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId),

View File

@@ -1,493 +1,89 @@
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart';
import '../../domain/entities/daily_ops_report.dart'; import 'package:krow_domain/krow_domain.dart';
import '../../domain/entities/spend_report.dart';
import '../../domain/entities/coverage_report.dart';
import '../../domain/entities/forecast_report.dart';
import '../../domain/entities/performance_report.dart';
import '../../domain/entities/no_show_report.dart';
import '../../domain/entities/reports_summary.dart';
import '../../domain/repositories/reports_repository.dart'; import '../../domain/repositories/reports_repository.dart';
/// Implementation of [ReportsRepository] that delegates to [ReportsConnectorRepository].
///
/// This implementation follows the "Buffer Layer" pattern by using a dedicated
/// connector repository from the data_connect package.
class ReportsRepositoryImpl implements ReportsRepository { class ReportsRepositoryImpl implements ReportsRepository {
final DataConnectService _service;
ReportsRepositoryImpl({DataConnectService? service}) ReportsRepositoryImpl({ReportsConnectorRepository? connectorRepository})
: _service = service ?? DataConnectService.instance; : _connectorRepository = connectorRepository ?? DataConnectService.instance.getReportsRepository();
final ReportsConnectorRepository _connectorRepository;
@override @override
Future<DailyOpsReport> getDailyOpsReport({ Future<DailyOpsReport> getDailyOpsReport({
String? businessId, String? businessId,
required DateTime date, required DateTime date,
}) async { }) => _connectorRepository.getDailyOpsReport(
return await _service.run(() async { businessId: businessId,
final String id = businessId ?? await _service.getBusinessId(); date: date,
final response = await _service.connector
.listShiftsForDailyOpsByBusiness(
businessId: id,
date: _service.toTimestamp(date),
)
.execute();
final shifts = response.data.shifts;
int scheduledShifts = shifts.length;
int workersConfirmed = 0;
int inProgressShifts = 0;
int completedShifts = 0;
final List<DailyOpsShift> dailyOpsShifts = [];
for (final shift in shifts) {
workersConfirmed += shift.filled ?? 0;
final statusStr = shift.status?.stringValue ?? '';
if (statusStr == 'IN_PROGRESS') inProgressShifts++;
if (statusStr == 'COMPLETED') completedShifts++;
dailyOpsShifts.add(DailyOpsShift(
id: shift.id,
title: shift.title ?? '',
location: shift.location ?? '',
startTime: shift.startTime?.toDateTime() ?? DateTime.now(),
endTime: shift.endTime?.toDateTime() ?? DateTime.now(),
workersNeeded: shift.workersNeeded ?? 0,
filled: shift.filled ?? 0,
status: statusStr,
));
}
return DailyOpsReport(
scheduledShifts: scheduledShifts,
workersConfirmed: workersConfirmed,
inProgressShifts: inProgressShifts,
completedShifts: completedShifts,
shifts: dailyOpsShifts,
); );
});
}
@override @override
Future<SpendReport> getSpendReport({ Future<SpendReport> getSpendReport({
String? businessId, String? businessId,
required DateTime startDate, required DateTime startDate,
required DateTime endDate, required DateTime endDate,
}) async { }) => _connectorRepository.getSpendReport(
return await _service.run(() async { businessId: businessId,
final String id = businessId ?? await _service.getBusinessId(); startDate: startDate,
final response = await _service.connector endDate: endDate,
.listInvoicesForSpendByBusiness(
businessId: id,
startDate: _service.toTimestamp(startDate),
endDate: _service.toTimestamp(endDate),
)
.execute();
final invoices = response.data.invoices;
double totalSpend = 0.0;
int paidInvoices = 0;
int pendingInvoices = 0;
int overdueInvoices = 0;
final List<SpendInvoice> spendInvoices = [];
final Map<DateTime, double> dailyAggregates = {};
final Map<String, double> industryAggregates = {};
for (final inv in invoices) {
final amount = (inv.amount ?? 0.0).toDouble();
totalSpend += amount;
final statusStr = inv.status.stringValue;
if (statusStr == 'PAID') {
paidInvoices++;
} else if (statusStr == 'PENDING') {
pendingInvoices++;
} else if (statusStr == 'OVERDUE') {
overdueInvoices++;
}
final industry = inv.vendor?.serviceSpecialty ?? 'Other';
industryAggregates[industry] = (industryAggregates[industry] ?? 0.0) + amount;
final issueDateTime = inv.issueDate.toDateTime();
spendInvoices.add(SpendInvoice(
id: inv.id,
invoiceNumber: inv.invoiceNumber ?? '',
issueDate: issueDateTime,
amount: amount,
status: statusStr,
vendorName: inv.vendor?.companyName ?? 'Unknown',
industry: industry,
));
// Chart data aggregation
final date = DateTime(issueDateTime.year, issueDateTime.month, issueDateTime.day);
dailyAggregates[date] = (dailyAggregates[date] ?? 0.0) + amount;
}
// Ensure chart data covers all days in range
final Map<DateTime, double> completeDailyAggregates = {};
for (int i = 0; i <= endDate.difference(startDate).inDays; i++) {
final date = startDate.add(Duration(days: i));
final normalizedDate = DateTime(date.year, date.month, date.day);
completeDailyAggregates[normalizedDate] =
dailyAggregates[normalizedDate] ?? 0.0;
}
final List<SpendChartPoint> chartData = completeDailyAggregates.entries
.map((e) => SpendChartPoint(date: e.key, amount: e.value))
.toList()
..sort((a, b) => a.date.compareTo(b.date));
final List<SpendIndustryCategory> industryBreakdown = industryAggregates.entries
.map((e) => SpendIndustryCategory(
name: e.key,
amount: e.value,
percentage: totalSpend > 0 ? (e.value / totalSpend * 100) : 0,
))
.toList()
..sort((a, b) => b.amount.compareTo(a.amount));
final daysCount = endDate.difference(startDate).inDays + 1;
return SpendReport(
totalSpend: totalSpend,
averageCost: daysCount > 0 ? totalSpend / daysCount : 0,
paidInvoices: paidInvoices,
pendingInvoices: pendingInvoices,
overdueInvoices: overdueInvoices,
invoices: spendInvoices,
chartData: chartData,
industryBreakdown: industryBreakdown,
); );
});
}
@override @override
Future<CoverageReport> getCoverageReport({ Future<CoverageReport> getCoverageReport({
String? businessId, String? businessId,
required DateTime startDate, required DateTime startDate,
required DateTime endDate, required DateTime endDate,
}) async { }) => _connectorRepository.getCoverageReport(
return await _service.run(() async { businessId: businessId,
final String id = businessId ?? await _service.getBusinessId(); startDate: startDate,
final response = await _service.connector endDate: endDate,
.listShiftsForCoverage(
businessId: id,
startDate: _service.toTimestamp(startDate),
endDate: _service.toTimestamp(endDate),
)
.execute();
final shifts = response.data.shifts;
int totalNeeded = 0;
int totalFilled = 0;
final Map<DateTime, (int, int)> dailyStats = {};
for (final shift in shifts) {
final shiftDate = shift.date?.toDateTime() ?? DateTime.now();
final date = DateTime(shiftDate.year, shiftDate.month, shiftDate.day);
final needed = shift.workersNeeded ?? 0;
final filled = shift.filled ?? 0;
totalNeeded += needed;
totalFilled += filled;
final current = dailyStats[date] ?? (0, 0);
dailyStats[date] = (current.$1 + needed, current.$2 + filled);
}
final List<CoverageDay> dailyCoverage = dailyStats.entries.map((e) {
final needed = e.value.$1;
final filled = e.value.$2;
return CoverageDay(
date: e.key,
needed: needed,
filled: filled,
percentage: needed == 0 ? 100.0 : (filled / needed) * 100.0,
);
}).toList()..sort((a, b) => a.date.compareTo(b.date));
return CoverageReport(
overallCoverage: totalNeeded == 0 ? 100.0 : (totalFilled / totalNeeded) * 100.0,
totalNeeded: totalNeeded,
totalFilled: totalFilled,
dailyCoverage: dailyCoverage,
); );
});
}
@override @override
Future<ForecastReport> getForecastReport({ Future<ForecastReport> getForecastReport({
String? businessId, String? businessId,
required DateTime startDate, required DateTime startDate,
required DateTime endDate, required DateTime endDate,
}) async { }) => _connectorRepository.getForecastReport(
return await _service.run(() async { businessId: businessId,
final String id = businessId ?? await _service.getBusinessId(); startDate: startDate,
final response = await _service.connector endDate: endDate,
.listShiftsForForecastByBusiness(
businessId: id,
startDate: _service.toTimestamp(startDate),
endDate: _service.toTimestamp(endDate),
)
.execute();
final shifts = response.data.shifts;
double projectedSpend = 0.0;
int projectedWorkers = 0;
final Map<DateTime, (double, int)> dailyStats = {};
for (final shift in shifts) {
final shiftDate = shift.date?.toDateTime() ?? DateTime.now();
final date = DateTime(shiftDate.year, shiftDate.month, shiftDate.day);
final cost = (shift.cost ?? 0.0).toDouble();
final workers = shift.workersNeeded ?? 0;
projectedSpend += cost;
projectedWorkers += workers;
final current = dailyStats[date] ?? (0.0, 0);
dailyStats[date] = (current.$1 + cost, current.$2 + workers);
}
final List<ForecastPoint> chartData = dailyStats.entries.map((e) {
return ForecastPoint(
date: e.key,
projectedCost: e.value.$1,
workersNeeded: e.value.$2,
);
}).toList()..sort((a, b) => a.date.compareTo(b.date));
return ForecastReport(
projectedSpend: projectedSpend,
projectedWorkers: projectedWorkers,
averageLaborCost: projectedWorkers == 0 ? 0.0 : projectedSpend / projectedWorkers,
chartData: chartData,
); );
});
}
@override @override
Future<PerformanceReport> getPerformanceReport({ Future<PerformanceReport> getPerformanceReport({
String? businessId, String? businessId,
required DateTime startDate, required DateTime startDate,
required DateTime endDate, required DateTime endDate,
}) async { }) => _connectorRepository.getPerformanceReport(
return await _service.run(() async { businessId: businessId,
final String id = businessId ?? await _service.getBusinessId(); startDate: startDate,
final response = await _service.connector endDate: endDate,
.listShiftsForPerformanceByBusiness(
businessId: id,
startDate: _service.toTimestamp(startDate),
endDate: _service.toTimestamp(endDate),
)
.execute();
final shifts = response.data.shifts;
int totalNeeded = 0;
int totalFilled = 0;
int completedCount = 0;
double totalFillTimeSeconds = 0.0;
int filledShiftsWithTime = 0;
for (final shift in shifts) {
totalNeeded += shift.workersNeeded ?? 0;
totalFilled += shift.filled ?? 0;
if ((shift.status?.stringValue ?? '') == 'COMPLETED') {
completedCount++;
}
if (shift.filledAt != null && shift.createdAt != null) {
final createdAt = shift.createdAt!.toDateTime();
final filledAt = shift.filledAt!.toDateTime();
totalFillTimeSeconds += filledAt.difference(createdAt).inSeconds;
filledShiftsWithTime++;
}
}
final double fillRate = totalNeeded == 0 ? 100.0 : (totalFilled / totalNeeded) * 100.0;
final double completionRate = shifts.isEmpty ? 100.0 : (completedCount / shifts.length) * 100.0;
final double avgFillTimeHours = filledShiftsWithTime == 0
? 0
: (totalFillTimeSeconds / filledShiftsWithTime) / 3600;
return PerformanceReport(
fillRate: fillRate,
completionRate: completionRate,
onTimeRate: 95.0,
avgFillTimeHours: avgFillTimeHours,
keyPerformanceIndicators: [
PerformanceMetric(label: 'Fill Rate', value: '${fillRate.toStringAsFixed(1)}%', trend: 0.02),
PerformanceMetric(label: 'Completion', value: '${completionRate.toStringAsFixed(1)}%', trend: 0.05),
PerformanceMetric(label: 'Avg Fill Time', value: '${avgFillTimeHours.toStringAsFixed(1)}h', trend: -0.1),
],
); );
});
}
@override @override
Future<NoShowReport> getNoShowReport({ Future<NoShowReport> getNoShowReport({
String? businessId, String? businessId,
required DateTime startDate, required DateTime startDate,
required DateTime endDate, required DateTime endDate,
}) async { }) => _connectorRepository.getNoShowReport(
return await _service.run(() async { businessId: businessId,
final String id = businessId ?? await _service.getBusinessId(); startDate: startDate,
endDate: endDate,
final shiftsResponse = await _service.connector
.listShiftsForNoShowRangeByBusiness(
businessId: id,
startDate: _service.toTimestamp(startDate),
endDate: _service.toTimestamp(endDate),
)
.execute();
final shiftIds = shiftsResponse.data.shifts.map((s) => s.id).toList();
if (shiftIds.isEmpty) {
return const NoShowReport(totalNoShows: 0, noShowRate: 0, flaggedWorkers: []);
}
final appsResponse = await _service.connector
.listApplicationsForNoShowRange(shiftIds: shiftIds)
.execute();
final apps = appsResponse.data.applications;
final noShowApps = apps.where((a) => (a.status.stringValue) == 'NO_SHOW').toList();
final noShowStaffIds = noShowApps.map((a) => a.staffId).toSet().toList();
if (noShowStaffIds.isEmpty) {
return NoShowReport(
totalNoShows: noShowApps.length,
noShowRate: apps.isEmpty ? 0 : (noShowApps.length / apps.length) * 100.0,
flaggedWorkers: [],
);
}
final staffResponse = await _service.connector
.listStaffForNoShowReport(staffIds: noShowStaffIds)
.execute();
final staffList = staffResponse.data.staffs;
final List<NoShowWorker> flaggedWorkers = staffList.map((s) => NoShowWorker(
id: s.id,
fullName: s.fullName ?? '',
noShowCount: s.noShowCount ?? 0,
reliabilityScore: (s.reliabilityScore ?? 0.0).toDouble(),
)).toList();
return NoShowReport(
totalNoShows: noShowApps.length,
noShowRate: apps.isEmpty ? 0 : (noShowApps.length / apps.length) * 100.0,
flaggedWorkers: flaggedWorkers,
); );
});
}
@override @override
Future<ReportsSummary> getReportsSummary({ Future<ReportsSummary> getReportsSummary({
String? businessId, String? businessId,
required DateTime startDate, required DateTime startDate,
required DateTime endDate, required DateTime endDate,
}) async { }) => _connectorRepository.getReportsSummary(
return await _service.run(() async { businessId: businessId,
final String id = businessId ?? await _service.getBusinessId(); startDate: startDate,
endDate: endDate,
// Use forecast query for hours/cost data
final shiftsResponse = await _service.connector
.listShiftsForForecastByBusiness(
businessId: id,
startDate: _service.toTimestamp(startDate),
endDate: _service.toTimestamp(endDate),
)
.execute();
// Use performance query for avgFillTime (has filledAt + createdAt)
final perfResponse = await _service.connector
.listShiftsForPerformanceByBusiness(
businessId: id,
startDate: _service.toTimestamp(startDate),
endDate: _service.toTimestamp(endDate),
)
.execute();
final invoicesResponse = await _service.connector
.listInvoicesForSpendByBusiness(
businessId: id,
startDate: _service.toTimestamp(startDate),
endDate: _service.toTimestamp(endDate),
)
.execute();
final forecastShifts = shiftsResponse.data.shifts;
final perfShifts = perfResponse.data.shifts;
final invoices = invoicesResponse.data.invoices;
// Aggregate hours and fill rate from forecast shifts
double totalHours = 0;
int totalNeeded = 0;
int totalFilled = 0;
for (final shift in forecastShifts) {
totalHours += (shift.hours ?? 0).toDouble();
totalNeeded += shift.workersNeeded ?? 0;
// Forecast query doesn't have 'filled' — use workersNeeded as proxy
// (fill rate will be computed from performance shifts below)
}
// Aggregate fill rate from performance shifts (has 'filled' field)
int perfNeeded = 0;
int perfFilled = 0;
double totalFillTimeSeconds = 0;
int filledShiftsWithTime = 0;
for (final shift in perfShifts) {
perfNeeded += shift.workersNeeded ?? 0;
perfFilled += shift.filled ?? 0;
if (shift.filledAt != null && shift.createdAt != null) {
final createdAt = shift.createdAt!.toDateTime();
final filledAt = shift.filledAt!.toDateTime();
totalFillTimeSeconds += filledAt.difference(createdAt).inSeconds;
filledShiftsWithTime++;
}
}
// Aggregate total spend from invoices
double totalSpend = 0;
for (final inv in invoices) {
totalSpend += (inv.amount ?? 0).toDouble();
}
// Fetch no-show rate using forecast shift IDs
final shiftIds = forecastShifts.map((s) => s.id).toList();
double noShowRate = 0;
if (shiftIds.isNotEmpty) {
final appsResponse = await _service.connector
.listApplicationsForNoShowRange(shiftIds: shiftIds)
.execute();
final apps = appsResponse.data.applications;
final noShowApps = apps.where((a) => (a.status.stringValue) == 'NO_SHOW').toList();
noShowRate = apps.isEmpty ? 0 : (noShowApps.length / apps.length) * 100.0;
}
final double fillRate = perfNeeded == 0 ? 100.0 : (perfFilled / perfNeeded) * 100.0;
return ReportsSummary(
totalHours: totalHours,
otHours: totalHours * 0.05, // ~5% OT approximation until schema supports it
totalSpend: totalSpend,
fillRate: fillRate,
avgFillTimeHours: filledShiftsWithTime == 0
? 0
: (totalFillTimeSeconds / filledShiftsWithTime) / 3600,
noShowRate: noShowRate,
); );
});
}
} }

View File

@@ -1,33 +0,0 @@
import 'package:equatable/equatable.dart';
class ForecastReport extends Equatable {
final double projectedSpend;
final int projectedWorkers;
final double averageLaborCost;
final List<ForecastPoint> chartData;
const ForecastReport({
required this.projectedSpend,
required this.projectedWorkers,
required this.averageLaborCost,
required this.chartData,
});
@override
List<Object?> get props => [projectedSpend, projectedWorkers, averageLaborCost, chartData];
}
class ForecastPoint extends Equatable {
final DateTime date;
final double projectedCost;
final int workersNeeded;
const ForecastPoint({
required this.date,
required this.projectedCost,
required this.workersNeeded,
});
@override
List<Object?> get props => [date, projectedCost, workersNeeded];
}

View File

@@ -1,10 +1,4 @@
import '../entities/daily_ops_report.dart'; import 'package:krow_domain/krow_domain.dart';
import '../entities/spend_report.dart';
import '../entities/coverage_report.dart';
import '../entities/forecast_report.dart';
import '../entities/performance_report.dart';
import '../entities/no_show_report.dart';
import '../entities/reports_summary.dart';
abstract class ReportsRepository { abstract class ReportsRepository {
Future<DailyOpsReport> getDailyOpsReport({ Future<DailyOpsReport> getDailyOpsReport({

View File

@@ -0,0 +1,34 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_domain/src/entities/reports/coverage_report.dart';
import '../../../domain/repositories/reports_repository.dart';
import 'coverage_event.dart';
import 'coverage_state.dart';
class CoverageBloc extends Bloc<CoverageEvent, CoverageState> {
CoverageBloc({required ReportsRepository reportsRepository})
: _reportsRepository = reportsRepository,
super(CoverageInitial()) {
on<LoadCoverageReport>(_onLoadCoverageReport);
}
final ReportsRepository _reportsRepository;
Future<void> _onLoadCoverageReport(
LoadCoverageReport event,
Emitter<CoverageState> emit,
) async {
emit(CoverageLoading());
try {
final CoverageReport report = await _reportsRepository.getCoverageReport(
businessId: event.businessId,
startDate: event.startDate,
endDate: event.endDate,
);
emit(CoverageLoaded(report));
} catch (e) {
emit(CoverageError(e.toString()));
}
}
}

View File

@@ -0,0 +1,25 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:equatable/equatable.dart';
abstract class CoverageEvent extends Equatable {
const CoverageEvent();
@override
List<Object?> get props => <Object?>[];
}
class LoadCoverageReport extends CoverageEvent {
const LoadCoverageReport({
this.businessId,
required this.startDate,
required this.endDate,
});
final String? businessId;
final DateTime startDate;
final DateTime endDate;
@override
List<Object?> get props => <Object?>[businessId, startDate, endDate];
}

View File

@@ -0,0 +1,33 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
abstract class CoverageState extends Equatable {
const CoverageState();
@override
List<Object?> get props => <Object?>[];
}
class CoverageInitial extends CoverageState {}
class CoverageLoading extends CoverageState {}
class CoverageLoaded extends CoverageState {
const CoverageLoaded(this.report);
final CoverageReport report;
@override
List<Object?> get props => <Object?>[report];
}
class CoverageError extends CoverageState {
const CoverageError(this.message);
final String message;
@override
List<Object?> get props => <Object?>[message];
}

View File

@@ -1,16 +1,17 @@
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_domain/src/entities/reports/daily_ops_report.dart';
import '../../../domain/repositories/reports_repository.dart'; import '../../../domain/repositories/reports_repository.dart';
import 'daily_ops_event.dart'; import 'daily_ops_event.dart';
import 'daily_ops_state.dart'; import 'daily_ops_state.dart';
class DailyOpsBloc extends Bloc<DailyOpsEvent, DailyOpsState> { class DailyOpsBloc extends Bloc<DailyOpsEvent, DailyOpsState> {
final ReportsRepository _reportsRepository;
DailyOpsBloc({required ReportsRepository reportsRepository}) DailyOpsBloc({required ReportsRepository reportsRepository})
: _reportsRepository = reportsRepository, : _reportsRepository = reportsRepository,
super(DailyOpsInitial()) { super(DailyOpsInitial()) {
on<LoadDailyOpsReport>(_onLoadDailyOpsReport); on<LoadDailyOpsReport>(_onLoadDailyOpsReport);
} }
final ReportsRepository _reportsRepository;
Future<void> _onLoadDailyOpsReport( Future<void> _onLoadDailyOpsReport(
LoadDailyOpsReport event, LoadDailyOpsReport event,
@@ -18,7 +19,7 @@ class DailyOpsBloc extends Bloc<DailyOpsEvent, DailyOpsState> {
) async { ) async {
emit(DailyOpsLoading()); emit(DailyOpsLoading());
try { try {
final report = await _reportsRepository.getDailyOpsReport( final DailyOpsReport report = await _reportsRepository.getDailyOpsReport(
businessId: event.businessId, businessId: event.businessId,
date: event.date, date: event.date,
); );

View File

@@ -4,18 +4,18 @@ abstract class DailyOpsEvent extends Equatable {
const DailyOpsEvent(); const DailyOpsEvent();
@override @override
List<Object?> get props => []; List<Object?> get props => <Object?>[];
} }
class LoadDailyOpsReport extends DailyOpsEvent { class LoadDailyOpsReport extends DailyOpsEvent {
final String? businessId;
final DateTime date;
const LoadDailyOpsReport({ const LoadDailyOpsReport({
this.businessId, this.businessId,
required this.date, required this.date,
}); });
final String? businessId;
final DateTime date;
@override @override
List<Object?> get props => [businessId, date]; List<Object?> get props => <Object?>[businessId, date];
} }

View File

@@ -1,11 +1,12 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import '../../../domain/entities/daily_ops_report.dart'; import 'package:krow_domain/krow_domain.dart';
abstract class DailyOpsState extends Equatable { abstract class DailyOpsState extends Equatable {
const DailyOpsState(); const DailyOpsState();
@override @override
List<Object?> get props => []; List<Object?> get props => <Object?>[];
} }
class DailyOpsInitial extends DailyOpsState {} class DailyOpsInitial extends DailyOpsState {}
@@ -13,19 +14,20 @@ class DailyOpsInitial extends DailyOpsState {}
class DailyOpsLoading extends DailyOpsState {} class DailyOpsLoading extends DailyOpsState {}
class DailyOpsLoaded extends DailyOpsState { class DailyOpsLoaded extends DailyOpsState {
final DailyOpsReport report;
const DailyOpsLoaded(this.report); const DailyOpsLoaded(this.report);
final DailyOpsReport report;
@override @override
List<Object?> get props => [report]; List<Object?> get props => <Object?>[report];
} }
class DailyOpsError extends DailyOpsState { class DailyOpsError extends DailyOpsState {
final String message;
const DailyOpsError(this.message); const DailyOpsError(this.message);
final String message;
@override @override
List<Object?> get props => [message]; List<Object?> get props => <Object?>[message];
} }

View File

@@ -1,16 +1,17 @@
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_domain/src/entities/reports/forecast_report.dart';
import '../../../domain/repositories/reports_repository.dart'; import '../../../domain/repositories/reports_repository.dart';
import 'forecast_event.dart'; import 'forecast_event.dart';
import 'forecast_state.dart'; import 'forecast_state.dart';
class ForecastBloc extends Bloc<ForecastEvent, ForecastState> { class ForecastBloc extends Bloc<ForecastEvent, ForecastState> {
final ReportsRepository _reportsRepository;
ForecastBloc({required ReportsRepository reportsRepository}) ForecastBloc({required ReportsRepository reportsRepository})
: _reportsRepository = reportsRepository, : _reportsRepository = reportsRepository,
super(ForecastInitial()) { super(ForecastInitial()) {
on<LoadForecastReport>(_onLoadForecastReport); on<LoadForecastReport>(_onLoadForecastReport);
} }
final ReportsRepository _reportsRepository;
Future<void> _onLoadForecastReport( Future<void> _onLoadForecastReport(
LoadForecastReport event, LoadForecastReport event,
@@ -18,7 +19,7 @@ class ForecastBloc extends Bloc<ForecastEvent, ForecastState> {
) async { ) async {
emit(ForecastLoading()); emit(ForecastLoading());
try { try {
final report = await _reportsRepository.getForecastReport( final ForecastReport report = await _reportsRepository.getForecastReport(
businessId: event.businessId, businessId: event.businessId,
startDate: event.startDate, startDate: event.startDate,
endDate: event.endDate, endDate: event.endDate,

View File

@@ -4,20 +4,20 @@ abstract class ForecastEvent extends Equatable {
const ForecastEvent(); const ForecastEvent();
@override @override
List<Object?> get props => []; List<Object?> get props => <Object?>[];
} }
class LoadForecastReport extends ForecastEvent { class LoadForecastReport extends ForecastEvent {
final String? businessId;
final DateTime startDate;
final DateTime endDate;
const LoadForecastReport({ const LoadForecastReport({
this.businessId, this.businessId,
required this.startDate, required this.startDate,
required this.endDate, required this.endDate,
}); });
final String? businessId;
final DateTime startDate;
final DateTime endDate;
@override @override
List<Object?> get props => [businessId, startDate, endDate]; List<Object?> get props => <Object?>[businessId, startDate, endDate];
} }

View File

@@ -1,11 +1,12 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import '../../../domain/entities/forecast_report.dart'; import 'package:krow_domain/krow_domain.dart';
abstract class ForecastState extends Equatable { abstract class ForecastState extends Equatable {
const ForecastState(); const ForecastState();
@override @override
List<Object?> get props => []; List<Object?> get props => <Object?>[];
} }
class ForecastInitial extends ForecastState {} class ForecastInitial extends ForecastState {}
@@ -13,19 +14,20 @@ class ForecastInitial extends ForecastState {}
class ForecastLoading extends ForecastState {} class ForecastLoading extends ForecastState {}
class ForecastLoaded extends ForecastState { class ForecastLoaded extends ForecastState {
final ForecastReport report;
const ForecastLoaded(this.report); const ForecastLoaded(this.report);
final ForecastReport report;
@override @override
List<Object?> get props => [report]; List<Object?> get props => <Object?>[report];
} }
class ForecastError extends ForecastState { class ForecastError extends ForecastState {
final String message;
const ForecastError(this.message); const ForecastError(this.message);
final String message;
@override @override
List<Object?> get props => [message]; List<Object?> get props => <Object?>[message];
} }

View File

@@ -1,16 +1,17 @@
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_domain/src/entities/reports/no_show_report.dart';
import '../../../domain/repositories/reports_repository.dart'; import '../../../domain/repositories/reports_repository.dart';
import 'no_show_event.dart'; import 'no_show_event.dart';
import 'no_show_state.dart'; import 'no_show_state.dart';
class NoShowBloc extends Bloc<NoShowEvent, NoShowState> { class NoShowBloc extends Bloc<NoShowEvent, NoShowState> {
final ReportsRepository _reportsRepository;
NoShowBloc({required ReportsRepository reportsRepository}) NoShowBloc({required ReportsRepository reportsRepository})
: _reportsRepository = reportsRepository, : _reportsRepository = reportsRepository,
super(NoShowInitial()) { super(NoShowInitial()) {
on<LoadNoShowReport>(_onLoadNoShowReport); on<LoadNoShowReport>(_onLoadNoShowReport);
} }
final ReportsRepository _reportsRepository;
Future<void> _onLoadNoShowReport( Future<void> _onLoadNoShowReport(
LoadNoShowReport event, LoadNoShowReport event,
@@ -18,7 +19,7 @@ class NoShowBloc extends Bloc<NoShowEvent, NoShowState> {
) async { ) async {
emit(NoShowLoading()); emit(NoShowLoading());
try { try {
final report = await _reportsRepository.getNoShowReport( final NoShowReport report = await _reportsRepository.getNoShowReport(
businessId: event.businessId, businessId: event.businessId,
startDate: event.startDate, startDate: event.startDate,
endDate: event.endDate, endDate: event.endDate,

Some files were not shown because too many files have changed in this diff Show More