Co-authored-by: MENDOZA BALLARDO GAEL RICARDO <gael-meb123@users.noreply.github.com>
Co-authored-by: Azareth-Tr <Azareth-Tr@users.noreply.github.com> Co-authored-by: eddgranados12 <eddgranados12@users.noreply.github.com> vistas de mockup actualizaco
This commit is contained in:
144
views_v2/app.dart
Normal file
144
views_v2/app.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
// lib/app.dart
|
||||
// Root de la app: go_router + bottom navigation de 4 tabs (P3).
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'features/eta/eta_screen.dart';
|
||||
import 'features/notifications/notifications_screen.dart';
|
||||
import 'features/feedback/feedback_screen.dart';
|
||||
import 'features/quiz/quiz_screen.dart';
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Router
|
||||
// ──────────────────────────────────────────
|
||||
final _router = GoRouter(
|
||||
initialLocation: '/eta',
|
||||
routes: [
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => _ScaffoldWithNav(child: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/eta',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(
|
||||
child: EtaScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/notifications',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(
|
||||
child: NotificationsScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/feedback',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(
|
||||
child: FeedbackScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/quiz',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(
|
||||
child: QuizScreen(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// App widget
|
||||
// ──────────────────────────────────────────
|
||||
class RecolectaApp extends StatelessWidget {
|
||||
const RecolectaApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
title: 'Recolecta',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: _buildTheme(Brightness.light),
|
||||
darkTheme: _buildTheme(Brightness.dark),
|
||||
themeMode: ThemeMode.system,
|
||||
routerConfig: _router,
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData _buildTheme(Brightness brightness) {
|
||||
final isDark = brightness == Brightness.dark;
|
||||
return ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF1D9E75), // teal-400
|
||||
brightness: brightness,
|
||||
),
|
||||
useMaterial3: true,
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor:
|
||||
isDark ? const Color(0xFF1A1A1A) : Colors.white,
|
||||
foregroundColor: isDark ? Colors.white : Colors.black87,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 1,
|
||||
titleTextStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Bottom navigation shell
|
||||
// ──────────────────────────────────────────
|
||||
class _ScaffoldWithNav extends StatelessWidget {
|
||||
final Widget child;
|
||||
const _ScaffoldWithNav({required this.child});
|
||||
|
||||
static const _tabs = [
|
||||
_TabItem(path: '/eta', icon: Icons.schedule_rounded, label: 'ETA'),
|
||||
_TabItem(
|
||||
path: '/notifications',
|
||||
icon: Icons.notifications_outlined,
|
||||
label: 'Avisos'),
|
||||
_TabItem(
|
||||
path: '/feedback',
|
||||
icon: Icons.feedback_outlined,
|
||||
label: 'Buzón'),
|
||||
_TabItem(
|
||||
path: '/quiz', icon: Icons.quiz_outlined, label: 'Quiz'),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final location = GoRouterState.of(context).uri.path;
|
||||
final currentIndex =
|
||||
_tabs.indexWhere((t) => location.startsWith(t.path));
|
||||
|
||||
return Scaffold(
|
||||
body: child,
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: currentIndex < 0 ? 0 : currentIndex,
|
||||
onDestinationSelected: (i) => context.go(_tabs[i].path),
|
||||
destinations: _tabs
|
||||
.map(
|
||||
(t) => NavigationDestination(
|
||||
icon: Icon(t.icon),
|
||||
label: t.label,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
height: 64,
|
||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TabItem {
|
||||
final String path;
|
||||
final IconData icon;
|
||||
final String label;
|
||||
const _TabItem(
|
||||
{required this.path, required this.icon, required this.label});
|
||||
}
|
||||
53
views_v2/dio_client.dart
Normal file
53
views_v2/dio_client.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
// lib/core/dio_client.dart
|
||||
// Cliente HTTP configurado con base URL, interceptor de JWT y timeouts.
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
// La base URL viene de las variables de entorno en flutter_dotenv o dart-define.
|
||||
// Para el emulador Android: http://10.0.2.2:8000
|
||||
// Para producción: https://tu-backend.run.app
|
||||
const String _kBaseUrl = String.fromEnvironment(
|
||||
'API_BASE_URL',
|
||||
defaultValue: 'http://10.0.2.2:8000',
|
||||
);
|
||||
|
||||
// Token JWT — se rellena desde el provider de auth tras login
|
||||
String? _jwtToken;
|
||||
|
||||
void setJwtToken(String token) => _jwtToken = token;
|
||||
void clearJwtToken() => _jwtToken = null;
|
||||
|
||||
Dio _buildDio() {
|
||||
final dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: _kBaseUrl,
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 15),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
),
|
||||
);
|
||||
|
||||
// Interceptor: adjunta JWT en cada petición
|
||||
dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
if (_jwtToken != null) {
|
||||
options.headers['Authorization'] = 'Bearer $_jwtToken';
|
||||
}
|
||||
handler.next(options);
|
||||
},
|
||||
onError: (error, handler) {
|
||||
// 401 → limpiar token (el router de go_router redirige al login)
|
||||
if (error.response?.statusCode == 401) {
|
||||
clearJwtToken();
|
||||
}
|
||||
handler.next(error);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return dio;
|
||||
}
|
||||
|
||||
final dioProvider = Provider<Dio>((ref) => _buildDio());
|
||||
39
views_v2/feedback_model.dart
Normal file
39
views_v2/feedback_model.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
// lib/features/feedback/feedback_model.dart
|
||||
// La queja solo registra target_unit_id (número de unidad), NUNCA el chofer.
|
||||
|
||||
enum FeedbackType {
|
||||
noPaso('no_paso', 'No pasó el camión'),
|
||||
llegoTarde('llego_tarde', 'Llegó tarde'),
|
||||
comportamiento('comportamiento', 'Comportamiento'),
|
||||
otro('otro', 'Otro');
|
||||
|
||||
final String value;
|
||||
final String label;
|
||||
const FeedbackType(this.value, this.label);
|
||||
}
|
||||
|
||||
class FeedbackRequest {
|
||||
final String addressId;
|
||||
final FeedbackType type;
|
||||
final int rating; // 1-5
|
||||
final String? message;
|
||||
/// Solo el número de unidad — nunca el ID del chofer.
|
||||
final String targetUnitId;
|
||||
|
||||
const FeedbackRequest({
|
||||
required this.addressId,
|
||||
required this.type,
|
||||
required this.rating,
|
||||
required this.targetUnitId,
|
||||
this.message,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'address_id': addressId,
|
||||
'type': type.value,
|
||||
'rating': rating,
|
||||
'target_unit_id': targetUnitId, // ej. "101"
|
||||
if (message != null && message!.isNotEmpty) 'message': message,
|
||||
// ⚠️ NUNCA se manda: driver_id, driver_name, chofer_*
|
||||
};
|
||||
}
|
||||
104
views_v2/feedback_provider.dart
Normal file
104
views_v2/feedback_provider.dart
Normal file
@@ -0,0 +1,104 @@
|
||||
// lib/features/feedback/feedback_provider.dart
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/dio_client.dart';
|
||||
import 'feedback_model.dart';
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Service
|
||||
// ──────────────────────────────────────────
|
||||
class FeedbackService {
|
||||
final Dio _dio;
|
||||
FeedbackService(this._dio);
|
||||
|
||||
Future<void> submit(FeedbackRequest req) async {
|
||||
await _dio.post<void>('/feedback', data: req.toJson());
|
||||
}
|
||||
}
|
||||
|
||||
final feedbackServiceProvider = Provider<FeedbackService>(
|
||||
(ref) => FeedbackService(ref.read(dioProvider)),
|
||||
);
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Estado del formulario
|
||||
// ──────────────────────────────────────────
|
||||
enum FeedbackFormStatus { idle, loading, success, error }
|
||||
|
||||
class FeedbackFormState {
|
||||
final FeedbackType selectedType;
|
||||
final int rating;
|
||||
final String message;
|
||||
final FeedbackFormStatus status;
|
||||
final String? errorMessage;
|
||||
|
||||
const FeedbackFormState({
|
||||
this.selectedType = FeedbackType.noPaso,
|
||||
this.rating = 3,
|
||||
this.message = '',
|
||||
this.status = FeedbackFormStatus.idle,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
FeedbackFormState copyWith({
|
||||
FeedbackType? selectedType,
|
||||
int? rating,
|
||||
String? message,
|
||||
FeedbackFormStatus? status,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return FeedbackFormState(
|
||||
selectedType: selectedType ?? this.selectedType,
|
||||
rating: rating ?? this.rating,
|
||||
message: message ?? this.message,
|
||||
status: status ?? this.status,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Notifier
|
||||
// ──────────────────────────────────────────
|
||||
class FeedbackNotifier extends Notifier<FeedbackFormState> {
|
||||
@override
|
||||
FeedbackFormState build() => const FeedbackFormState();
|
||||
|
||||
void setType(FeedbackType type) =>
|
||||
state = state.copyWith(selectedType: type);
|
||||
|
||||
void setRating(int r) => state = state.copyWith(rating: r);
|
||||
|
||||
void setMessage(String m) => state = state.copyWith(message: m);
|
||||
|
||||
void reset() => state = const FeedbackFormState();
|
||||
|
||||
Future<void> submit({
|
||||
required String addressId,
|
||||
required String unitId,
|
||||
}) async {
|
||||
state = state.copyWith(status: FeedbackFormStatus.loading);
|
||||
try {
|
||||
final req = FeedbackRequest(
|
||||
addressId: addressId,
|
||||
type: state.selectedType,
|
||||
rating: state.rating,
|
||||
message: state.message,
|
||||
targetUnitId: unitId,
|
||||
);
|
||||
await ref.read(feedbackServiceProvider).submit(req);
|
||||
state = state.copyWith(status: FeedbackFormStatus.success);
|
||||
} on DioException catch (e) {
|
||||
state = state.copyWith(
|
||||
status: FeedbackFormStatus.error,
|
||||
errorMessage: e.message ?? 'Error al enviar',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final feedbackProvider =
|
||||
NotifierProvider<FeedbackNotifier, FeedbackFormState>(
|
||||
FeedbackNotifier.new,
|
||||
);
|
||||
354
views_v2/feedback_screen.dart
Normal file
354
views_v2/feedback_screen.dart
Normal file
@@ -0,0 +1,354 @@
|
||||
// lib/features/feedback/feedback_screen.dart
|
||||
// Buzón de retroalimentación. Expone "Unidad 101", nunca el nombre del chofer.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'feedback_model.dart';
|
||||
import 'feedback_provider.dart';
|
||||
import '../eta/eta_provider.dart'; // activeAddressIdProvider
|
||||
|
||||
// El unitId activo se obtiene del ETA response o de la sesión del chofer.
|
||||
// Por simplidad se provee aquí; en producción viene del provider de sesión.
|
||||
final activeUnitIdProvider = StateProvider<String>((ref) => '101');
|
||||
|
||||
class FeedbackScreen extends ConsumerStatefulWidget {
|
||||
const FeedbackScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<FeedbackScreen> createState() => _FeedbackScreenState();
|
||||
}
|
||||
|
||||
class _FeedbackScreenState extends ConsumerState<FeedbackScreen> {
|
||||
final _messageController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_messageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final formState = ref.watch(feedbackProvider);
|
||||
|
||||
if (formState.status == FeedbackFormStatus.success) {
|
||||
return _SuccessView(onReset: () {
|
||||
ref.read(feedbackProvider.notifier).reset();
|
||||
_messageController.clear();
|
||||
});
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Buzón de retroalimentación')),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// Tipo de reporte
|
||||
const _SectionLabel('Tipo de reporte'),
|
||||
const SizedBox(height: 8),
|
||||
_TypeChips(
|
||||
selected: formState.selectedType,
|
||||
onSelect: (t) => ref.read(feedbackProvider.notifier).setType(t),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Rating
|
||||
const _SectionLabel('Calificación del servicio'),
|
||||
const SizedBox(height: 8),
|
||||
_StarRating(
|
||||
rating: formState.rating,
|
||||
onRate: (r) => ref.read(feedbackProvider.notifier).setRating(r),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Unidad (sin exponer chofer)
|
||||
const _SectionLabel('Unidad involucrada'),
|
||||
const SizedBox(height: 8),
|
||||
const _UnitBadge(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Mensaje libre
|
||||
const _SectionLabel('Descripción (opcional)'),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _messageController,
|
||||
maxLines: 4,
|
||||
maxLength: 300,
|
||||
onChanged: (v) =>
|
||||
ref.read(feedbackProvider.notifier).setMessage(v),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cuéntanos qué pasó...',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Error
|
||||
if (formState.status == FeedbackFormStatus.error)
|
||||
_ErrorBanner(message: formState.errorMessage ?? 'Error'),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Submit
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: formState.status == FeedbackFormStatus.loading
|
||||
? null
|
||||
: () {
|
||||
final addressId = ref.read(activeAddressIdProvider);
|
||||
final unitId = ref.read(activeUnitIdProvider);
|
||||
if (addressId == null) return;
|
||||
ref.read(feedbackProvider.notifier).submit(
|
||||
addressId: addressId,
|
||||
unitId: unitId,
|
||||
);
|
||||
},
|
||||
child: formState.status == FeedbackFormStatus.loading
|
||||
? const SizedBox(
|
||||
height: 18,
|
||||
width: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Text('Enviar reporte'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Chips de tipo
|
||||
// ──────────────────────────────────────────
|
||||
class _TypeChips extends StatelessWidget {
|
||||
final FeedbackType selected;
|
||||
final ValueChanged<FeedbackType> onSelect;
|
||||
const _TypeChips({required this.selected, required this.onSelect});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: FeedbackType.values.map((t) {
|
||||
final isSelected = t == selected;
|
||||
return ChoiceChip(
|
||||
label: Text(t.label),
|
||||
selected: isSelected,
|
||||
onSelected: (_) => onSelect(t),
|
||||
selectedColor: const Color(0xFFE1F5EE),
|
||||
side: BorderSide(
|
||||
color: isSelected
|
||||
? const Color(0xFF5DCAA5)
|
||||
: Theme.of(context).colorScheme.outlineVariant,
|
||||
),
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected
|
||||
? const Color(0xFF085041)
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 13,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Stars
|
||||
// ──────────────────────────────────────────
|
||||
class _StarRating extends StatelessWidget {
|
||||
final int rating;
|
||||
final ValueChanged<int> onRate;
|
||||
const _StarRating({required this.rating, required this.onRate});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: List.generate(5, (i) {
|
||||
final filled = i < rating;
|
||||
return GestureDetector(
|
||||
onTap: () => onRate(i + 1),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Icon(
|
||||
filled ? Icons.star_rounded : Icons.star_outline_rounded,
|
||||
size: 32,
|
||||
color: filled
|
||||
? const Color(0xFFEF9F27)
|
||||
: Theme.of(context).colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Badge de unidad (sin exponer chofer)
|
||||
// ──────────────────────────────────────────
|
||||
class _UnitBadge extends ConsumerWidget {
|
||||
const _UnitBadge();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final unitId = ref.watch(activeUnitIdProvider);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.local_shipping_outlined, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Unidad $unitId',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.shield_outlined,
|
||||
size: 13,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Solo se registra el número de unidad. El operador no es identificado.',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Error banner
|
||||
// ──────────────────────────────────────────
|
||||
class _ErrorBanner extends StatelessWidget {
|
||||
final String message;
|
||||
const _ErrorBanner({required this.message});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(message, style: const TextStyle(fontSize: 12))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Success view
|
||||
// ──────────────────────────────────────────
|
||||
class _SuccessView extends StatelessWidget {
|
||||
final VoidCallback onReset;
|
||||
const _SuccessView({required this.onReset});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Buzón de retroalimentación')),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFE1F5EE),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.check_circle_outline_rounded,
|
||||
size: 36,
|
||||
color: Color(0xFF1D9E75),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Reporte enviado',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Gracias. Tu retroalimentación ayuda a mejorar el servicio. El reporte fue registrado de forma anónima.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
OutlinedButton(
|
||||
onPressed: onReset,
|
||||
child: const Text('Enviar otro reporte'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Helpers
|
||||
// ──────────────────────────────────────────
|
||||
class _SectionLabel extends StatelessWidget {
|
||||
final String text;
|
||||
const _SectionLabel(this.text);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
25
views_v2/main.dart
Normal file
25
views_v2/main.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
// lib/main.dart
|
||||
// Punto de entrada. Inicializa Firebase, FCM, y monta el árbol Riverpod.
|
||||
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'features/notifications/notification_service.dart';
|
||||
import 'app.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Inicializar Firebase (requiere google-services.json en android/app/)
|
||||
await Firebase.initializeApp();
|
||||
|
||||
// Inicializar FCM: permisos, canal Android, handlers foreground/background
|
||||
await NotificationService.initialize();
|
||||
|
||||
runApp(
|
||||
// ProviderScope es el contenedor global de Riverpod
|
||||
const ProviderScope(
|
||||
child: RecolectaApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
378
views_v2/notifications_screen.dart
Normal file
378
views_v2/notifications_screen.dart
Normal file
@@ -0,0 +1,378 @@
|
||||
// lib/features/notifications/notifications_screen.dart
|
||||
// Historial de notificaciones FCM recibidas.
|
||||
// Los items se almacenan en memoria (no en BD) — solo mensajes del topic propio.
|
||||
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'notification_service.dart';
|
||||
import '../eta/eta_screen.dart'; // activeRouteIdProvider
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Modelo local de item de notificación
|
||||
// ──────────────────────────────────────────
|
||||
enum FcmEventType { routeStart, truckProximity, routeCompleted, reassignment, unknown }
|
||||
|
||||
FcmEventType _eventTypeFromMessage(RemoteMessage msg) {
|
||||
final type = msg.data['event'] as String?;
|
||||
switch (type) {
|
||||
case 'ROUTE_START':
|
||||
return FcmEventType.routeStart;
|
||||
case 'TRUCK_PROXIMITY':
|
||||
return FcmEventType.truckProximity;
|
||||
case 'ROUTE_COMPLETED':
|
||||
return FcmEventType.routeCompleted;
|
||||
case 'reasignacion':
|
||||
case 'retraso':
|
||||
return FcmEventType.reassignment;
|
||||
default:
|
||||
return FcmEventType.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationItem {
|
||||
final String title;
|
||||
final String body;
|
||||
final FcmEventType type;
|
||||
final DateTime receivedAt;
|
||||
|
||||
const NotificationItem({
|
||||
required this.title,
|
||||
required this.body,
|
||||
required this.type,
|
||||
required this.receivedAt,
|
||||
});
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Provider: lista de notificaciones en memoria
|
||||
// ──────────────────────────────────────────
|
||||
final notificationsListProvider =
|
||||
NotifierProvider<NotificationsNotifier, List<NotificationItem>>(
|
||||
NotificationsNotifier.new,
|
||||
);
|
||||
|
||||
class NotificationsNotifier extends Notifier<List<NotificationItem>> {
|
||||
@override
|
||||
List<NotificationItem> build() {
|
||||
// Escuchar mensajes FCM en foreground
|
||||
NotificationService.onFcmMessage.addListener(_onMessage);
|
||||
ref.onDispose(
|
||||
() => NotificationService.onFcmMessage.removeListener(_onMessage),
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
void _onMessage() {
|
||||
final msg = NotificationService.onFcmMessage.lastMessage;
|
||||
if (msg == null) return;
|
||||
final item = NotificationItem(
|
||||
title: msg.notification?.title ?? 'Recolección',
|
||||
body: msg.notification?.body ?? '',
|
||||
type: _eventTypeFromMessage(msg),
|
||||
receivedAt: DateTime.now(),
|
||||
);
|
||||
state = [item, ...state];
|
||||
}
|
||||
|
||||
void clearAll() => state = [];
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Pantalla de notificaciones
|
||||
// ──────────────────────────────────────────
|
||||
class NotificationsScreen extends ConsumerWidget {
|
||||
const NotificationsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final items = ref.watch(notificationsListProvider);
|
||||
final routeId = ref.watch(activeRouteIdProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Notificaciones'),
|
||||
actions: [
|
||||
if (items.isNotEmpty)
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
ref.read(notificationsListProvider.notifier).clearAll(),
|
||||
child: const Text('Limpiar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
children: [
|
||||
// Badge de suscripción FCM
|
||||
_FcmTopicBadge(routeId: routeId),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Aviso de privacidad
|
||||
_PrivacyNote(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (items.isEmpty)
|
||||
const _EmptyState()
|
||||
else ...[
|
||||
const _SectionLabel(label: 'Recientes'),
|
||||
...items.map((item) => _NotificationCard(item: item)),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────
|
||||
// Widgets auxiliares
|
||||
// ──────────────────────────────────────────
|
||||
class _FcmTopicBadge extends StatelessWidget {
|
||||
final String? routeId;
|
||||
const _FcmTopicBadge({required this.routeId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF1D9E75),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
TextSpan(children: [
|
||||
const TextSpan(
|
||||
text: 'Suscrito a ',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
TextSpan(
|
||||
text: routeId != null
|
||||
? 'topic_$routeId'
|
||||
: 'topic pendiente',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const TextSpan(
|
||||
text: ' · Solo recibes eventos de tu ruta',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PrivacyNote extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAEEDA), // amber-50
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0xFFFAC775)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.info_outline_rounded,
|
||||
size: 18, color: Color(0xFFBA7517)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Los mensajes no revelan la ubicación del camión. Solo se muestra el tiempo estimado de llegada.',
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF633806)),
|
||||
maxLines: 3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionLabel extends StatelessWidget {
|
||||
final String label;
|
||||
const _SectionLabel({required this.label});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Text(
|
||||
label.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.8,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NotificationCard extends StatelessWidget {
|
||||
final NotificationItem item;
|
||||
const _NotificationCard({required this.item});
|
||||
|
||||
IconData get _icon {
|
||||
switch (item.type) {
|
||||
case FcmEventType.routeStart:
|
||||
return Icons.arrow_forward_rounded;
|
||||
case FcmEventType.truckProximity:
|
||||
return Icons.local_shipping_rounded;
|
||||
case FcmEventType.routeCompleted:
|
||||
return Icons.check_circle_outline_rounded;
|
||||
case FcmEventType.reassignment:
|
||||
return Icons.swap_horiz_rounded;
|
||||
default:
|
||||
return Icons.notifications_outlined;
|
||||
}
|
||||
}
|
||||
|
||||
Color _accentColor() {
|
||||
switch (item.type) {
|
||||
case FcmEventType.routeStart:
|
||||
return const Color(0xFF1D9E75);
|
||||
case FcmEventType.truckProximity:
|
||||
return const Color(0xFFBA7517);
|
||||
case FcmEventType.routeCompleted:
|
||||
return Colors.grey;
|
||||
case FcmEventType.reassignment:
|
||||
return const Color(0xFF378ADD);
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
String _relativeTime() {
|
||||
final diff = DateTime.now().difference(item.receivedAt);
|
||||
if (diff.inMinutes < 1) return 'Ahora mismo';
|
||||
if (diff.inMinutes < 60) return 'Hace ${diff.inMinutes} min';
|
||||
if (diff.inHours < 24) return 'Hace ${diff.inHours} h';
|
||||
return 'Ayer';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final accent = _accentColor();
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border(
|
||||
left: BorderSide(color: accent, width: 3),
|
||||
top: BorderSide(color: Theme.of(context).colorScheme.outlineVariant, width: 0.5),
|
||||
right: BorderSide(color: Theme.of(context).colorScheme.outlineVariant, width: 0.5),
|
||||
bottom: BorderSide(color: Theme.of(context).colorScheme.outlineVariant, width: 0.5),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: accent.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(_icon, size: 16, color: accent),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
item.body,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_relativeTime(),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyState extends StatelessWidget {
|
||||
const _EmptyState();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 48),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.notifications_none_rounded,
|
||||
size: 48,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Sin notificaciones aún',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Recibirás un aviso cuando el camión esté cerca.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
51
views_v2/prevention_banner.dart
Normal file
51
views_v2/prevention_banner.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
// lib/shared/widgets/prevention_banner.dart
|
||||
// Banner de mensajería preventiva — obligatorio en la vista ETA.
|
||||
// Regla de privacidad #5: textos que desalientan sacar basura fuera de horario
|
||||
// o perseguir la unidad.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PreventionBanner extends StatelessWidget {
|
||||
final String? customMessage;
|
||||
|
||||
const PreventionBanner({super.key, this.customMessage});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAEEDA), // amber-50
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0xFFFAC775)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 1),
|
||||
child: Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
size: 18,
|
||||
color: Color(0xFFBA7517),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
customMessage ??
|
||||
'No saques tu basura antes de recibir el aviso de proximidad '
|
||||
'ni dejes bolsas en la calle por más de 30 min. '
|
||||
'No persigas ni detengas la unidad recolectora.',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF633806),
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
174
views_v2/progress_steps.dart
Normal file
174
views_v2/progress_steps.dart
Normal file
@@ -0,0 +1,174 @@
|
||||
// lib/shared/widgets/progress_steps.dart
|
||||
// Barra de 4 pasos del servicio. Sin mapa ni coordenadas.
|
||||
// Los pasos mapean a los eventos de positionId del backend:
|
||||
// 0 = pendiente, 1 = ROUTE_START (pos 2), 2 = TRUCK_PROXIMITY (pos 4), 3 = ROUTE_COMPLETED (pos 8)
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ProgressSteps extends StatelessWidget {
|
||||
/// 0 = pendiente, 1 = en camino, 2 = cerca, 3 = completado
|
||||
final int stepIndex;
|
||||
|
||||
const ProgressSteps({super.key, required this.stepIndex});
|
||||
|
||||
static const _steps = [
|
||||
_StepData('Servicio pendiente', Icons.access_time_rounded),
|
||||
_StepData('Salió al sector', Icons.arrow_forward_rounded),
|
||||
_StepData('Cerca (~15 min)', Icons.local_shipping_rounded),
|
||||
_StepData('Finalizado', Icons.check_circle_outline_rounded),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 10, 14, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.route_rounded,
|
||||
size: 16, color: Color(0xFF1D9E75)),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Progreso del servicio',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1, thickness: 0.5),
|
||||
...List.generate(_steps.length, (i) {
|
||||
final status = _stepStatus(i);
|
||||
return _StepRow(
|
||||
data: _steps[i],
|
||||
status: status,
|
||||
isLast: i == _steps.length - 1,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_Status _stepStatus(int i) {
|
||||
if (i < stepIndex) return _Status.done;
|
||||
if (i == stepIndex) return _Status.active;
|
||||
return _Status.pending;
|
||||
}
|
||||
}
|
||||
|
||||
enum _Status { done, active, pending }
|
||||
|
||||
class _StepData {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
const _StepData(this.label, this.icon);
|
||||
}
|
||||
|
||||
class _StepRow extends StatelessWidget {
|
||||
final _StepData data;
|
||||
final _Status status;
|
||||
final bool isLast;
|
||||
|
||||
const _StepRow({
|
||||
required this.data,
|
||||
required this.status,
|
||||
required this.isLast,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color iconBg;
|
||||
Color iconColor;
|
||||
IconData displayIcon;
|
||||
|
||||
switch (status) {
|
||||
case _Status.done:
|
||||
iconBg = const Color(0xFFE1F5EE);
|
||||
iconColor = const Color(0xFF1D9E75);
|
||||
displayIcon = Icons.check_rounded;
|
||||
break;
|
||||
case _Status.active:
|
||||
iconBg = const Color(0xFFFAEEDA);
|
||||
iconColor = const Color(0xFFBA7517);
|
||||
displayIcon = data.icon;
|
||||
break;
|
||||
case _Status.pending:
|
||||
iconBg = Theme.of(context).colorScheme.surfaceContainerLow;
|
||||
iconColor = Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
displayIcon = Icons.radio_button_unchecked_rounded;
|
||||
break;
|
||||
}
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: isLast
|
||||
? null
|
||||
: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: iconBg,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(displayIcon, size: 15, color: iconColor),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
data.label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: status == _Status.pending
|
||||
? Theme.of(context).colorScheme.onSurfaceVariant
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (status == _Status.active)
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAEEDA),
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
child: const Text(
|
||||
'Ahora',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF633806),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user