265 lines
13 KiB
Dart
265 lines
13 KiB
Dart
import 'dart:io';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import '../../core/app_colors.dart';
|
|
import '../shared/reporte_chat_screen.dart';
|
|
import '../../database/db_helper.dart';
|
|
import '../../models/models.dart';
|
|
import '../../services/auth_service.dart';
|
|
|
|
class CitizenReporteScreen extends StatefulWidget {
|
|
const CitizenReporteScreen({super.key});
|
|
@override State<CitizenReporteScreen> createState() => _CitizenReporteScreenState();
|
|
}
|
|
|
|
class _CitizenReporteScreenState extends State<CitizenReporteScreen> {
|
|
String _tipo = 'CAMION_NO_PASO';
|
|
final _desc = TextEditingController();
|
|
int _calif = 5;
|
|
bool _loading = false, _sent = false;
|
|
List<ReporteModel> _reportes = [];
|
|
File? _foto;
|
|
final _picker = ImagePicker();
|
|
|
|
static const _tipos = {
|
|
'CAMION_NO_PASO': 'El camion no paso',
|
|
'RETRASO': 'Retraso significativo',
|
|
'RESIDUOS_NO_RECOGIDOS': 'Residuos no recogidos',
|
|
'OTRO': 'Otro motivo',
|
|
};
|
|
|
|
@override void initState() { super.initState(); _load(); }
|
|
|
|
Future<void> _load() async {
|
|
final auth = context.read<AuthService>();
|
|
if (auth.currentUser == null) return;
|
|
final r = await DbHelper.getReportesByUser(auth.currentUser!.id!);
|
|
if (mounted) setState(() => _reportes = r);
|
|
}
|
|
|
|
Future<void> _pickImage(ImageSource source) async {
|
|
try {
|
|
final picked = await _picker.pickImage(source: source, imageQuality: 70, maxWidth: 1024);
|
|
if (picked != null && mounted) setState(() => _foto = File(picked.path));
|
|
} catch (e) {
|
|
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('No se pudo acceder a la camara: $e'),
|
|
backgroundColor: AppColors.rojoError));
|
|
}
|
|
}
|
|
|
|
void _showPhotoOptions() {
|
|
showModalBottomSheet(context: context, builder: (_) => SafeArea(
|
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
|
ListTile(leading: const Icon(Icons.camera_alt, color: AppColors.guindaPrimary),
|
|
title: const Text('Tomar foto'),
|
|
onTap: () { Navigator.pop(context); _pickImage(ImageSource.camera); }),
|
|
ListTile(leading: const Icon(Icons.photo_library, color: AppColors.guindaPrimary),
|
|
title: const Text('Elegir de galeria'),
|
|
onTap: () { Navigator.pop(context); _pickImage(ImageSource.gallery); }),
|
|
if (_foto != null)
|
|
ListTile(leading: const Icon(Icons.delete_outline, color: AppColors.rojoError),
|
|
title: const Text('Quitar foto', style: TextStyle(color: AppColors.rojoError)),
|
|
onTap: () { Navigator.pop(context); setState(() => _foto = null); }),
|
|
])));
|
|
}
|
|
|
|
Future<void> _send() async {
|
|
final auth = context.read<AuthService>();
|
|
if (auth.currentUser == null) return;
|
|
if (_desc.text.trim().isEmpty) {
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
|
content: Text('Describe el problema para poder enviar el reporte'),
|
|
backgroundColor: AppColors.rojoError));
|
|
return;
|
|
}
|
|
setState(() => _loading = true);
|
|
|
|
try {
|
|
// Insertar reporte directo en la BD
|
|
final db = await DbHelper.database;
|
|
final id = await db.insert('reportes', {
|
|
'user_id': auth.currentUser!.id,
|
|
'tipo': _tipo,
|
|
'descripcion': _desc.text.trim(),
|
|
'colonia': auth.primaryDomicilio?.colonia ?? 'Sin colonia',
|
|
'route_id': auth.primaryDomicilio?.routeId ?? '',
|
|
'fecha': DateTime.now().toIso8601String(),
|
|
'estado': 'PENDIENTE',
|
|
'calificacion': _calif,
|
|
'foto_path': _foto?.path,
|
|
});
|
|
|
|
if (id <= 0) throw Exception('No se pudo guardar el reporte');
|
|
|
|
await _load();
|
|
_desc.clear();
|
|
setState(() { _foto = null; _loading = false; _sent = true; });
|
|
await Future.delayed(const Duration(seconds: 3));
|
|
if (mounted) setState(() => _sent = false);
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
setState(() => _loading = false);
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
|
content: Text('Error al enviar: $e'),
|
|
backgroundColor: AppColors.rojoError));
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Scaffold(
|
|
backgroundColor: AppColors.grisFondo,
|
|
appBar: AppBar(automaticallyImplyLeading: false,
|
|
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
|
title: const Text('Reportar Incidencia'),
|
|
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
|
child: Container(height: 4, color: AppColors.dorado))),
|
|
body: _sent
|
|
? Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
|
const Icon(Icons.check_circle, color: AppColors.verdeExito, size: 72),
|
|
const SizedBox(height: 16),
|
|
const Text('Reporte enviado', style: TextStyle(fontSize: 22,
|
|
fontWeight: FontWeight.bold, color: AppColors.verdeExito)),
|
|
const SizedBox(height: 8),
|
|
const Text('El Ayuntamiento revisara tu reporte pronto.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: AppColors.grisTexto)),
|
|
const SizedBox(height: 8),
|
|
const Text('Podras chatear con ellos desde "Mis Reportes".',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: AppColors.grisTexto, fontSize: 12)),
|
|
]))
|
|
: SingleChildScrollView(padding: const EdgeInsets.all(16), child: Column(children: [
|
|
Card(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
child: Padding(padding: const EdgeInsets.all(16), child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
const Text('Tipo de incidencia', style: TextStyle(
|
|
fontWeight: FontWeight.bold, color: AppColors.guindaPrimary, fontSize: 15)),
|
|
const SizedBox(height: 8),
|
|
..._tipos.entries.map((e) => RadioListTile<String>(dense: true,
|
|
value: e.key, groupValue: _tipo,
|
|
title: Text(e.value, style: const TextStyle(fontSize: 13)),
|
|
activeColor: AppColors.guindaPrimary,
|
|
onChanged: (v) => setState(() => _tipo = v!))),
|
|
const SizedBox(height: 8),
|
|
DropdownButtonFormField<int>(value: _calif,
|
|
decoration: const InputDecoration(labelText: 'Calificacion del servicio',
|
|
border: OutlineInputBorder()),
|
|
items: [5,4,3,2,1].map((n) => DropdownMenuItem(value: n,
|
|
child: Text(['Excelente','Bueno','Regular','Malo','Muy malo'][5-n]))).toList(),
|
|
onChanged: (v) => setState(() => _calif = v!)),
|
|
const SizedBox(height: 10),
|
|
TextField(controller: _desc, maxLines: 3,
|
|
decoration: const InputDecoration(hintText: 'Describe el problema...',
|
|
border: OutlineInputBorder(), filled: true, fillColor: Colors.white)),
|
|
const SizedBox(height: 12),
|
|
|
|
// Foto adjunta
|
|
const Text('Foto del incidente (opcional)',
|
|
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13)),
|
|
const SizedBox(height: 8),
|
|
GestureDetector(
|
|
onTap: _showPhotoOptions,
|
|
child: Container(
|
|
width: double.infinity, height: _foto != null ? 180 : 80,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: _foto != null
|
|
? AppColors.guindaPrimary : Colors.grey.shade300,
|
|
style: BorderStyle.solid)),
|
|
child: _foto != null
|
|
? Stack(children: [
|
|
ClipRRect(borderRadius: BorderRadius.circular(8),
|
|
child: Image.file(_foto!, fit: BoxFit.cover,
|
|
width: double.infinity, height: 180)),
|
|
Positioned(top: 8, right: 8,
|
|
child: GestureDetector(onTap: () => setState(() => _foto = null),
|
|
child: Container(padding: const EdgeInsets.all(4),
|
|
decoration: const BoxDecoration(
|
|
color: AppColors.rojoError, shape: BoxShape.circle),
|
|
child: const Icon(Icons.close, color: Colors.white, size: 16)))),
|
|
])
|
|
: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
|
const Icon(Icons.add_a_photo_outlined,
|
|
color: AppColors.grisTexto, size: 28),
|
|
const SizedBox(height: 4),
|
|
const Text('Agregar foto', style: TextStyle(
|
|
color: AppColors.grisTexto, fontSize: 12)),
|
|
]),
|
|
),
|
|
),
|
|
const SizedBox(height: 14),
|
|
SizedBox(width: double.infinity, height: 48,
|
|
child: ElevatedButton.icon(
|
|
onPressed: _loading ? null : _send,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
|
icon: _loading ? const SizedBox(width: 18, height: 18,
|
|
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
|
: const Icon(Icons.send),
|
|
label: const Text('ENVIAR REPORTE',
|
|
style: TextStyle(fontWeight: FontWeight.bold)))),
|
|
]))),
|
|
if (_reportes.isNotEmpty) ...[
|
|
const SizedBox(height: 16),
|
|
const Align(alignment: Alignment.centerLeft,
|
|
child: Text('Mis Reportes', style: TextStyle(fontWeight: FontWeight.bold,
|
|
color: AppColors.guindaPrimary, fontSize: 15))),
|
|
const SizedBox(height: 8),
|
|
..._reportes.map((r) {
|
|
final isClosed = r.estado == 'COMPLETADO';
|
|
final id = r.id ?? 0;
|
|
final folio = 'RPT-${id.toString().padLeft(5, "0")}';
|
|
return Card(margin: const EdgeInsets.only(bottom: 6),
|
|
child: Column(children: [
|
|
ListTile(dense: true,
|
|
leading: CircleAvatar(backgroundColor: AppColors.guindaPrimary, radius: 16,
|
|
child: const Icon(Icons.report, color: Colors.white, size: 16)),
|
|
title: Row(children: [
|
|
Expanded(child: Text(_tipos[r.tipo] ?? r.tipo,
|
|
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600))),
|
|
Text(folio, style: const TextStyle(fontSize: 9, color: AppColors.grisTexto)),
|
|
]),
|
|
subtitle: Text(r.descripcion, maxLines: 1, overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(fontSize: 11)),
|
|
trailing: Container(padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
|
decoration: BoxDecoration(color: _estadoColor(r.estado).withOpacity(0.15),
|
|
borderRadius: BorderRadius.circular(10)),
|
|
child: Text(r.estado.replaceAll('_',' '),
|
|
style: TextStyle(fontSize: 9, color: _estadoColor(r.estado),
|
|
fontWeight: FontWeight.bold)))),
|
|
if (r.id != null) Padding(
|
|
padding: const EdgeInsets.fromLTRB(14, 0, 14, 8),
|
|
child: SizedBox(width: double.infinity,
|
|
child: OutlinedButton.icon(
|
|
onPressed: () => Navigator.push(context, MaterialPageRoute(
|
|
builder: (_) => ReporteChatScreen(
|
|
reporteId: r.id!, folio: folio, isClosed: isClosed))),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: isClosed ? AppColors.grisTexto : AppColors.guindaPrimary,
|
|
side: BorderSide(color: isClosed ? AppColors.grisTexto : AppColors.guindaPrimary),
|
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
|
minimumSize: Size.zero),
|
|
icon: Icon(isClosed ? Icons.lock : Icons.chat_bubble_outline, size: 14),
|
|
label: Text(isClosed ? 'Chat cerrado' : 'Escribir a la Direccion General',
|
|
style: const TextStyle(fontSize: 11))))),
|
|
]));
|
|
}),
|
|
],
|
|
])),
|
|
);
|
|
|
|
Color _estadoColor(String e) {
|
|
switch (e) {
|
|
case 'RESUELTO': return AppColors.verdeExito;
|
|
case 'EN_REVISION': return AppColors.azulInfo;
|
|
default: return AppColors.naranjaAlerta;
|
|
}
|
|
}
|
|
|
|
@override void dispose() { _desc.dispose(); super.dispose(); }
|
|
}
|