Files
AppRecoleccion/lib/screens/shared/reporte_chat_screen.dart
2026-05-23 09:31:45 -06:00

254 lines
11 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../core/app_colors.dart';
import '../../database/db_helper.dart';
import '../../services/auth_service.dart';
class ReporteChatScreen extends StatefulWidget {
final int reporteId;
final String folio;
final bool isClosed;
const ReporteChatScreen({super.key, required this.reporteId,
required this.folio, this.isClosed = false});
@override State<ReporteChatScreen> createState() => _ReporteChatScreenState();
}
class _ReporteChatScreenState extends State<ReporteChatScreen> {
final _ctrl = TextEditingController();
final _scroll = ScrollController();
List<Map<String, dynamic>> _msgs = [];
bool _loading = true;
Timer? _timer;
String? _myRol;
int? _myUserId;
@override
void initState() {
super.initState();
final auth = context.read<AuthService>();
_myRol = auth.currentUser?.rol ?? 'CIUDADANO';
_myUserId = auth.currentUser?.id;
_load();
_timer = Timer.periodic(const Duration(seconds: 4), (_) => _load());
}
Future<void> _load() async {
// Cargar TODOS los mensajes del reporte sin filtro de rol
final msgs = await DbHelper.getChatMsgs(widget.reporteId);
// Marcar como leídos los que NO son míos
if (_myRol != null) {
try {
await DbHelper.markChatRead(widget.reporteId, _myRol!);
} catch (_) {}
}
if (mounted) {
setState(() { _msgs = msgs; _loading = false; });
_scrollToBottom();
}
}
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scroll.hasClients) {
_scroll.animateTo(_scroll.position.maxScrollExtent,
duration: const Duration(milliseconds: 200), curve: Curves.easeOut);
}
});
}
Future<void> _send() async {
final text = _ctrl.text.trim();
if (text.isEmpty || _myRol == null || _myUserId == null) return;
_ctrl.clear();
await DbHelper.insertChatMsg(widget.reporteId, _myUserId!, _myRol!, text);
await _load();
}
bool _isMe(Map<String, dynamic> msg) {
// Un mensaje es mío si fue enviado con el mismo rol (admin ve sus msgs, ciudadano ve los suyos)
final msgRol = msg['rol'] as String? ?? '';
final msgUserId = msg['user_id'] as int?;
// Comparar por user_id si disponible, sino por rol
if (_myUserId != null && msgUserId != null) return msgUserId == _myUserId;
return msgRol == _myRol;
}
@override
Widget build(BuildContext context) {
final isAdmin = _myRol == 'ADMINISTRADOR';
return Scaffold(
backgroundColor: AppColors.grisFondo,
appBar: AppBar(
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Text('Chat del Reporte',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
Text('Folio: ${widget.folio}',
style: const TextStyle(fontSize: 11, color: Colors.white70)),
]),
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
child: Container(height: 4, color: AppColors.dorado)),
actions: [
if (widget.isClosed)
Container(margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: AppColors.verdeExito,
borderRadius: BorderRadius.circular(12)),
child: const Text('COMPLETADO',
style: TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold))),
IconButton(icon: const Icon(Icons.refresh), onPressed: _load),
],
),
body: Column(children: [
if (widget.isClosed)
Container(width: double.infinity, padding: const EdgeInsets.all(10),
color: AppColors.verdeExito.withOpacity(0.08),
child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.lock, size: 14, color: AppColors.verdeExito),
SizedBox(width: 6),
Text('Reporte completado — Chat cerrado',
style: TextStyle(fontSize: 12, color: AppColors.verdeExito,
fontWeight: FontWeight.w600)),
])),
// Mensajes
Expanded(child: _loading
? const Center(child: CircularProgressIndicator(
color: AppColors.guindaPrimary))
: _msgs.isEmpty
? Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
const Icon(Icons.chat_bubble_outline, size: 48, color: AppColors.grisTexto),
const SizedBox(height: 12),
Text(isAdmin
? 'Sin mensajes aún. Inicia la conversación.'
: 'Escribe tu mensaje al Dir. Gral. Servicios Municipales.',
style: const TextStyle(color: AppColors.grisTexto),
textAlign: TextAlign.center),
]))
: ListView.builder(
controller: _scroll,
padding: const EdgeInsets.fromLTRB(12, 12, 12, 4),
itemCount: _msgs.length,
itemBuilder: (_, i) {
final m = _msgs[i];
final me = _isMe(m);
final rol = m['rol'] as String? ?? '';
final fecha = DateTime.tryParse(m['fecha'] as String? ?? '');
final hora = fecha != null
? '${fecha.hour.toString().padLeft(2,'0')}:${fecha.minute.toString().padLeft(2,'0')}'
: '';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: me ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (!me) ...[
CircleAvatar(radius: 16,
backgroundColor: AppColors.guindaPrimary.withOpacity(0.15),
child: Icon(
rol == 'ADMINISTRADOR' ? Icons.admin_panel_settings : Icons.person,
size: 15, color: AppColors.guindaPrimary)),
const SizedBox(width: 6),
],
Flexible(child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.72),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: me ? AppColors.guindaPrimary : Colors.white,
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(18),
topRight: const Radius.circular(18),
bottomLeft: Radius.circular(me ? 18 : 4),
bottomRight: Radius.circular(me ? 4 : 18),
),
boxShadow: [BoxShadow(
color: Colors.black.withOpacity(0.07),
blurRadius: 4, offset: const Offset(0, 2))],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (!me) ...[
Text(
rol == 'ADMINISTRADOR' ? 'Dir. Gral. Servicios Municipales' : 'Ciudadano',
style: const TextStyle(fontSize: 10,
fontWeight: FontWeight.bold,
color: AppColors.guindaPrimary)),
const SizedBox(height: 2),
],
Text(m['mensaje'] as String? ?? '',
style: TextStyle(fontSize: 13, height: 1.4,
color: me ? Colors.white : AppColors.negroTexto)),
const SizedBox(height: 2),
Align(alignment: Alignment.bottomRight,
child: Text(hora, style: TextStyle(fontSize: 9,
color: me ? Colors.white54 : AppColors.grisTexto))),
],
),
)),
if (me) const SizedBox(width: 6),
],
),
);
})),
// Input
if (!widget.isClosed)
Container(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.08),
blurRadius: 6, offset: const Offset(0, -2))]),
child: SafeArea(top: false, child: Row(children: [
Expanded(child: TextField(
controller: _ctrl,
maxLines: 4, minLines: 1,
textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(
hintText: isAdmin
? 'Responde al ciudadano...'
: 'Escribe al Dir. Gral. Servicios Municipales...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
borderSide: BorderSide.none),
filled: true, fillColor: AppColors.grisFondo,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
isDense: true,
),
onSubmitted: (_) => _send(),
)),
const SizedBox(width: 8),
CircleAvatar(
radius: 24,
backgroundColor: AppColors.guindaPrimary,
child: IconButton(
icon: const Icon(Icons.send, color: Colors.white, size: 20),
onPressed: _send)),
])))
else
Container(
padding: const EdgeInsets.all(14), color: Colors.white,
child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.lock_outline, size: 16, color: AppColors.grisTexto),
SizedBox(width: 6),
Text('Chat cerrado — Reporte completado',
style: TextStyle(color: AppColors.grisTexto, fontSize: 12)),
])),
]),
);
}
@override
void dispose() {
_ctrl.dispose(); _scroll.dispose(); _timer?.cancel();
super.dispose();
}
}