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:
2
.github/workflows/mobile-ci.yml
vendored
2
.github/workflows/mobile-ci.yml
vendored
@@ -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
2
.gitignore
vendored
@@ -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/
|
||||||
|
|||||||
@@ -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.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,3 +28,27 @@ export 'src/connectors/staff/domain/usecases/get_tax_forms_completion_usecase.da
|
|||||||
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';
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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});
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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});
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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});
|
||||||
|
}
|
||||||
@@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
|
||||||
return session!.staff!.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Check Cache
|
if (staffId == null || staffId.isEmpty) {
|
||||||
if (_cachedStaffId != null) return _cachedStaffId!;
|
// 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) {
|
staffId = dc.StaffSessionStore.instance.session?.ownerId;
|
||||||
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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._();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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']!,
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/// Defines the period for billing calculations.
|
||||||
|
enum BillingPeriod {
|
||||||
|
/// Weekly billing period.
|
||||||
|
week,
|
||||||
|
|
||||||
|
/// Monthly billing period.
|
||||||
|
month,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
enum BillingPeriod {
|
|
||||||
week,
|
|
||||||
month,
|
|
||||||
}
|
|
||||||
@@ -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.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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>>[
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>[
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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];
|
|
||||||
}
|
|
||||||
@@ -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({
|
||||||
|
|||||||
@@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user