UX: notificaciones de chat con toast y sonido (WebAudio) + skeletons de carga en Agenda y Chat

This commit is contained in:
2026-09-01 22:51:01 +00:00
parent f0d7ac4c01
commit cfacb23f45
4 changed files with 203 additions and 4 deletions

View File

@@ -1,9 +1,11 @@
import type { FC, FormEvent, KeyboardEvent } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Send, Search, ArrowLeft } from 'lucide-react';
import Layout from '../components/Layout';
import { Card, Button, EmptyState, toast } from '../components/ui';
import { Card, Button, EmptyState, Skeleton, toast } from '../components/ui';
import { ROLE_LABELS } from '../lib/auth';import { odooApi, type ChatUser, type ChatMessage } from '../services/odoo';
import { setActiveChatUser } from '../lib/chat-notifications';
const POLL_MS = 5000;
@@ -38,6 +40,7 @@ const Chat: FC = () => {
const [search, setSearch] = useState('');
const lastIdRef = useRef(0);
const bottomRef = useRef<HTMLDivElement>(null);
const [searchParams] = useSearchParams();
const activeUser = users.find((u) => u.id === activeId) || null;
@@ -93,6 +96,21 @@ const Chat: FC = () => {
return () => clearInterval(id);
}, [fetchUsers, fetchThread, activeId]);
// Registrar la conversación visible: las notificaciones globales no suenan
// ni muestran toast para el remitente que ya está en pantalla
useEffect(() => {
setActiveChatUser(activeId);
return () => setActiveChatUser(null);
}, [activeId]);
// Abrir conversación directa por query (?with=<userId>), ej. desde el toast
useEffect(() => {
const withId = parseInt(searchParams.get('with') || '', 10);
if (!withId || withId === activeId) return;
if (!users.some((u) => u.id === withId)) return;
openConversation(withId);
}, [searchParams, users, activeId, openConversation]);
const send = async (e?: FormEvent) => {
e?.preventDefault();
const body = draft.trim();
@@ -146,7 +164,17 @@ const Chat: FC = () => {
</div>
<div className="flex-1 overflow-y-auto">
{loadingUsers ? (
<p className="p-4 text-sm text-theme-muted">Cargando...</p>
<div className="p-3 space-y-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="flex items-center gap-3">
<Skeleton className="h-10 w-10 rounded-full shrink-0" />
<div className="flex-1 space-y-1.5">
<Skeleton className="h-3.5 w-2/3" />
<Skeleton className="h-3 w-1/2" />
</div>
</div>
))}
</div>
) : filteredUsers.length === 0 ? (
<p className="p-4 text-sm text-theme-muted">No hay usuarios para mostrar.</p>
) : (
@@ -220,7 +248,13 @@ const Chat: FC = () => {
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-2 bg-theme-bg">
{loadingThread ? (
<p className="text-sm text-theme-muted text-center py-6">Cargando mensajes...</p>
<div className="space-y-3">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className={`flex ${i % 2 ? 'justify-end' : 'justify-start'}`}>
<Skeleton className={`h-9 ${i % 2 ? 'w-1/2' : 'w-2/3'} rounded-2xl`} />
</div>
))}
</div>
) : messages.length === 0 ? (
<p className="text-sm text-theme-muted text-center py-6">
Aún no hay mensajes. Escribe el primero.