Initial commit: SIBU 2.0 MISSION
This commit is contained in:
@ -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(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user