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>
658 lines
22 KiB
Dart
658 lines
22 KiB
Dart
import 'dart:async';
|
|
import 'package:community_material_icon/community_material_icon.dart';
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:injector/injector.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:professional_repository/professional_repository.dart';
|
|
import 'package:prosappco/blocs/service_bloc/service_bloc.dart';
|
|
import 'package:prosappco/components/general_reputation.dart';
|
|
import 'package:prosappco/components/general_secondary_button.dart';
|
|
import 'package:prosappco/screens/chat/chat_screen.dart';
|
|
import 'package:prosappco/screens/score/score_screen.dart';
|
|
import 'package:score_repository/score_repository.dart';
|
|
import 'package:service_repository/service_repository.dart';
|
|
import 'package:setting_repository/setting_repository.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
import 'package:user_repository/user_repository.dart';
|
|
|
|
const _kPrimary = Color(0xFF1565C0);
|
|
|
|
extension _Th on BuildContext {
|
|
ThemeData get _t => Theme.of(this);
|
|
Color get bg => _t.scaffoldBackgroundColor;
|
|
Color get card => _t.cardColor;
|
|
Color get onSurface => _t.colorScheme.onSurface;
|
|
Color get muted => _t.colorScheme.onSurface.withOpacity(0.55);
|
|
Color get subtle => _t.colorScheme.onSurface.withOpacity(0.35);
|
|
bool get isDark => _t.brightness == Brightness.dark;
|
|
Color get shadowSm =>
|
|
isDark ? Colors.transparent : Colors.black.withOpacity(0.05);
|
|
}
|
|
|
|
class ProfessionalServiceScreen extends StatefulWidget {
|
|
final String serviceId;
|
|
const ProfessionalServiceScreen({super.key, required this.serviceId});
|
|
|
|
@override
|
|
State<ProfessionalServiceScreen> createState() =>
|
|
_ProfessionalServiceScreenState();
|
|
}
|
|
|
|
class _ProfessionalServiceScreenState
|
|
extends State<ProfessionalServiceScreen> {
|
|
final settingRepository = Injector.appInstance.get<SettingRepository>();
|
|
SettingEntity? settings;
|
|
Timer? _timer;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadSettings();
|
|
_startTimer();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_timer?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
void _startTimer() {
|
|
_timer = Timer.periodic(const Duration(minutes: 5), (_) {
|
|
setState(() {});
|
|
});
|
|
}
|
|
|
|
void _loadSettings() {
|
|
settingRepository.getSettings().then((v) => setState(() => settings = v));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocProvider<ServiceBloc>(
|
|
create: (_) => Injector.appInstance.get<ServiceBloc>(),
|
|
child: Scaffold(
|
|
backgroundColor: context.bg,
|
|
appBar: AppBar(
|
|
title: const Text('Detalle del servicio'),
|
|
backgroundColor: _kPrimary,
|
|
foregroundColor: Colors.white,
|
|
elevation: 0,
|
|
),
|
|
body: BlocBuilder<ServiceBloc, ServiceState>(
|
|
builder: (context, state) {
|
|
if (state is ServiceLoaded) {
|
|
final service = state.service;
|
|
return FutureBuilder<List<dynamic>>(
|
|
future: _getUserInfo(service),
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
if (snapshot.hasError) {
|
|
return Center(
|
|
child: Text('Error: ${snapshot.error}'));
|
|
}
|
|
final userInfo = snapshot.data![0] as MyUser;
|
|
return Column(
|
|
children: [
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
children: [
|
|
_headerCard(context, service, userInfo),
|
|
_locationCard(context, service),
|
|
if (service.description.isNotEmpty)
|
|
_descriptionCard(context, service),
|
|
_actionButtons(context, service, userInfo),
|
|
_statusBanner(context, service),
|
|
const SizedBox(height: 24),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const Divider(height: 1, thickness: 0.5),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 12, horizontal: 16),
|
|
child: _bottomButton(context, service),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
} else {
|
|
BlocProvider.of<ServiceBloc>(context)
|
|
.add(LoadService(widget.serviceId));
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _headerCard(
|
|
BuildContext context, ServiceEntity service, MyUser user) {
|
|
return Container(
|
|
width: double.infinity,
|
|
margin: const EdgeInsets.all(16),
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: context.card,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: context.shadowSm,
|
|
blurRadius: 12,
|
|
offset: const Offset(0, 2))
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
// Avatar
|
|
Container(
|
|
width: 90,
|
|
height: 90,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
border:
|
|
Border.all(color: _kPrimary.withOpacity(0.25), width: 3),
|
|
color: Colors.grey.shade200,
|
|
image: user.picture != null && user.picture!.isNotEmpty
|
|
? DecorationImage(
|
|
image: NetworkImage(user.picture!),
|
|
fit: BoxFit.cover)
|
|
: null,
|
|
),
|
|
child: user.picture == null || user.picture!.isEmpty
|
|
? Icon(CupertinoIcons.person,
|
|
color: Colors.grey.shade500, size: 40)
|
|
: null,
|
|
),
|
|
// Star rating badge
|
|
GeneralReputation(
|
|
userId: service.userId,
|
|
builder: (_, ReputationEntity reputation) {
|
|
return Positioned(
|
|
top: 0,
|
|
right: MediaQuery.of(context).size.width * 0.18,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: context.card,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: context.shadowSm,
|
|
blurRadius: 6,
|
|
spreadRadius: 1)
|
|
],
|
|
),
|
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
|
const Icon(Icons.star, color: Colors.amber, size: 14),
|
|
const SizedBox(width: 3),
|
|
Text(reputation.average.toStringAsFixed(1),
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
color: context.onSurface)),
|
|
]),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
Text(user.name ?? '',
|
|
style: TextStyle(
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w700,
|
|
color: context.onSurface)),
|
|
const SizedBox(height: 4),
|
|
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
|
Icon(Icons.calendar_today_outlined, size: 13, color: context.muted),
|
|
const SizedBox(width: 5),
|
|
Text(
|
|
'${DateFormat('dd MMMM', 'es').format(DateTime.parse(service.day))} · ${ScheduleEntity.getFormatTime(service.range1Hour1) ?? ''}',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: context.muted,
|
|
fontWeight: FontWeight.w500),
|
|
),
|
|
]),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _locationCard(BuildContext context, ServiceEntity service) {
|
|
final isDelivery =
|
|
service.location == ServiceLocationPreferences.delivery;
|
|
if (!isDelivery &&
|
|
service.status != ServiceStatus.acepted &&
|
|
service.status != ServiceStatus.pending &&
|
|
service.status != ServiceStatus.active) {
|
|
return const SizedBox();
|
|
}
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: _kPrimary.withOpacity(0.06),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: _kPrimary.withOpacity(0.15)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
isDelivery ? Icons.home_outlined : Icons.business_outlined,
|
|
color: _kPrimary,
|
|
size: 20),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
isDelivery
|
|
? 'Servicio a domicilio'
|
|
: 'Servicio en consultorio',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: _kPrimary)),
|
|
if (isDelivery &&
|
|
(service.status == ServiceStatus.acepted ||
|
|
service.status == ServiceStatus.pending ||
|
|
service.status == ServiceStatus.active) &&
|
|
service.address.isNotEmpty)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 2),
|
|
child: Text(service.address,
|
|
style: TextStyle(
|
|
fontSize: 12, color: context.muted)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (isDelivery &&
|
|
(service.status == ServiceStatus.acepted ||
|
|
service.status == ServiceStatus.pending ||
|
|
service.status == ServiceStatus.active))
|
|
IconButton(
|
|
icon: Icon(Icons.near_me, color: _kPrimary),
|
|
onPressed: () async {
|
|
final url = Uri.parse(
|
|
'https://www.google.com/maps/search/?api=1&query=${service.latitude},${service.longitude}');
|
|
if (!await launchUrl(url)) throw Exception('No se pudo abrir mapa');
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _descriptionCard(BuildContext context, ServiceEntity service) {
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: context.card,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(color: context.shadowSm, blurRadius: 8)
|
|
],
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(Icons.format_quote_rounded,
|
|
color: context.subtle, size: 20),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
service.description.trim(),
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: context.muted,
|
|
fontStyle: FontStyle.italic,
|
|
height: 1.5),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _actionButtons(
|
|
BuildContext context, ServiceEntity service, MyUser user) {
|
|
final serviceDate = DateTime.parse(service.day);
|
|
final now = DateTime.now();
|
|
final serviceDateTime = DateTime(serviceDate.year, serviceDate.month,
|
|
serviceDate.day, service.range1Hour2.hour, service.range1Hour2.minute);
|
|
|
|
if ((service.status == ServiceStatus.acepted ||
|
|
service.status == ServiceStatus.active) &&
|
|
!now.isAfter(serviceDateTime)) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
if (user.phone != null)
|
|
_ActionBtn(
|
|
icon: Icons.phone_outlined,
|
|
label: 'Llamar',
|
|
onTap: () => launchUrl(Uri.parse('tel:${user.phone}')),
|
|
),
|
|
if (user.phone != null) const SizedBox(width: 10),
|
|
_ActionBtn(
|
|
icon: Icons.chat_bubble_outline_rounded,
|
|
label: 'Chat',
|
|
onTap: () {
|
|
if (service.id != null) {
|
|
Navigator.push(
|
|
context,
|
|
CupertinoPageRoute(
|
|
builder: (_) => ChatScreen(service: service)));
|
|
}
|
|
},
|
|
),
|
|
if (user.phone != null) const SizedBox(width: 10),
|
|
if (user.phone != null)
|
|
_ActionBtn(
|
|
icon: CommunityMaterialIcons.whatsapp,
|
|
label: 'WhatsApp',
|
|
onTap: () async {
|
|
final url =
|
|
'https://wa.me/${user.phone}?text=${Uri.encodeFull('Hola! me contactaste por Prossapp')}';
|
|
if (!await launchUrl(Uri.parse(url))) {
|
|
throw Exception('No se pudo abrir WhatsApp');
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
return const SizedBox();
|
|
}
|
|
|
|
Widget _statusBanner(BuildContext context, ServiceEntity service) {
|
|
final serviceDate = DateTime.parse(service.day);
|
|
final now = DateTime.now();
|
|
final serviceDateTime = DateTime(serviceDate.year, serviceDate.month,
|
|
serviceDate.day, service.range1Hour2.hour, service.range1Hour2.minute);
|
|
|
|
final bool expired = now.isAfter(serviceDateTime);
|
|
|
|
if (expired) {
|
|
return _StatusCard(
|
|
color: const Color(0xFFDC2626),
|
|
icon: Icons.timer_off_outlined,
|
|
title: 'Caducado',
|
|
subtitle: 'El tiempo del servicio ha expirado',
|
|
);
|
|
}
|
|
|
|
switch (service.status) {
|
|
case ServiceStatus.cancelled:
|
|
return _StatusCard(
|
|
color: const Color(0xFF9CA3AF),
|
|
icon: Icons.remove_circle_outline,
|
|
title: 'Cancelado',
|
|
subtitle: 'El servicio fue cancelado',
|
|
);
|
|
case ServiceStatus.denied:
|
|
return _StatusCard(
|
|
color: const Color(0xFFDC2626),
|
|
icon: Icons.cancel_outlined,
|
|
title: 'Rechazado',
|
|
subtitle: 'El servicio fue rechazado',
|
|
);
|
|
case ServiceStatus.completed:
|
|
if (service.userScored == false) {
|
|
return _StatusCard(
|
|
color: const Color(0xFF16A34A),
|
|
icon: Icons.star_outline_rounded,
|
|
title: 'Completado',
|
|
subtitle: 'Califica el servicio',
|
|
action: _ScoreButton(service: service),
|
|
);
|
|
}
|
|
return _StatusCard(
|
|
color: const Color(0xFF16A34A),
|
|
icon: Icons.task_alt_outlined,
|
|
title: 'Completado',
|
|
subtitle: '¡Servicio finalizado exitosamente!',
|
|
);
|
|
default:
|
|
return const SizedBox();
|
|
}
|
|
}
|
|
|
|
Widget _bottomButton(BuildContext context, ServiceEntity service) {
|
|
final serviceDate = DateTime.parse(service.day);
|
|
final now = DateTime.now();
|
|
final serviceDateTime = DateTime(serviceDate.year, serviceDate.month,
|
|
serviceDate.day, service.range1Hour2.hour, service.range1Hour2.minute);
|
|
|
|
if (now.isAfter(serviceDateTime)) {
|
|
return GeneralSecondaryButton(
|
|
label: 'Volver', onPressed: () => Navigator.pop(context));
|
|
}
|
|
|
|
if (service.status == ServiceStatus.pending) {
|
|
return Column(
|
|
children: [
|
|
GeneralSecondaryButton(
|
|
label: 'Rechazar servicio',
|
|
color: Theme.of(context).colorScheme.error,
|
|
onPressed: () {
|
|
final s = context.read<ServiceBloc>().state;
|
|
if (s is ServiceLoaded) {
|
|
context.read<ServiceBloc>().add(
|
|
UpdateServiceStatus(widget.serviceId, ServiceStatus.denied));
|
|
}
|
|
},
|
|
),
|
|
const SizedBox(height: 10),
|
|
GeneralSecondaryButton(
|
|
label: 'Aceptar servicio',
|
|
onPressed: () {
|
|
final s = context.read<ServiceBloc>().state;
|
|
if (s is ServiceLoaded) {
|
|
context.read<ServiceBloc>().add(UpdateServiceStatus(
|
|
widget.serviceId, ServiceStatus.acepted));
|
|
}
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
if (service.status == ServiceStatus.acepted) {
|
|
if (serviceDateTime.difference(now).inHours > 1) {
|
|
return GeneralSecondaryButton(
|
|
label: 'Cancelar servicio',
|
|
color: Theme.of(context).colorScheme.error,
|
|
onPressed: () {
|
|
final s = context.read<ServiceBloc>().state;
|
|
if (s is ServiceLoaded) {
|
|
context.read<ServiceBloc>().add(UpdateServiceStatus(
|
|
widget.serviceId, ServiceStatus.cancelled));
|
|
}
|
|
},
|
|
);
|
|
} else {
|
|
return GeneralSecondaryButton(
|
|
label: 'Iniciar servicio',
|
|
onPressed: () {
|
|
final s = context.read<ServiceBloc>().state;
|
|
if (s is ServiceLoaded) {
|
|
context.read<ServiceBloc>().add(
|
|
UpdateServiceStatus(widget.serviceId, ServiceStatus.active));
|
|
}
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
if (service.status == ServiceStatus.active) {
|
|
return GeneralSecondaryButton(
|
|
label: 'Terminar servicio',
|
|
onPressed: () {
|
|
final s = context.read<ServiceBloc>().state;
|
|
if (s is ServiceLoaded) {
|
|
context.read<ServiceBloc>().add(UpdateServiceStatus(
|
|
widget.serviceId, ServiceStatus.completed));
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
return GeneralSecondaryButton(
|
|
label: 'Volver', onPressed: () => Navigator.pop(context));
|
|
}
|
|
|
|
String formatCurrency(int number) {
|
|
final formatter =
|
|
NumberFormat.currency(locale: 'es_CO', decimalDigits: 0, symbol: '');
|
|
return '\$${formatter.format(number)}';
|
|
}
|
|
|
|
Future<List<dynamic>> _getUserInfo(ServiceEntity service) async {
|
|
final userRepo = Injector.appInstance.get<UserRepository>();
|
|
final userInfo = await userRepo.getMyUser(service.userId);
|
|
return [userInfo];
|
|
}
|
|
}
|
|
|
|
// ── shared widgets ──────────────────────────────────────────────────────────
|
|
|
|
class _ActionBtn extends StatelessWidget {
|
|
final IconData icon;
|
|
final String label;
|
|
final VoidCallback onTap;
|
|
const _ActionBtn(
|
|
{required this.icon, required this.label, required this.onTap});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return OutlinedButton.icon(
|
|
onPressed: onTap,
|
|
icon: Icon(icon, size: 16),
|
|
label: Text(label, style: const TextStyle(fontSize: 12)),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: _kPrimary,
|
|
side: BorderSide(color: _kPrimary.withOpacity(0.5)),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _StatusCard extends StatelessWidget {
|
|
final Color color;
|
|
final IconData icon;
|
|
final String title;
|
|
final String subtitle;
|
|
final Widget? action;
|
|
const _StatusCard({
|
|
required this.color,
|
|
required this.icon,
|
|
required this.title,
|
|
required this.subtitle,
|
|
this.action,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.08),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: color.withOpacity(0.3)),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 44,
|
|
height: 44,
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.15),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(icon, color: color, size: 24),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(title,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w700,
|
|
color: color)),
|
|
Text(subtitle,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: color.withOpacity(0.75))),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (action != null) ...[
|
|
const SizedBox(height: 12),
|
|
action!,
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ScoreButton extends StatelessWidget {
|
|
final ServiceEntity service;
|
|
const _ScoreButton({required this.service});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton.icon(
|
|
onPressed: () => Navigator.push(
|
|
context,
|
|
CupertinoPageRoute(builder: (_) => ScoreScreen(service: service)),
|
|
),
|
|
icon: const Icon(Icons.star_outline_rounded, size: 18),
|
|
label: const Text('Calificar servicio'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF16A34A),
|
|
foregroundColor: Colors.white,
|
|
elevation: 0,
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
shape:
|
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|