From a564cd2de722e0b60980a60a34fa897637ea5815 Mon Sep 17 00:00:00 2001 From: esdrasrenan Date: Sun, 5 Oct 2025 02:13:32 -0300 Subject: [PATCH 1/6] fix: stabilize dashboard chart layout Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- web/src/app/dashboard/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/app/dashboard/page.tsx b/web/src/app/dashboard/page.tsx index 14b9a99..4a0908a 100644 --- a/web/src/app/dashboard/page.tsx +++ b/web/src/app/dashboard/page.tsx @@ -17,7 +17,7 @@ export default function Dashboard() { } > -
+
From 0abb425d51c44d87497e979b3828439c3b50707d Mon Sep 17 00:00:00 2001 From: esdrasrenan Date: Sun, 5 Oct 2025 02:16:53 -0300 Subject: [PATCH 2/6] feat: compact recent tickets panel on dashboard Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .../tickets/recent-tickets-panel.tsx | 135 ++++++++++++++---- 1 file changed, 110 insertions(+), 25 deletions(-) diff --git a/web/src/components/tickets/recent-tickets-panel.tsx b/web/src/components/tickets/recent-tickets-panel.tsx index 17d587b..a41b65f 100644 --- a/web/src/components/tickets/recent-tickets-panel.tsx +++ b/web/src/components/tickets/recent-tickets-panel.tsx @@ -1,32 +1,117 @@ -"use client"; +"use client" -import { useQuery } from "convex/react"; +import Link from "next/link" +import { formatDistanceToNow } from "date-fns" +import { ptBR } from "date-fns/locale" +import { useQuery } from "convex/react" // @ts-expect-error Convex runtime API lacks TS declarations -import { api } from "@/convex/_generated/api"; -import { DEFAULT_TENANT_ID } from "@/lib/constants"; -import { mapTicketsFromServerList } from "@/lib/mappers/ticket"; -import { TicketsTable } from "@/components/tickets/tickets-table"; +import { api } from "@/convex/_generated/api" +import { DEFAULT_TENANT_ID } from "@/lib/constants" +import { mapTicketsFromServerList } from "@/lib/mappers/ticket" +import type { Ticket } from "@/lib/schemas/ticket" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Skeleton } from "@/components/ui/skeleton" +import { TicketPriorityPill } from "@/components/tickets/priority-pill" +import { TicketStatusBadge } from "@/components/tickets/status-badge" -export function RecentTicketsPanel() { - const ticketsRaw = useQuery(api.tickets.list, { tenantId: DEFAULT_TENANT_ID, limit: 10 }); - if (ticketsRaw === undefined) { - return ( -
-
- {Array.from({ length: 4 }).map((_, i) => ( -
-
-
-
- ))} +const metaBadgeClass = + "inline-flex items-center gap-1 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-[11px] font-semibold text-neutral-700" + +const channelLabel: Record = { + EMAIL: "E-mail", + WHATSAPP: "WhatsApp", + CHAT: "Chat", + PHONE: "Telefone", + API: "API", + MANUAL: "Manual", +} + +function TicketRow({ ticket }: { ticket: Ticket }) { + return ( +
+
+
+
+ #{ticket.reference} + + {ticket.queue ?? "Sem fila"} + +
+
+ + {ticket.subject} + +

{ticket.summary ?? "Sem resumo"}

+
+
+ {ticket.requester.name} + + {formatDistanceToNow(ticket.updatedAt, { addSuffix: true, locale: ptBR })} +
+
+
+ + +
+ {channelLabel[ticket.channel] ?? ticket.channel} + {ticket.assignee?.name ?? "Sem responsável"} + {ticket.category ? ( + + {ticket.category.name} + {ticket.subcategory ? ` • ${ticket.subcategory.name}` : ""} + + ) : ( + Sem categoria + )} +
- ); - } - const tickets = mapTicketsFromServerList((ticketsRaw ?? []) as unknown[]); - return ( -
-
- ); + ) +} + +export function RecentTicketsPanel() { + const ticketsRaw = useQuery(api.tickets.list, { tenantId: DEFAULT_TENANT_ID, limit: 6 }) + + if (ticketsRaw === undefined) { + return ( + + + Últimos chamados + + + {Array.from({ length: 4 }).map((_, index) => ( +
+ + +
+ ))} +
+
+ ) + } + + const tickets = mapTicketsFromServerList((ticketsRaw ?? []) as unknown[]).slice(0, 6) + + return ( + + + Últimos chamados + + + + {tickets.length === 0 ? ( +
+ Nenhum ticket recente encontrado. +
+ ) : ( + tickets.map((ticket) => ) + )} +
+
+ ) } From d73b1e44685b07355212b43dd6bd8b8c7191ea34 Mon Sep 17 00:00:00 2001 From: esdrasrenan Date: Sun, 5 Oct 2025 02:19:21 -0300 Subject: [PATCH 3/6] style: align badges in dashboard tickets panel Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .../tickets/recent-tickets-panel.tsx | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/web/src/components/tickets/recent-tickets-panel.tsx b/web/src/components/tickets/recent-tickets-panel.tsx index a41b65f..67ffb74 100644 --- a/web/src/components/tickets/recent-tickets-panel.tsx +++ b/web/src/components/tickets/recent-tickets-panel.tsx @@ -31,7 +31,7 @@ const channelLabel: Record = { function TicketRow({ ticket }: { ticket: Ticket }) { return (
-
+
#{ticket.reference} @@ -51,19 +51,25 @@ function TicketRow({ ticket }: { ticket: Ticket }) { {formatDistanceToNow(ticket.updatedAt, { addSuffix: true, locale: ptBR })}
-
- - -
- {channelLabel[ticket.channel] ?? ticket.channel} - {ticket.assignee?.name ?? "Sem responsável"} +
+
+ + +
+
+
+ {channelLabel[ticket.channel] ?? ticket.channel} + {ticket.assignee?.name ?? "Sem responsável"} +
{ticket.category ? ( - + {ticket.category.name} {ticket.subcategory ? ` • ${ticket.subcategory.name}` : ""} ) : ( - Sem categoria + + Sem categoria + )}
From de7314cff10ccb949781390cfcf08f52f2cdc594 Mon Sep 17 00:00:00 2001 From: esdrasrenan Date: Sun, 5 Oct 2025 02:22:12 -0300 Subject: [PATCH 4/6] style: reposition category badge in dashboard tickets Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .../tickets/recent-tickets-panel.tsx | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/web/src/components/tickets/recent-tickets-panel.tsx b/web/src/components/tickets/recent-tickets-panel.tsx index 67ffb74..0b77068 100644 --- a/web/src/components/tickets/recent-tickets-panel.tsx +++ b/web/src/components/tickets/recent-tickets-panel.tsx @@ -50,27 +50,25 @@ function TicketRow({ ticket }: { ticket: Ticket }) { {formatDistanceToNow(ticket.updatedAt, { addSuffix: true, locale: ptBR })}
+ {ticket.category ? ( + + {ticket.category.name} + {ticket.subcategory ? ` • ${ticket.subcategory.name}` : ""} + + ) : ( + + Sem categoria + + )}
-
+
-
-
- {channelLabel[ticket.channel] ?? ticket.channel} - {ticket.assignee?.name ?? "Sem responsável"} -
- {ticket.category ? ( - - {ticket.category.name} - {ticket.subcategory ? ` • ${ticket.subcategory.name}` : ""} - - ) : ( - - Sem categoria - - )} +
+ {channelLabel[ticket.channel] ?? ticket.channel} + {ticket.assignee?.name ?? "Sem responsável"}
From 9b16f3cd1eeff808adb1d666426a9f5cbb7f39a5 Mon Sep 17 00:00:00 2001 From: esdrasrenan Date: Sun, 5 Oct 2025 02:27:39 -0300 Subject: [PATCH 5/6] feat: animate realtime recent tickets panel Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- web/src/app/globals.css | 15 ++++++ .../tickets/recent-tickets-panel.tsx | 48 +++++++++++++++++-- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/web/src/app/globals.css b/web/src/app/globals.css index 2f2ba1d..400c7d2 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -137,4 +137,19 @@ .rich-text h3 { @apply text-base font-semibold my-2; } .rich-text code { @apply rounded bg-muted px-1 py-0.5 text-xs; } .rich-text pre { @apply my-3 overflow-x-auto rounded bg-muted p-3 text-xs; } + + @keyframes recent-ticket-enter { + 0% { + opacity: 0; + transform: translateY(-12px); + } + 100% { + opacity: 1; + transform: translateY(0); + } + } + + .recent-ticket-enter { + animation: recent-ticket-enter 0.45s ease-out; + } } diff --git a/web/src/components/tickets/recent-tickets-panel.tsx b/web/src/components/tickets/recent-tickets-panel.tsx index 0b77068..d65850c 100644 --- a/web/src/components/tickets/recent-tickets-panel.tsx +++ b/web/src/components/tickets/recent-tickets-panel.tsx @@ -1,5 +1,6 @@ "use client" +import { useEffect, useMemo, useRef, useState } from "react" import Link from "next/link" import { formatDistanceToNow } from "date-fns" import { ptBR } from "date-fns/locale" @@ -28,9 +29,11 @@ const channelLabel: Record = { MANUAL: "Manual", } -function TicketRow({ ticket }: { ticket: Ticket }) { +function TicketRow({ ticket, entering }: { ticket: Ticket; entering: boolean }) { return ( -
+
@@ -78,6 +81,41 @@ function TicketRow({ ticket }: { ticket: Ticket }) { export function RecentTicketsPanel() { const ticketsRaw = useQuery(api.tickets.list, { tenantId: DEFAULT_TENANT_ID, limit: 6 }) + const [enteringId, setEnteringId] = useState(null) + const previousIdsRef = useRef([]) + + const tickets = useMemo( + () => mapTicketsFromServerList((ticketsRaw ?? []) as unknown[]).slice(0, 6), + [ticketsRaw] + ) + + useEffect(() => { + if (ticketsRaw === undefined) { + previousIdsRef.current = [] + return + } + const ids = tickets.map((ticket) => ticket.id) + const previous = previousIdsRef.current + if (!ids.length) { + previousIdsRef.current = ids + return + } + if (!previous.length) { + previousIdsRef.current = ids + return + } + const topId = ids[0] + if (!previous.includes(topId)) { + setEnteringId(topId) + } + previousIdsRef.current = ids + }, [tickets, ticketsRaw]) + + useEffect(() => { + if (!enteringId) return + const timer = window.setTimeout(() => setEnteringId(null), 600) + return () => window.clearTimeout(timer) + }, [enteringId]) if (ticketsRaw === undefined) { return ( @@ -97,8 +135,6 @@ export function RecentTicketsPanel() { ) } - const tickets = mapTicketsFromServerList((ticketsRaw ?? []) as unknown[]).slice(0, 6) - return ( @@ -113,7 +149,9 @@ export function RecentTicketsPanel() { Nenhum ticket recente encontrado.
) : ( - tickets.map((ticket) => ) + tickets.map((ticket) => ( + + )) )} From c5864dbefdfb801d10e2f4051893edc3c90d6c2b Mon Sep 17 00:00:00 2001 From: esdrasrenan Date: Sun, 5 Oct 2025 03:12:53 -0300 Subject: [PATCH 6/6] feat: refine ticket creation and comments experience Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- web/src/app/globals.css | 10 + .../components/tickets/category-select.tsx | 6 +- .../components/tickets/new-ticket-dialog.tsx | 323 ++++++++++-------- .../tickets/ticket-comments.rich.tsx | 2 +- web/src/components/tickets/tickets-table.tsx | 34 +- 5 files changed, 216 insertions(+), 159 deletions(-) diff --git a/web/src/app/globals.css b/web/src/app/globals.css index 400c7d2..fcd5555 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -138,6 +138,16 @@ .rich-text code { @apply rounded bg-muted px-1 py-0.5 text-xs; } .rich-text pre { @apply my-3 overflow-x-auto rounded bg-muted p-3 text-xs; } + .rich-text .ProseMirror.is-editor-empty::before, + .rich-text .ProseMirror p.is-editor-empty:first-child::before { + color: #94a3b8; + content: attr(data-placeholder); + pointer-events: none; + height: 0; + float: left; + font-weight: 400; + } + @keyframes recent-ticket-enter { 0% { opacity: 0; diff --git a/web/src/components/tickets/category-select.tsx b/web/src/components/tickets/category-select.tsx index 522fdcf..679e860 100644 --- a/web/src/components/tickets/category-select.tsx +++ b/web/src/components/tickets/category-select.tsx @@ -25,6 +25,7 @@ interface CategorySelectProps { subcategoryLabel?: string className?: string secondaryEmptyLabel?: string + layout?: "grid" | "stacked" } function findCategory(categories: TicketCategory[], categoryId: string | null) { @@ -44,6 +45,7 @@ export function CategorySelectFields({ subcategoryLabel = "Secundária", secondaryEmptyLabel = "Selecione uma categoria primária", className, + layout = "grid", }: CategorySelectProps) { const { categories, isLoading } = useTicketCategories(tenantId) const activeCategory = useMemo(() => findCategory(categories, categoryId), [categories, categoryId]) @@ -74,8 +76,10 @@ export function CategorySelectFields({ } }, [categoryId, secondaryOptions, subcategoryId, onSubcategoryChange]) + const containerClass = layout === "stacked" ? "flex flex-col gap-3" : "grid gap-3 sm:grid-cols-2" + return ( -
+