Initial commit: SIBU 2.0 MISSION

This commit is contained in:
2026-02-21 09:53:31 -05:00
commit 0c7aa53c8b
400 changed files with 67708 additions and 0 deletions

View File

@ -0,0 +1,440 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:sizer/sizer.dart';
import '../../core/app_export.dart';
import './widgets/bus_route_card_widget.dart';
import './widgets/nearby_landmarks_widget.dart';
import './widgets/report_issue_widget.dart';
import './widgets/stop_amenities_widget.dart';
import './widgets/user_comments_widget.dart';
class BusStopDetails extends StatefulWidget {
const BusStopDetails({super.key});
@override
State<BusStopDetails> createState() => _BusStopDetailsState();
}
class _BusStopDetailsState extends State<BusStopDetails> {
bool _isLoading = true;
DateTime _lastUpdated = DateTime.now();
// Mock data for bus stop details
final Map<String, dynamic> _busStopData = {
"stopId": "BS001",
"stopName": "Parada Central Boquete",
"address": "Av. Central, frente al Parque José Domingo de Obaldía",
"coordinates": {"lat": 8.7833, "lng": -82.4333},
"lastUpdated": "2025-10-19 18:45:00",
};
final List<Map<String, dynamic>> _routesData = [
{
"routeName": "Boquete - David",
"nextBusMinutes": 12,
"upcomingTimes": ["19:15", "19:45", "20:15", "20:45"],
"isDelayed": false,
},
{
"routeName": "David - Boquete",
"nextBusMinutes": 25,
"upcomingTimes": ["19:30", "20:00", "20:30", "21:00"],
"isDelayed": true,
},
{
"routeName": "Boquete - Caldera",
"nextBusMinutes": 45,
"upcomingTimes": ["19:50", "20:50", "21:50"],
"isDelayed": false,
},
];
final Map<String, dynamic> _amenitiesData = {
"hasShelter": true,
"hasBench": true,
"isAccessible": false,
"hasLighting": true,
"hasTrashCan": true,
};
final List<Map<String, dynamic>> _landmarksData = [
{
"name": "Parque José Domingo de Obaldía",
"distance": "50m",
"type": "park",
},
{
"name": "Banco Nacional de Panamá",
"distance": "120m",
"type": "bank",
},
{
"name": "Supermercado El Mandado",
"distance": "200m",
"type": "store",
},
{
"name": "Hospital Regional de Boquete",
"distance": "350m",
"type": "hospital",
},
];
final List<Map<String, dynamic>> _commentsData = [
{
"userName": "María González",
"userAvatar":
"https://images.unsplash.com/photo-1687757660301-7aac1198ed63",
"semanticLabel":
"Profile photo of a woman with long brown hair wearing a blue blouse, smiling at the camera",
"comment":
"Muy buena parada, siempre está limpia y los buses llegan a tiempo. El refugio protege bien de la lluvia.",
"timestamp": "Hace 2 horas",
"rating": 5,
},
{
"userName": "Carlos Rodríguez",
"userAvatar":
"https://images.unsplash.com/photo-1735651705945-64bc6d18d555",
"semanticLabel":
"Profile photo of a middle-aged man with short gray hair and glasses wearing a white shirt",
"comment":
"La parada está bien ubicada pero necesita mejor iluminación en las noches. Los horarios son confiables.",
"timestamp": "Hace 1 día",
"rating": 4,
},
{
"userName": "Ana Morales",
"userAvatar":
"https://images.unsplash.com/photo-1722291493584-9e75986c6c5c",
"semanticLabel":
"Profile photo of a young woman with curly black hair wearing a red top, smiling outdoors",
"comment":
"Excelente ubicación cerca del parque. Los buses de la ruta Boquete-David son muy puntuales.",
"timestamp": "Hace 3 días",
"rating": 5,
},
];
@override
void initState() {
super.initState();
_loadBusStopData();
}
Future<void> _loadBusStopData() async {
// Simulate loading data
await Future.delayed(const Duration(seconds: 1));
if (mounted) {
setState(() {
_isLoading = false;
_lastUpdated = DateTime.now();
});
}
}
Future<void> _refreshData() async {
setState(() {
_isLoading = true;
});
await _loadBusStopData();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Información actualizada'),
backgroundColor: AppTheme.successGreen,
duration: Duration(seconds: 2),
),
);
}
}
void _shareStopInfo() {
HapticFeedback.lightImpact();
final stopName = _busStopData['stopName'] as String;
final address = _busStopData['address'] as String;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Compartiendo información de: $stopName'),
backgroundColor: AppTheme.primaryBlack,
duration: const Duration(seconds: 2),
),
);
}
void _handleNotificationToggle() {
HapticFeedback.lightImpact();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Notificación configurada'),
backgroundColor: AppTheme.accentYellow,
duration: Duration(seconds: 2),
),
);
}
void _handleAddComment() {
HapticFeedback.lightImpact();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Función de comentarios próximamente'),
backgroundColor: AppTheme.primaryBlack,
duration: Duration(seconds: 2),
),
);
}
void _handleReportSubmitted() {
HapticFeedback.lightImpact();
}
String _formatLastUpdated() {
final now = DateTime.now();
final difference = now.difference(_lastUpdated);
if (difference.inMinutes < 1) {
return 'Actualizado hace unos segundos';
} else if (difference.inMinutes < 60) {
return 'Actualizado hace ${difference.inMinutes} min';
} else {
return 'Actualizado hace ${difference.inHours}h';
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final stopName = _busStopData['stopName'] as String? ?? '';
final address = _busStopData['address'] as String? ?? '';
return Scaffold(
backgroundColor: AppTheme.backgroundGray,
appBar: AppBar(
backgroundColor: theme.colorScheme.surface,
elevation: 0,
leading: GestureDetector(
onTap: () {
HapticFeedback.lightImpact();
Navigator.pop(context);
},
child: Container(
margin: EdgeInsets.all(2.w),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Center(
child: CustomIconWidget(
iconName: 'close',
color: theme.colorScheme.onSurface,
size: 24,
),
),
),
),
actions: [
GestureDetector(
onTap: _shareStopInfo,
child: Container(
margin: EdgeInsets.all(2.w),
padding: EdgeInsets.all(2.w),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: CustomIconWidget(
iconName: 'share',
color: theme.colorScheme.onSurface,
size: 24,
),
),
),
],
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: theme.brightness == Brightness.light
? Brightness.dark
: Brightness.light,
),
),
body: _isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(
color: AppTheme.accentYellow,
),
SizedBox(height: 2.h),
Text(
'Cargando información de la parada...',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
),
],
),
)
: RefreshIndicator(
onRefresh: _refreshData,
color: AppTheme.accentYellow,
backgroundColor: theme.colorScheme.surface,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Stop header information
Container(
width: double.infinity,
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
stopName,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
color: theme.colorScheme.onSurface,
),
),
SizedBox(height: 1.h),
Row(
children: [
CustomIconWidget(
iconName: 'location_on',
color: AppTheme.primaryBlack,
size: 16,
),
SizedBox(width: 1.w),
Expanded(
child: Text(
address,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface
.withValues(alpha: 0.7),
),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
),
],
),
SizedBox(height: 2.h),
Container(
padding: EdgeInsets.symmetric(
horizontal: 3.w, vertical: 1.h),
decoration: BoxDecoration(
color: AppTheme.successGreen
.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CustomIconWidget(
iconName: 'access_time',
color: AppTheme.successGreen,
size: 14,
),
SizedBox(width: 1.w),
Text(
_formatLastUpdated(),
style: theme.textTheme.labelSmall?.copyWith(
color: AppTheme.successGreen,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
),
SizedBox(height: 3.h),
// Real-time arrival predictions
Text(
'Llegadas en tiempo real',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
SizedBox(height: 2.h),
// Route cards
..._routesData
.map((routeData) => BusRouteCardWidget(
routeData: routeData,
onNotificationToggle: _handleNotificationToggle,
))
.toList(),
SizedBox(height: 4.h),
// Stop amenities
StopAmenitiesWidget(amenitiesData: _amenitiesData),
SizedBox(height: 3.h),
// Nearby landmarks
NearbyLandmarksWidget(landmarks: _landmarksData),
SizedBox(height: 3.h),
// User comments section
UserCommentsWidget(
comments: _commentsData,
onAddComment: _handleAddComment,
),
SizedBox(height: 3.h),
// Report issue section
ReportIssueWidget(
onReportSubmitted: _handleReportSubmitted,
),
SizedBox(height: 4.h),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,207 @@
import 'package:flutter/material.dart';
import 'package:sizer/sizer.dart';
import '../../../../core/app_export.dart';
class BusRouteCardWidget extends StatefulWidget {
final Map<String, dynamic> routeData;
final VoidCallback? onNotificationToggle;
const BusRouteCardWidget({
super.key,
required this.routeData,
this.onNotificationToggle,
});
@override
State<BusRouteCardWidget> createState() => _BusRouteCardWidgetState();
}
class _BusRouteCardWidgetState extends State<BusRouteCardWidget> {
bool _isNotificationEnabled = false;
void _toggleNotification() {
setState(() {
_isNotificationEnabled = !_isNotificationEnabled;
});
if (widget.onNotificationToggle != null) {
widget.onNotificationToggle!();
}
}
String _formatTime(int minutes) {
if (minutes < 60) {
return '${minutes}min';
} else {
final hours = minutes ~/ 60;
final remainingMinutes = minutes % 60;
return remainingMinutes > 0
? '${hours}h ${remainingMinutes}min'
: '${hours}h';
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final routeName = widget.routeData['routeName'] as String? ?? '';
final nextBusMinutes = widget.routeData['nextBusMinutes'] as int? ?? 0;
final upcomingTimes =
(widget.routeData['upcomingTimes'] as List?)?.cast<String>() ?? [];
final isDelayed = widget.routeData['isDelayed'] as bool? ?? false;
return Container(
margin: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Padding(
padding: EdgeInsets.all(4.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Route header with notification toggle
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
routeName,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
overflow: TextOverflow.ellipsis,
),
if (isDelayed) ...[
SizedBox(height: 0.5.h),
Container(
padding: EdgeInsets.symmetric(
horizontal: 2.w, vertical: 0.5.h),
decoration: BoxDecoration(
color:
AppTheme.warningOrange.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'Retraso reportado',
style: theme.textTheme.labelSmall?.copyWith(
color: AppTheme.warningOrange,
fontWeight: FontWeight.w500,
),
),
),
],
],
),
),
GestureDetector(
onTap: _toggleNotification,
child: Container(
padding: EdgeInsets.all(2.w),
decoration: BoxDecoration(
color: _isNotificationEnabled
? AppTheme.accentYellow.withValues(alpha: 0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: CustomIconWidget(
iconName: _isNotificationEnabled
? 'notifications'
: 'notifications_none',
color: _isNotificationEnabled
? AppTheme.accentYellow
: theme.colorScheme.onSurface.withValues(alpha: 0.6),
size: 24,
),
),
),
],
),
SizedBox(height: 3.h),
// Next bus countdown
Container(
width: double.infinity,
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: AppTheme.accentYellow.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppTheme.accentYellow.withValues(alpha: 0.3),
width: 1,
),
),
child: Column(
children: [
Text(
'Próximo bus en:',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
),
SizedBox(height: 1.h),
Text(
_formatTime(nextBusMinutes),
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
color: AppTheme.primaryBlack,
),
),
],
),
),
// Upcoming times
if (upcomingTimes.isNotEmpty) ...[
SizedBox(height: 3.h),
Text(
'Próximas salidas:',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurface,
),
),
SizedBox(height: 1.h),
Wrap(
spacing: 2.w,
runSpacing: 1.h,
children: upcomingTimes
.take(4)
.map((time) => Container(
padding: EdgeInsets.symmetric(
horizontal: 3.w, vertical: 1.h),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: theme.colorScheme.outline
.withValues(alpha: 0.3),
width: 1,
),
),
child: Text(
time,
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurface,
),
),
))
.toList(),
),
],
],
),
),
);
}
}

View File

@ -0,0 +1,172 @@
import 'package:flutter/material.dart';
import 'package:sizer/sizer.dart';
import '../../../../core/app_export.dart';
class NearbyLandmarksWidget extends StatelessWidget {
final List<Map<String, dynamic>> landmarks;
const NearbyLandmarksWidget({
super.key,
required this.landmarks,
});
Widget _buildLandmarkItem(
BuildContext context, Map<String, dynamic> landmark) {
final theme = Theme.of(context);
final name = landmark['name'] as String? ?? '';
final distance = landmark['distance'] as String? ?? '';
final type = landmark['type'] as String? ?? '';
String iconName = 'place';
switch (type.toLowerCase()) {
case 'restaurant':
iconName = 'restaurant';
break;
case 'hospital':
iconName = 'local_hospital';
break;
case 'school':
iconName = 'school';
break;
case 'bank':
iconName = 'account_balance';
break;
case 'store':
iconName = 'store';
break;
case 'gas_station':
iconName = 'local_gas_station';
break;
default:
iconName = 'place';
}
return Container(
margin: EdgeInsets.only(bottom: 2.h),
padding: EdgeInsets.all(3.w),
decoration: BoxDecoration(
color: theme.colorScheme.surface.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: theme.colorScheme.outline.withValues(alpha: 0.2),
width: 1,
),
),
child: Row(
children: [
Container(
width: 10.w,
height: 5.h,
decoration: BoxDecoration(
color: AppTheme.primaryBlack.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Center(
child: CustomIconWidget(
iconName: iconName,
color: AppTheme.primaryBlack,
size: 20,
),
),
),
SizedBox(width: 3.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurface,
),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 0.5.h),
Text(
distance,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
),
],
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
if (landmarks.isEmpty) {
return Container(
width: double.infinity,
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
CustomIconWidget(
iconName: 'location_off',
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
size: 32,
),
SizedBox(height: 2.h),
Text(
'No hay puntos de referencia cercanos',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
textAlign: TextAlign.center,
),
],
),
);
}
return Container(
width: double.infinity,
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Puntos de referencia cercanos',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
SizedBox(height: 3.h),
...landmarks
.map((landmark) => _buildLandmarkItem(context, landmark))
.toList(),
],
),
);
}
}

View File

@ -0,0 +1,396 @@
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:sizer/sizer.dart';
import '../../../../core/app_export.dart';
class ReportIssueWidget extends StatefulWidget {
final VoidCallback? onReportSubmitted;
const ReportIssueWidget({
super.key,
this.onReportSubmitted,
});
@override
State<ReportIssueWidget> createState() => _ReportIssueWidgetState();
}
class _ReportIssueWidgetState extends State<ReportIssueWidget> {
final TextEditingController _issueController = TextEditingController();
String _selectedIssueType = 'Limpieza';
XFile? _capturedImage;
CameraController? _cameraController;
List<CameraDescription> _cameras = [];
bool _isCameraInitialized = false;
bool _isSubmitting = false;
final List<String> _issueTypes = [
'Limpieza',
'Daños en la estructura',
'Falta de iluminación',
'Problemas de accesibilidad',
'Vandalismo',
'Otro',
];
@override
void initState() {
super.initState();
_initializeCamera();
}
@override
void dispose() {
_issueController.dispose();
_cameraController?.dispose();
super.dispose();
}
Future<bool> _requestCameraPermission() async {
if (kIsWeb) return true;
return (await Permission.camera.request()).isGranted;
}
Future<void> _initializeCamera() async {
try {
if (!await _requestCameraPermission()) return;
_cameras = await availableCameras();
if (_cameras.isEmpty) return;
final camera = kIsWeb
? _cameras.firstWhere(
(c) => c.lensDirection == CameraLensDirection.front,
orElse: () => _cameras.first)
: _cameras.firstWhere(
(c) => c.lensDirection == CameraLensDirection.back,
orElse: () => _cameras.first);
_cameraController = CameraController(
camera, kIsWeb ? ResolutionPreset.medium : ResolutionPreset.high);
await _cameraController!.initialize();
if (!kIsWeb) {
try {
await _cameraController!.setFocusMode(FocusMode.auto);
await _cameraController!.setFlashMode(FlashMode.auto);
} catch (e) {}
}
if (mounted) {
setState(() {
_isCameraInitialized = true;
});
}
} catch (e) {
// Silent fail - camera not available
}
}
Future<void> _capturePhoto() async {
if (_cameraController == null || !_cameraController!.value.isInitialized)
return;
try {
final XFile photo = await _cameraController!.takePicture();
setState(() {
_capturedImage = photo;
});
} catch (e) {
// Silent fail
}
}
Future<void> _pickImageFromGallery() async {
try {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
setState(() {
_capturedImage = image;
});
}
} catch (e) {
// Silent fail
}
}
Future<void> _submitReport() async {
if (_issueController.text.trim().isEmpty) return;
setState(() {
_isSubmitting = true;
});
// Simulate report submission
await Future.delayed(const Duration(seconds: 2));
if (mounted) {
setState(() {
_isSubmitting = false;
_issueController.clear();
_capturedImage = null;
_selectedIssueType = 'Limpieza';
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Reporte enviado exitosamente'),
backgroundColor: AppTheme.successGreen,
),
);
if (widget.onReportSubmitted != null) {
widget.onReportSubmitted!();
}
}
}
void _showReportDialog() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
height: 85.h,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Padding(
padding: EdgeInsets.all(4.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Handle bar
Center(
child: Container(
width: 12.w,
height: 0.5.h,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
),
),
SizedBox(height: 3.h),
// Header
Row(
children: [
Expanded(
child: Text(
'Reportar problema',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
GestureDetector(
onTap: () => Navigator.pop(context),
child: CustomIconWidget(
iconName: 'close',
color: Theme.of(context).colorScheme.onSurface,
size: 24,
),
),
],
),
SizedBox(height: 4.h),
// Issue type dropdown
Text(
'Tipo de problema',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 1.h),
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 4.w),
decoration: BoxDecoration(
border:
Border.all(color: Theme.of(context).colorScheme.outline),
borderRadius: BorderRadius.circular(8),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _selectedIssueType,
isExpanded: true,
items: _issueTypes
.map((type) => DropdownMenuItem(
value: type,
child: Text(type),
))
.toList(),
onChanged: (value) {
if (value != null) {
setState(() {
_selectedIssueType = value;
});
}
},
),
),
),
SizedBox(height: 3.h),
// Description field
Text(
'Descripción del problema',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 1.h),
TextField(
controller: _issueController,
maxLines: 4,
decoration: const InputDecoration(
hintText:
'Describe el problema que encontraste en esta parada...',
),
),
SizedBox(height: 3.h),
// Photo section
Text(
'Agregar foto (opcional)',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 1.h),
if (_capturedImage != null) ...[
Container(
width: double.infinity,
height: 20.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).colorScheme.outline),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: kIsWeb
? Image.network(_capturedImage!.path, fit: BoxFit.cover)
: Image.network(_capturedImage!.path,
fit: BoxFit.cover),
),
),
SizedBox(height: 2.h),
],
Row(
children: [
if (_isCameraInitialized) ...[
Expanded(
child: ElevatedButton.icon(
onPressed: _capturePhoto,
icon: CustomIconWidget(
iconName: 'camera_alt',
color: AppTheme.primaryBlack,
size: 20,
),
label: const Text('Tomar foto'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.accentYellow,
foregroundColor: AppTheme.primaryBlack,
),
),
),
SizedBox(width: 2.w),
],
Expanded(
child: OutlinedButton.icon(
onPressed: _pickImageFromGallery,
icon: CustomIconWidget(
iconName: 'photo_library',
color: Theme.of(context).colorScheme.primary,
size: 20,
),
label: const Text('Galería'),
),
),
],
),
const Spacer(),
// Submit button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isSubmitting ? null : _submitReport,
child: _isSubmitting
? const CircularProgressIndicator()
: const Text('Enviar reporte'),
),
),
SizedBox(height: 2.h),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
width: double.infinity,
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Reportar problema',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
SizedBox(height: 2.h),
Text(
'¿Encontraste algún problema en esta parada? Ayúdanos a mejorar reportándolo.',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
),
SizedBox(height: 3.h),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _showReportDialog,
icon: CustomIconWidget(
iconName: 'report_problem',
color: AppTheme.primaryBlack,
size: 20,
),
label: const Text('Reportar problema'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.warningOrange,
foregroundColor: AppTheme.primaryBlack,
padding: EdgeInsets.symmetric(vertical: 3.h),
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'package:sizer/sizer.dart';
import '../../../../core/app_export.dart';
class StopAmenitiesWidget extends StatelessWidget {
final Map<String, dynamic> amenitiesData;
const StopAmenitiesWidget({
super.key,
required this.amenitiesData,
});
Widget _buildAmenityItem(
BuildContext context, String iconName, String label, bool isAvailable) {
final theme = Theme.of(context);
return Column(
children: [
Container(
width: 12.w,
height: 6.h,
decoration: BoxDecoration(
color: isAvailable
? AppTheme.successGreen.withValues(alpha: 0.1)
: theme.colorScheme.surface.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isAvailable
? AppTheme.successGreen.withValues(alpha: 0.3)
: theme.colorScheme.outline.withValues(alpha: 0.3),
width: 1,
),
),
child: Center(
child: CustomIconWidget(
iconName: iconName,
color: isAvailable
? AppTheme.successGreen
: theme.colorScheme.onSurface.withValues(alpha: 0.4),
size: 24,
),
),
),
SizedBox(height: 1.h),
Text(
label,
style: theme.textTheme.labelSmall?.copyWith(
color: isAvailable
? theme.colorScheme.onSurface
: theme.colorScheme.onSurface.withValues(alpha: 0.5),
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
],
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final hasShelter = amenitiesData['hasShelter'] as bool? ?? false;
final hasBench = amenitiesData['hasBench'] as bool? ?? false;
final isAccessible = amenitiesData['isAccessible'] as bool? ?? false;
final hasLighting = amenitiesData['hasLighting'] as bool? ?? false;
final hasTrashCan = amenitiesData['hasTrashCan'] as bool? ?? false;
return Container(
width: double.infinity,
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Comodidades de la parada',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
SizedBox(height: 3.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: _buildAmenityItem(
context,
'home',
'Refugio',
hasShelter,
),
),
Expanded(
child: _buildAmenityItem(
context,
'chair',
'Asiento',
hasBench,
),
),
Expanded(
child: _buildAmenityItem(
context,
'accessible',
'Accesible',
isAccessible,
),
),
Expanded(
child: _buildAmenityItem(
context,
'lightbulb',
'Iluminación',
hasLighting,
),
),
Expanded(
child: _buildAmenityItem(
context,
'delete',
'Basura',
hasTrashCan,
),
),
],
),
],
),
);
}
}

View File

@ -0,0 +1,236 @@
import 'package:flutter/material.dart';
import 'package:sizer/sizer.dart';
import '../../../../core/app_export.dart';
class UserCommentsWidget extends StatefulWidget {
final List<Map<String, dynamic>> comments;
final VoidCallback? onAddComment;
const UserCommentsWidget({
super.key,
required this.comments,
this.onAddComment,
});
@override
State<UserCommentsWidget> createState() => _UserCommentsWidgetState();
}
class _UserCommentsWidgetState extends State<UserCommentsWidget> {
bool _isExpanded = false;
Widget _buildCommentItem(BuildContext context, Map<String, dynamic> comment) {
final theme = Theme.of(context);
final userName = comment['userName'] as String? ?? '';
final userAvatar = comment['userAvatar'] as String? ?? '';
final semanticLabel = comment['semanticLabel'] as String? ?? '';
final commentText = comment['comment'] as String? ?? '';
final timestamp = comment['timestamp'] as String? ?? '';
final rating = comment['rating'] as int? ?? 0;
return Container(
margin: EdgeInsets.only(bottom: 3.h),
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: theme.colorScheme.surface.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: theme.colorScheme.outline.withValues(alpha: 0.2),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// User info and rating
Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: CustomImageWidget(
imageUrl: userAvatar,
width: 40,
height: 40,
fit: BoxFit.cover,
semanticLabel: semanticLabel,
),
),
SizedBox(width: 3.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
userName,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
overflow: TextOverflow.ellipsis,
),
Text(
timestamp,
style: theme.textTheme.bodySmall?.copyWith(
color:
theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
),
],
),
),
// Rating stars
Row(
children: List.generate(
5,
(index) => CustomIconWidget(
iconName: index < rating ? 'star' : 'star_border',
color: index < rating
? AppTheme.accentYellow
: theme.colorScheme.onSurface
.withValues(alpha: 0.3),
size: 16,
)),
),
],
),
SizedBox(height: 2.h),
// Comment text
Text(
commentText,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface,
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
width: double.infinity,
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header with expand/collapse
GestureDetector(
onTap: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
child: Row(
children: [
Expanded(
child: Text(
'Comentarios de usuarios (${widget.comments.length})',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
),
CustomIconWidget(
iconName: _isExpanded ? 'expand_less' : 'expand_more',
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
size: 24,
),
],
),
),
if (_isExpanded) ...[
SizedBox(height: 3.h),
// Add comment button
GestureDetector(
onTap: widget.onAddComment,
child: Container(
width: double.infinity,
padding: EdgeInsets.all(3.w),
decoration: BoxDecoration(
color: AppTheme.accentYellow.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppTheme.accentYellow.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
children: [
CustomIconWidget(
iconName: 'add_comment',
color: AppTheme.primaryBlack,
size: 20,
),
SizedBox(width: 2.w),
Text(
'Agregar comentario',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
color: AppTheme.primaryBlack,
),
),
],
),
),
),
SizedBox(height: 3.h),
// Comments list
if (widget.comments.isEmpty)
Container(
width: double.infinity,
padding: EdgeInsets.all(6.w),
child: Column(
children: [
CustomIconWidget(
iconName: 'comment',
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
size: 32,
),
SizedBox(height: 2.h),
Text(
'Aún no hay comentarios',
style: theme.textTheme.bodyMedium?.copyWith(
color:
theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
textAlign: TextAlign.center,
),
SizedBox(height: 1.h),
Text(
'¡Sé el primero en compartir tu experiencia!',
style: theme.textTheme.bodySmall?.copyWith(
color:
theme.colorScheme.onSurface.withValues(alpha: 0.5),
),
textAlign: TextAlign.center,
),
],
),
)
else
...widget.comments
.map((comment) => _buildCommentItem(context, comment))
.toList(),
],
],
),
);
}
}