vistas de ciudadano, escalar animaciones de mascota, implementacion de chatbot para concientizacion, modificacion de datos de ciudadano, modificacion de vista principal

This commit is contained in:
shinra32
2026-05-23 05:03:05 -06:00
parent 89dcc6250b
commit ca076607c7
39 changed files with 2909 additions and 560 deletions

View File

@@ -0,0 +1,244 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:dio/dio.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/widgets/app_widgets.dart';
import '../data/incident_service.dart';
import '../models/incident.dart';
import '../providers/incident_providers.dart';
class ReportIssueScreen extends ConsumerStatefulWidget {
const ReportIssueScreen({super.key});
@override
ConsumerState<ReportIssueScreen> createState() => _ReportIssueScreenState();
}
class _ReportIssueScreenState extends ConsumerState<ReportIssueScreen> {
final _formKey = GlobalKey<FormState>();
final _descCtrl = TextEditingController();
int? _unitId;
String _category = 'no_recoleccion';
File? _photo;
bool _submitting = false;
@override
void dispose() {
_descCtrl.dispose();
super.dispose();
}
Future<void> _pickPhoto() async {
final picker = ImagePicker();
final picked = await picker.pickImage(
source: ImageSource.gallery,
maxWidth: 1600,
imageQuality: 80,
);
if (picked != null) {
setState(() => _photo = File(picked.path));
}
}
Future<void> _submit() async {
if (!(_formKey.currentState?.validate() ?? false)) return;
setState(() => _submitting = true);
try {
await ref
.read(incidentServiceProvider)
.createIncident(
category: _category,
description: _descCtrl.text.trim(),
unitId: _unitId,
photo: _photo,
);
ref.invalidate(myIncidentsProvider);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Reporte enviado. ¡Gracias!')),
);
Navigator.of(context).pop();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(_friendly(e))));
} finally {
if (mounted) setState(() => _submitting = false);
}
}
String _friendly(Object e) {
if (e is DioException) {
final data = e.response?.data;
if (data is Map && data['detail'] != null)
return data['detail'].toString();
}
return 'No se pudo enviar el reporte';
}
@override
Widget build(BuildContext context) {
final unitsAsync = ref.watch(unitsProvider);
return Scaffold(
backgroundColor: AppTheme.background,
appBar: AppBar(title: const Text('Reportar un problema')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: AppCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const AppSectionTitle(title: 'Detalles del reporte'),
// Unidad
Text(
'Unidad relacionada (opcional)',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 6),
unitsAsync.when(
loading: () => const LinearProgressIndicator(),
error: (e, _) => Text(
'No se pudieron cargar las unidades',
style: const TextStyle(
color: AppTheme.danger,
fontSize: 12,
),
),
data: (units) => DropdownButtonFormField<int?>(
initialValue: _unitId,
isExpanded: true,
items: [
const DropdownMenuItem<int?>(
value: null,
child: Text('Sin unidad'),
),
for (final u in units)
DropdownMenuItem<int?>(
value: u.id,
child: Text(u.label),
),
],
onChanged: (v) => setState(() => _unitId = v),
),
),
const SizedBox(height: 16),
// Categoría
Text(
'Categoría',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 6),
DropdownButtonFormField<String>(
initialValue: _category,
isExpanded: true,
items: [
for (final entry in incidentCategories.entries)
DropdownMenuItem<String>(
value: entry.key,
child: Text(entry.value),
),
],
onChanged: (v) {
if (v != null) setState(() => _category = v);
},
validator: (v) => (v == null || v.isEmpty)
? 'Selecciona una categoría'
: null,
),
const SizedBox(height: 16),
AppFormField(
label: 'Descripción',
controller: _descCtrl,
hint: 'Cuéntanos qué pasó…',
maxLines: 5,
keyboardType: TextInputType.multiline,
validator: (v) {
final t = (v ?? '').trim();
if (t.length < 3)
return 'Describe el problema (mínimo 3 caracteres)';
return null;
},
),
const SizedBox(height: 16),
// Foto
Row(
children: [
OutlinedButton.icon(
onPressed: _submitting ? null : _pickPhoto,
icon: const Icon(Icons.photo_camera_outlined),
label: Text(
_photo == null ? 'Adjuntar foto' : 'Cambiar foto',
),
),
if (_photo != null) ...[
const SizedBox(width: 8),
IconButton(
tooltip: 'Quitar foto',
icon: const Icon(Icons.close, color: AppTheme.danger),
onPressed: _submitting
? null
: () => setState(() => _photo = null),
),
],
],
),
if (_photo != null) ...[
const SizedBox(height: 12),
ClipRRect(
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
child: Image.file(
_photo!,
height: 180,
width: double.infinity,
fit: BoxFit.cover,
),
),
],
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _submitting ? null : _submit,
child: _submitting
? const SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Enviar reporte'),
),
),
],
),
),
),
),
);
}
}