fix(chat): ajustes de texto, layout e icone do menu

- Mudar texto 'Chat #' para 'Ticket #' no desktop
- Passar ticketRef via URL para exibir numero correto
- Ajustar tamanho da janela minimizada (240px)
- Incluir ticketRef no checkMachineUpdates (Convex)
- Ajustar padding dos botoes no chat web
- Mudar icone 'Todos os tickets' para ClipboardList

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
rever-tecnologia 2025-12-09 13:06:03 -03:00
parent 3c2d1824fb
commit d20ebf7416
8 changed files with 35 additions and 25 deletions

View file

@ -70,6 +70,7 @@ pub struct ChatPollResponse {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ChatSessionSummary { pub struct ChatSessionSummary {
pub ticket_id: String, pub ticket_id: String,
pub ticket_ref: u64,
pub unread_count: u32, pub unread_count: u32,
pub last_activity_at: i64, pub last_activity_at: i64,
} }
@ -923,7 +924,7 @@ async fn process_chat_update(
if let Some(session) = current_sessions.first() { if let Some(session) = current_sessions.first() {
let label = format!("chat-{}", session.ticket_id); let label = format!("chat-{}", session.ticket_id);
if app.get_webview_window(&label).is_none() { if app.get_webview_window(&label).is_none() {
let _ = open_chat_window(app, &session.ticket_id); let _ = open_chat_window(app, &session.ticket_id, session.ticket_ref);
// Minimizar imediatamente após abrir // Minimizar imediatamente após abrir
let _ = set_chat_minimized(app, &session.ticket_id, true); let _ = set_chat_minimized(app, &session.ticket_id, true);
} }
@ -950,7 +951,7 @@ async fn process_chat_update(
// WINDOW MANAGEMENT // WINDOW MANAGEMENT
// ============================================================================ // ============================================================================
fn open_chat_window_internal(app: &tauri::AppHandle, ticket_id: &str) -> Result<(), String> { fn open_chat_window_internal(app: &tauri::AppHandle, ticket_id: &str, ticket_ref: u64) -> Result<(), String> {
let label = format!("chat-{}", ticket_id); let label = format!("chat-{}", ticket_id);
// Verificar se ja existe // Verificar se ja existe
@ -980,7 +981,7 @@ fn open_chat_window_internal(app: &tauri::AppHandle, ticket_id: &str) -> Result<
}; };
// Usar query param ao inves de path para compatibilidade com SPA // Usar query param ao inves de path para compatibilidade com SPA
let url_path = format!("index.html?view=chat&ticketId={}", ticket_id); let url_path = format!("index.html?view=chat&ticketId={}&ticketRef={}", ticket_id, ticket_ref);
WebviewWindowBuilder::new( WebviewWindowBuilder::new(
app, app,
@ -989,7 +990,7 @@ fn open_chat_window_internal(app: &tauri::AppHandle, ticket_id: &str) -> Result<
) )
.title("Chat de Suporte") .title("Chat de Suporte")
.inner_size(380.0, 520.0) .inner_size(380.0, 520.0)
.min_inner_size(220.0, 52.0) // Tamanho minimo para modo minimizado com badge .min_inner_size(240.0, 52.0) // Tamanho minimo para modo minimizado com badge
.position(x, y) .position(x, y)
.decorations(false) // Sem decoracoes nativas - usa header customizado .decorations(false) // Sem decoracoes nativas - usa header customizado
.transparent(true) // Permite fundo transparente .transparent(true) // Permite fundo transparente
@ -1005,8 +1006,8 @@ fn open_chat_window_internal(app: &tauri::AppHandle, ticket_id: &str) -> Result<
Ok(()) Ok(())
} }
pub fn open_chat_window(app: &tauri::AppHandle, ticket_id: &str) -> Result<(), String> { pub fn open_chat_window(app: &tauri::AppHandle, ticket_id: &str, ticket_ref: u64) -> Result<(), String> {
open_chat_window_internal(app, ticket_id) open_chat_window_internal(app, ticket_id, ticket_ref)
} }
pub fn close_chat_window(app: &tauri::AppHandle, ticket_id: &str) -> Result<(), String> { pub fn close_chat_window(app: &tauri::AppHandle, ticket_id: &str) -> Result<(), String> {
@ -1032,7 +1033,7 @@ pub fn set_chat_minimized(app: &tauri::AppHandle, ticket_id: &str, minimized: bo
// Tamanhos - chip minimizado com margem extra para badge (absolute -top-1 -right-1) // Tamanhos - chip minimizado com margem extra para badge (absolute -top-1 -right-1)
let (width, height) = if minimized { let (width, height) = if minimized {
(220.0, 52.0) // Tamanho com folga para badge que fica fora do chip (240.0, 52.0) // Tamanho com folga para "Ticket #XXX" e badge
} else { } else {
(380.0, 520.0) // Tamanho expandido (380.0, 520.0) // Tamanho expandido
}; };

View file

@ -38,9 +38,10 @@ function getFileIcon(fileName: string) {
interface ChatWidgetProps { interface ChatWidgetProps {
ticketId: string ticketId: string
ticketRef?: number
} }
export function ChatWidget({ ticketId }: ChatWidgetProps) { export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
const [messages, setMessages] = useState<ChatMessage[]>([]) const [messages, setMessages] = useState<ChatMessage[]>([])
const [inputValue, setInputValue] = useState("") const [inputValue, setInputValue] = useState("")
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
@ -267,7 +268,7 @@ export function ChatWidget({ ticketId }: ChatWidgetProps) {
<div className="pointer-events-auto flex items-center gap-2 rounded-full bg-slate-200 px-4 py-2 text-slate-600 shadow-lg"> <div className="pointer-events-auto flex items-center gap-2 rounded-full bg-slate-200 px-4 py-2 text-slate-600 shadow-lg">
<MessageCircle className="size-4" /> <MessageCircle className="size-4" />
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{ticketInfo ? `Chat #${ticketInfo.ref}` : "Chat"} {ticketRef ? `Ticket #${ticketRef}` : ticketInfo?.ref ? `Ticket #${ticketInfo.ref}` : "Chat"}
</span> </span>
<span className="size-2 rounded-full bg-slate-400" /> <span className="size-2 rounded-full bg-slate-400" />
<span className="text-xs text-slate-500">Offline</span> <span className="text-xs text-slate-500">Offline</span>
@ -287,7 +288,7 @@ export function ChatWidget({ ticketId }: ChatWidgetProps) {
> >
<MessageCircle className="size-4" /> <MessageCircle className="size-4" />
<span className="text-sm font-medium"> <span className="text-sm font-medium">
Chat #{ticketInfo?.ref} Ticket #{ticketRef ?? ticketInfo?.ref}
</span> </span>
<span className="size-2 rounded-full bg-emerald-400" /> <span className="size-2 rounded-full bg-emerald-400" />
<ChevronUp className="size-4" /> <ChevronUp className="size-4" />
@ -321,9 +322,9 @@ export function ChatWidget({ ticketId }: ChatWidgetProps) {
Online Online
</span> </span>
</div> </div>
{ticketInfo && ( {(ticketRef || ticketInfo) && (
<p className="text-xs text-slate-500"> <p className="text-xs text-slate-500">
#{ticketInfo.ref} - {ticketInfo.agentName ?? "Suporte"} Ticket #{ticketRef ?? ticketInfo?.ref} - {ticketInfo?.agentName ?? "Suporte"}
</p> </p>
)} )}
</div> </div>

View file

@ -30,7 +30,7 @@ let cached: ClientCache | null = null
type MachineUpdatePayload = { type MachineUpdatePayload = {
hasActiveSessions: boolean hasActiveSessions: boolean
sessions: Array<{ ticketId: string; unreadCount: number; lastActivityAt: number }> sessions: Array<{ ticketId: string; ticketRef: number; unreadCount: number; lastActivityAt: number }>
totalUnread: number totalUnread: number
} }

View file

@ -1,9 +1,10 @@
import { ChatWidget } from "./ChatWidget" import { ChatWidget } from "./ChatWidget"
export function ChatApp() { export function ChatApp() {
// Obter ticketId da URL // Obter ticketId e ticketRef da URL
const params = new URLSearchParams(window.location.search) const params = new URLSearchParams(window.location.search)
const ticketId = params.get("ticketId") const ticketId = params.get("ticketId")
const ticketRef = params.get("ticketRef")
if (!ticketId) { if (!ticketId) {
return ( return (
@ -13,7 +14,7 @@ export function ChatApp() {
) )
} }
return <ChatWidget ticketId={ticketId} /> return <ChatWidget ticketId={ticketId} ticketRef={ticketRef ? Number(ticketRef) : undefined} />
} }
export { ChatWidget } export { ChatWidget }

View file

@ -1098,10 +1098,10 @@ const resolvedAppUrl = useMemo(() => {
// Abre/minimiza chat quando aparecem novas não lidas // Abre/minimiza chat quando aparecem novas não lidas
if (hasSessions && totalUnread > prevUnread) { if (hasSessions && totalUnread > prevUnread) {
const ticketId = payload.sessions[0].ticketId const session = payload.sessions[0]
invoke("open_chat_window", { ticketId }).catch(console.error) invoke("open_chat_window", { ticketId: session.ticketId, ticketRef: session.ticketRef }).catch(console.error)
// Minimiza para não ser intrusivo // Minimiza para não ser intrusivo
invoke("set_chat_minimized", { ticketId, minimized: true }).catch(console.error) invoke("set_chat_minimized", { ticketId: session.ticketId, minimized: true }).catch(console.error)
} }
prevUnread = totalUnread prevUnread = totalUnread

View file

@ -524,11 +524,18 @@ export const checkMachineUpdates = query({
} }
} }
const sessionSummaries = sessions.map((s) => ({ // Buscar tickets para obter o reference
ticketId: s.ticketId, const sessionSummaries = await Promise.all(
unreadCount: s.unreadByMachine ?? 0, sessions.map(async (s) => {
lastActivityAt: s.lastActivityAt, const ticket = await ctx.db.get(s.ticketId)
})) return {
ticketId: s.ticketId,
ticketRef: ticket?.reference ?? 0,
unreadCount: s.unreadByMachine ?? 0,
lastActivityAt: s.lastActivityAt,
}
})
)
const totalUnread = sessionSummaries.reduce((sum, s) => sum + s.unreadCount, 0) const totalUnread = sessionSummaries.reduce((sum, s) => sum + s.unreadCount, 0)

View file

@ -81,7 +81,7 @@ const navigation: NavigationGroup[] = [
icon: Ticket, icon: Ticket,
requiredRole: "staff", requiredRole: "staff",
children: [ children: [
{ title: "Todos os tickets", url: "/tickets", icon: Ticket, requiredRole: "staff" }, { title: "Todos os tickets", url: "/tickets", icon: ClipboardList, requiredRole: "staff" },
{ title: "Resolvidos", url: "/tickets/resolved", icon: ShieldCheck, requiredRole: "staff" }, { title: "Resolvidos", url: "/tickets/resolved", icon: ShieldCheck, requiredRole: "staff" },
], ],
}, },

View file

@ -210,7 +210,7 @@ export function TicketChatPanel({ ticketId }: TicketChatPanelProps) {
return ( return (
<Card className="flex flex-col overflow-hidden border-slate-200"> <Card className="flex flex-col overflow-hidden border-slate-200">
{/* Header */} {/* Header */}
<CardHeader className="flex flex-row items-center justify-between gap-2 border-b border-slate-100 bg-slate-50 px-4 py-3"> <CardHeader className="flex flex-row items-center justify-between gap-2 border-b border-slate-100 bg-slate-50 px-4 pr-3 py-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex size-10 items-center justify-center rounded-full bg-black text-white"> <div className="flex size-10 items-center justify-center rounded-full bg-black text-white">
<MessageCircle className="size-5" /> <MessageCircle className="size-5" />