Files
prosappco/lib/blocs/service_bloc/service_bloc.dart
T
Lizandro GuarnizoandClaude Opus 5 8631e6f729 fix: port 7 web features and repair the endless-loading screens
Root cause behind most "stuck loading" reports: the backend changed shape
(schedules became an array, location_preferences a string) while the mobile
parser still hard-cast to Map/int. The TypeError was swallowed by a silent
catch that returned null, and screens only handled the success state, so a
parse failure rendered as a permanent spinner. Same class of bug appeared
across service lists via non-null map lookups and a total absence of
request timeouts.

Ported from prosappweb:
- in-app suggestions (POST /suggestions)
- policies/terms from GET /settings/policies
- configurable appointment length (slot_duration_minutes)
- block/unblock calendar slots (POST /services/block)
- GPS city detection on the profile (Nominatim)
- server-side professional search with haversine distance
- retry cooldown after a rejected professional application

Reliability:
- parse schedules array (day_of_week 0=Mon) and string location_preferences
- read times as wall clock, so 08:00 stays 08:00 across timezones
- carry minutes into hours in TimeOfDay.add; a minute-based step used to
  loop forever and freeze the calendar (covered by test/time_slots_test.dart)
- semver update check instead of string equality, which blocked every build
  that did not exactly match the configured version
- request timeouts across all repositories
- surface HTTP >= 400 instead of reporting failed writes as success
- error states with retry instead of an indefinite shimmer

Includes pre-existing uncommitted work from the UI redesign.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 16:45:42 -05:00

353 lines
11 KiB
Dart

import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:professional_repository/professional_repository.dart';
import 'package:service_repository/service_repository.dart';
import 'package:user_repository/user_repository.dart';
part 'service_event.dart';
part 'service_state.dart';
class ServiceBloc extends Bloc<ServiceEvent, ServiceState> {
final ApiServiceRepository _serviceRepository;
final UserRepository _userRepository;
final ApiProfessionalRepository _professionalRepository;
ServiceBloc({
required ApiServiceRepository serviceRepository,
required UserRepository userRepository,
required ApiProfessionalRepository professionRepository,
}) : _serviceRepository = serviceRepository,
_userRepository = userRepository,
_professionalRepository = professionRepository,
super(CreateServiceInitial()) {
on<CreateService>(_onCreateService);
on<BlockSlot>(_onBlockSlot);
on<LoadService>(_onLoadService);
on<UpdateServiceStatus>(_onUpdateServiceStatus);
on<LoadServicesForUser>(_onLoadServicesForUser);
on<LoadServicesForProfessional>(_onLoadServicesForProfessional);
on<LoadServicesHistoryForUser>(_onLoadServicesHistoryForUser);
on<LoadServicesHistoryForProfessional>(
_onLoadServicesHistoryForProfessional);
on<LoadPendingServicesForProfessional>(
_onLoadPendingServicesForProfessional);
on<UpdateProfessionalScored>(_onUpdateProfessionalScored);
on<UpdateUserScored>(_onUpdateUserScored);
on<CountPendingServicesForProfessional>(
_onCountPendingServicesForProfessional);
}
void _onCreateService(CreateService event, Emitter<ServiceState> emit) async {
try {
emit(CreateServiceLoading());
ServiceEntity service = ServiceEntity(
professionalId: event.professionalId,
professionalScored: false,
userId: event.userId,
userScored: false,
address: event.address,
aditionalAddress: event.aditionalAddress,
latitude: event.latitude,
longitude: event.longitude,
day: event.day,
createdAt: event.createdAt,
description: event.description,
range1Hour1: event.range1Hour1,
range1Hour2: event.range1Hour2,
rate: event.rate,
status: event.status ?? ServiceStatus.pending,
location: event.location,
);
String serviceId = await _serviceRepository.createService(service);
emit(CreateServiceSuccess(serviceId));
} catch (e) {
log(e.toString());
emit(CreateServiceFailure());
}
}
void _onBlockSlot(BlockSlot event, Emitter<ServiceState> emit) async {
try {
emit(CreateServiceLoading());
final serviceId = await _serviceRepository.blockSlot(event.day, event.hour1);
emit(CreateServiceSuccess(serviceId));
} catch (e) {
log(e.toString());
emit(CreateServiceFailure());
}
}
void _onLoadService(LoadService event, Emitter<ServiceState> emit) async {
try {
final serviceStream = _serviceRepository.getService(event.serviceId);
await for (var service in serviceStream) {
emit(ServiceLoaded(service));
}
} catch (e) {
log(e.toString());
emit(CreateServiceFailure());
}
}
void _onLoadServicesHistoryForUser(
LoadServicesHistoryForUser event, Emitter<ServiceState> emit) async {
try {
final servicesStream =
_serviceRepository.getServicesHistoryForUser(event.userId);
await for (var services in servicesStream) {
if (services.isEmpty) {
emit(const ServicesForUserLoaded([]));
return;
}
final professionalIds = services.map((e) => e.professionalId);
final List<MyUser> users =
await _userRepository.getUsersFromIds(professionalIds);
final usersDir = {for (var e in users) e.id: e};
final List<ProfessionalEntity> professionsList =
await _professionalRepository
.getProfessionalsFromIds(professionalIds);
final professionsDir = {for (var e in professionsList) e.id: e};
final servicesInfo = services
.where((e) =>
usersDir.containsKey(e.professionalId) &&
professionsDir.containsKey(e.professionalId))
.map((e) {
return ServiceInfoUI(
service: e,
user: usersDir[e.professionalId]!,
professional: professionsDir[e.professionalId]!);
}).toList();
emit(ServicesForUserLoaded(servicesInfo));
}
} catch (e) {
log(e.toString());
emit(CreateServiceFailure());
}
}
void _onLoadServicesHistoryForProfessional(
LoadServicesHistoryForProfessional event,
Emitter<ServiceState> emit) async {
try {
final servicesStream =
_serviceRepository.getServicesHistoryForProfessional(event.userId);
await for (var services in servicesStream) {
if (services.isEmpty) {
emit(const ServicesForUserLoaded([]));
return;
}
final userIds = services.map((e) => e.userId);
final List<MyUser> users =
await _userRepository.getUsersFromIds(userIds);
final usersDir = {for (var e in users) e.id: e};
final servicesInfo = services
.where((e) => usersDir.containsKey(e.userId))
.map((e) {
return ServiceInfoUI(
service: e,
user: usersDir[e.userId]!,
);
}).toList();
emit(ServicesForUserLoaded(servicesInfo));
}
} catch (e) {
log(e.toString());
emit(CreateServiceFailure());
}
}
void _onLoadServicesForUser(
LoadServicesForUser event, Emitter<ServiceState> emit) async {
try {
final servicesStream =
_serviceRepository.getServicesForUser(event.userId);
await for (var services in servicesStream) {
if (services.isEmpty) {
emit(const ServicesForUserLoaded([]));
return;
}
final professionalIds = services.map((e) => e.professionalId);
final List<MyUser> users =
await _userRepository.getUsersFromIds(professionalIds);
final usersDir = {for (var e in users) e.id: e};
final List<ProfessionalEntity> professionsList =
await _professionalRepository
.getProfessionalsFromIds(professionalIds);
final professionsDir = {for (var e in professionsList) e.id: e};
final servicesInfo = services
.where((e) =>
usersDir.containsKey(e.professionalId) &&
professionsDir.containsKey(e.professionalId))
.map((e) {
return ServiceInfoUI(
service: e,
user: usersDir[e.professionalId]!,
professional: professionsDir[e.professionalId]!);
}).toList();
emit(ServicesForUserLoaded(servicesInfo));
}
} catch (e) {
log(e.toString());
emit(CreateServiceFailure());
}
}
void _onLoadServicesForProfessional(
LoadServicesForProfessional event, Emitter<ServiceState> emit) async {
try {
final servicesStream =
_serviceRepository.getServicesForProfessional(event.userId);
await for (var services in servicesStream) {
if (services.isEmpty) {
emit(const ServicesForUserLoaded([]));
return;
}
final userIds = services.map((e) => e.userId);
final List<MyUser> users =
await _userRepository.getUsersFromIds(userIds);
final usersDir = {for (var e in users) e.id: e};
final servicesInfo = services
.where((e) => usersDir.containsKey(e.userId))
.map((e) {
return ServiceInfoUI(
service: e,
user: usersDir[e.userId]!,
);
}).toList();
emit(ServicesForUserLoaded(servicesInfo));
}
} catch (e) {
log(e.toString());
emit(CreateServiceFailure());
}
}
void _onLoadPendingServicesForProfessional(
LoadPendingServicesForProfessional event,
Emitter<ServiceState> emit) async {
try {
final servicesStream =
_serviceRepository.getPendingServicesForProfessional(event.userId);
await for (var services in servicesStream) {
if (services.isEmpty) {
emit(const ServicesForUserLoaded([]));
return;
}
final userIds = services.map((e) => e.userId);
final List<MyUser> users = await _userRepository.getUsersFromIds(userIds);
final usersDir = {for (var e in users) e.id: e};
final servicesInfo = services
.where((e) => usersDir.containsKey(e.userId))
.map((e) {
return ServiceInfoUI(
service: e,
user: usersDir[e.userId]!,
);
}).toList();
emit(ServicesForUserLoaded(servicesInfo));
}
} catch (e) {
log(e.toString());
emit(CreateServiceFailure());
}
}
void _onUpdateServiceStatus(
UpdateServiceStatus event, Emitter<ServiceState> emit) async {
try {
emit(CreateServiceLoading());
await _serviceRepository.updateServiceStatus(
event.serviceId, event.newStatus);
emit(ServiceStatusUpdated(event.newStatus));
} catch (e) {
log(e.toString());
emit(CreateServiceFailure());
}
}
void _onUpdateProfessionalScored(
UpdateProfessionalScored event, Emitter<ServiceState> emit) async {
try {
emit(CreateServiceLoading());
await _serviceRepository.setProfessionalScored(event.serviceId);
emit(ScoredUpdated());
} catch (e) {
log(e.toString());
emit(CreateServiceFailure());
}
}
void _onUpdateUserScored(
UpdateUserScored event, Emitter<ServiceState> emit) async {
try {
emit(CreateServiceLoading());
await _serviceRepository.setUserScored(event.serviceId);
emit(ScoredUpdated());
} catch (e) {
log(e.toString());
emit(CreateServiceFailure());
}
}
void _onCountPendingServicesForProfessional(
CountPendingServicesForProfessional event,
Emitter<ServiceState> emit) async {
try {
final count = await _serviceRepository
.countPendingServicesForProfessional(event.professionalId);
emit(PendingServicesCountedLoaded(count));
} catch (e) {
log(e.toString());
emit(CreateServiceFailure());
}
}
}
class ServiceInfoUI {
final ServiceEntity service;
final MyUser user;
final ProfessionalEntity? professional;
ServiceInfoUI({
required this.service,
required this.user,
this.professional,
});
}