Actualizacion de mejoras
This commit is contained in:
@@ -20,155 +20,234 @@ class _ReporteChatScreenState extends State<ReporteChatScreen> {
|
||||
List<Map<String, dynamic>> _msgs = [];
|
||||
bool _loading = true;
|
||||
Timer? _timer;
|
||||
String? _myRol;
|
||||
int? _myUserId;
|
||||
|
||||
@override void initState() { super.initState(); _load();
|
||||
_timer = Timer.periodic(const Duration(seconds: 5), (_) => _load()); }
|
||||
@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 {
|
||||
final auth = context.read<AuthService>();
|
||||
final rol = auth.currentUser?.rol ?? 'CIUDADANO';
|
||||
// Cargar TODOS los mensajes del reporte sin filtro de rol
|
||||
final msgs = await DbHelper.getChatMsgs(widget.reporteId);
|
||||
await DbHelper.markChatRead(widget.reporteId, rol);
|
||||
// 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; });
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_scroll.hasClients) _scroll.animateTo(_scroll.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 200), curve: Curves.easeOut);
|
||||
});
|
||||
_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) return;
|
||||
final auth = context.read<AuthService>();
|
||||
final user = auth.currentUser;
|
||||
if (user == null) return;
|
||||
if (text.isEmpty || _myRol == null || _myUserId == null) return;
|
||||
_ctrl.clear();
|
||||
await DbHelper.insertChatMsg(widget.reporteId, user.id!, user.rol, text);
|
||||
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 auth = context.watch<AuthService>();
|
||||
final myRol = auth.currentUser?.rol ?? 'CIUDADANO';
|
||||
final isAdmin = myRol == 'ADMINISTRADOR';
|
||||
final accent = isAdmin ? AppColors.verdeAdmin : AppColors.guindaPrimary;
|
||||
final isAdmin = _myRol == 'ADMINISTRADOR';
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.grisFondo,
|
||||
appBar: AppBar(
|
||||
backgroundColor: accent, foregroundColor: Colors.white,
|
||||
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)),
|
||||
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)))],
|
||||
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)),
|
||||
])),
|
||||
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())
|
||||
? const Center(child: CircularProgressIndicator(
|
||||
color: AppColors.guindaPrimary))
|
||||
: _msgs.isEmpty
|
||||
? Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Icon(Icons.chat_bubble_outline, size: 48, color: Colors.grey.shade400),
|
||||
const Icon(Icons.chat_bubble_outline, size: 48, color: AppColors.grisTexto),
|
||||
const SizedBox(height: 12),
|
||||
Text(isAdmin ? 'Inicia la conversacion con el ciudadano'
|
||||
: 'Escribe tu mensaje al Ayuntamiento',
|
||||
style: TextStyle(color: Colors.grey.shade500)),
|
||||
Text(isAdmin
|
||||
? 'Sin mensajes aún. Inicia la conversación.'
|
||||
: 'Escribe tu mensaje al Ayuntamiento de Celaya.',
|
||||
style: const TextStyle(color: AppColors.grisTexto),
|
||||
textAlign: TextAlign.center),
|
||||
]))
|
||||
: ListView.builder(
|
||||
controller: _scroll, padding: const EdgeInsets.all(12),
|
||||
controller: _scroll,
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 4),
|
||||
itemCount: _msgs.length,
|
||||
itemBuilder: (_, i) {
|
||||
final m = _msgs[i];
|
||||
final rol = m['rol'] as String;
|
||||
final isMe = rol == myRol;
|
||||
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')}' : '';
|
||||
? '${fecha.hour.toString().padLeft(2,'0')}:${fecha.minute.toString().padLeft(2,'0')}'
|
||||
: '';
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
mainAxisAlignment: me ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (!isMe) ...[
|
||||
CircleAvatar(radius: 14,
|
||||
backgroundColor: (rol=='ADMINISTRADOR' ? AppColors.verdeAdmin : AppColors.guindaPrimary).withOpacity(0.15),
|
||||
child: Icon(rol=='ADMINISTRADOR' ? Icons.admin_panel_settings : Icons.person,
|
||||
size: 14, color: rol=='ADMINISTRADOR' ? AppColors.verdeAdmin : AppColors.guindaPrimary)),
|
||||
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(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.72),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isMe ? accent : Colors.white,
|
||||
color: me ? AppColors.guindaPrimary : Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: const Radius.circular(16), topRight: const Radius.circular(16),
|
||||
bottomLeft: Radius.circular(isMe ? 16 : 4),
|
||||
bottomRight: Radius.circular(isMe ? 4 : 16),
|
||||
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)],
|
||||
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' ? 'Ayuntamiento de Celaya' : '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))),
|
||||
],
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
if (!isMe) Text(rol=='ADMINISTRADOR' ? 'Ayuntamiento' : 'Ciudadano',
|
||||
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold,
|
||||
color: rol=='ADMINISTRADOR' ? AppColors.verdeAdmin : AppColors.guindaPrimary)),
|
||||
Text(m['mensaje'] as String? ?? '',
|
||||
style: TextStyle(fontSize: 13, height: 1.4,
|
||||
color: isMe ? Colors.white : AppColors.negroTexto)),
|
||||
Text(hora, style: TextStyle(fontSize: 9,
|
||||
color: isMe ? Colors.white60 : AppColors.grisTexto)),
|
||||
]),
|
||||
)),
|
||||
if (isMe) const SizedBox(width: 6),
|
||||
if (me) const SizedBox(width: 6),
|
||||
],
|
||||
),
|
||||
);
|
||||
})),
|
||||
if (!widget.isClosed) Container(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
|
||||
decoration: BoxDecoration(color: Colors.white,
|
||||
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 4, offset: const Offset(0,-2))]),
|
||||
child: SafeArea(top: false, child: Row(children: [
|
||||
Expanded(child: TextField(
|
||||
controller: _ctrl, maxLines: 3, minLines: 1,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
hintText: isAdmin ? 'Responde al ciudadano...' : 'Escribe al Ayuntamiento...',
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none),
|
||||
filled: true, fillColor: AppColors.grisFondo,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), isDense: true),
|
||||
)),
|
||||
const SizedBox(width: 8),
|
||||
CircleAvatar(radius: 22, backgroundColor: accent,
|
||||
child: IconButton(icon: const Icon(Icons.send, color: Colors.white, size: 18), onPressed: _send)),
|
||||
])))
|
||||
else Container(padding: const EdgeInsets.all(14), color: Colors.white,
|
||||
|
||||
// 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 Ayuntamiento de Celaya...',
|
||||
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', style: TextStyle(color: AppColors.grisTexto, fontSize: 12)),
|
||||
Text('Chat cerrado — Reporte completado',
|
||||
style: TextStyle(color: AppColors.grisTexto, fontSize: 12)),
|
||||
])),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
@override void dispose() { _ctrl.dispose(); _scroll.dispose(); _timer?.cancel(); super.dispose(); }
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrl.dispose(); _scroll.dispose(); _timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user