fix(select): evitar value vazio em Select (fila) no novo ticket; feat(timeline): nomes/avatares do ator e mensagens PT-BR; feat(ui): skeletons para lista e recentes; grid de anexos e legendas; melhorias de cards/padding
This commit is contained in:
parent
44c98fec4a
commit
da1633a30e
6 changed files with 59 additions and 23 deletions
|
|
@ -217,10 +217,11 @@ export const create = mutation({
|
||||||
slaPolicyId: undefined,
|
slaPolicyId: undefined,
|
||||||
dueAt: undefined,
|
dueAt: undefined,
|
||||||
});
|
});
|
||||||
|
const requester = await ctx.db.get(args.requesterId);
|
||||||
await ctx.db.insert("ticketEvents", {
|
await ctx.db.insert("ticketEvents", {
|
||||||
ticketId: id,
|
ticketId: id,
|
||||||
type: "CREATED",
|
type: "CREATED",
|
||||||
payload: { requesterId: args.requesterId },
|
payload: { requesterId: args.requesterId, requesterName: requester?.name, requesterAvatar: requester?.avatarUrl },
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
});
|
});
|
||||||
return id;
|
return id;
|
||||||
|
|
|
||||||
|
|
@ -125,18 +125,24 @@ export function NewTicketDialog() {
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel>Fila</FieldLabel>
|
<FieldLabel>Fila</FieldLabel>
|
||||||
<Select value={form.watch("queueName") ?? ""} onValueChange={(v) => form.setValue("queueName", v || null)}>
|
{(() => {
|
||||||
<SelectTrigger><SelectValue placeholder="Sem fila" /></SelectTrigger>
|
const NONE = "NONE";
|
||||||
<SelectContent>
|
const current = form.watch("queueName") ?? NONE;
|
||||||
<SelectItem value="">Sem fila</SelectItem>
|
return (
|
||||||
{queues.map((q: any) => (
|
<Select value={current} onValueChange={(v) => form.setValue("queueName", v === NONE ? null : v)}>
|
||||||
<SelectItem key={q.id} value={q.name}>{q.name}</SelectItem>
|
<SelectTrigger><SelectValue placeholder="Sem fila" /></SelectTrigger>
|
||||||
))}
|
<SelectContent>
|
||||||
</SelectContent>
|
<SelectItem value={NONE}>Sem fila</SelectItem>
|
||||||
</Select>
|
{queues.map((q: any) => (
|
||||||
</Field>
|
<SelectItem key={q.id} value={q.name}>{q.name}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,15 @@ export function RecentTicketsPanel() {
|
||||||
const ticketsRaw = useQuery(api.tickets.list, { tenantId: DEFAULT_TENANT_ID, limit: 10 });
|
const ticketsRaw = useQuery(api.tickets.list, { tenantId: DEFAULT_TENANT_ID, limit: 10 });
|
||||||
if (ticketsRaw === undefined) {
|
if (ticketsRaw === undefined) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border bg-card p-6 text-sm text-muted-foreground">
|
<div className="rounded-xl border bg-card p-4">
|
||||||
<Spinner className="me-2" /> Carregando tickets…
|
<div className="grid gap-3">
|
||||||
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
|
<div key={i} className="flex items-center justify-between gap-3">
|
||||||
|
<div className="h-4 w-56 animate-pulse rounded bg-muted" />
|
||||||
|
<div className="h-4 w-20 animate-pulse rounded bg-muted" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
|
||||||
{comment.body}
|
{comment.body}
|
||||||
</div>
|
</div>
|
||||||
{comment.attachments?.length ? (
|
{comment.attachments?.length ? (
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="grid max-w-xl grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-3">
|
||||||
{comment.attachments.map((a) => {
|
{comment.attachments.map((a) => {
|
||||||
const att = a as any
|
const att = a as any
|
||||||
const isImg = (att?.url ?? "").match(/\.(png|jpe?g|gif|webp|svg)$/i)
|
const isImg = (att?.url ?? "").match(/\.(png|jpe?g|gif|webp|svg)$/i)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
import type { TicketWithDetails } from "@/lib/schemas/ticket"
|
import type { TicketWithDetails } from "@/lib/schemas/ticket"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
|
||||||
const timelineIcons: Record<string, ComponentType<{ className?: string }>> = {
|
const timelineIcons: Record<string, ComponentType<{ className?: string }>> = {
|
||||||
|
|
@ -52,6 +53,17 @@ export function TicketTimeline({ ticket }: TicketTimelineProps) {
|
||||||
<span className="text-sm font-medium text-foreground">
|
<span className="text-sm font-medium text-foreground">
|
||||||
{timelineLabels[entry.type] ?? entry.type}
|
{timelineLabels[entry.type] ?? entry.type}
|
||||||
</span>
|
</span>
|
||||||
|
{entry.payload?.actorName ? (
|
||||||
|
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||||
|
<Avatar className="size-5">
|
||||||
|
<AvatarImage src={entry.payload.actorAvatar} alt={entry.payload.actorName} />
|
||||||
|
<AvatarFallback>
|
||||||
|
{entry.payload.actorName.split(' ').slice(0,2).map((p:string)=>p[0]).join('').toUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
por {entry.payload.actorName}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{format(entry.createdAt, "dd MMM yyyy HH:mm", { locale: ptBR })}
|
{format(entry.createdAt, "dd MMM yyyy HH:mm", { locale: ptBR })}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -62,6 +74,7 @@ export function TicketTimeline({ ticket }: TicketTimelineProps) {
|
||||||
if (entry.type === "STATUS_CHANGED" && (p.toLabel || p.to)) message = `Status alterado para ${p.toLabel || p.to}`
|
if (entry.type === "STATUS_CHANGED" && (p.toLabel || p.to)) message = `Status alterado para ${p.toLabel || p.to}`
|
||||||
if (entry.type === "ASSIGNEE_CHANGED" && (p.assigneeName || p.assigneeId)) message = `Responsável alterado${p.assigneeName ? ` para ${p.assigneeName}` : ""}`
|
if (entry.type === "ASSIGNEE_CHANGED" && (p.assigneeName || p.assigneeId)) message = `Responsável alterado${p.assigneeName ? ` para ${p.assigneeName}` : ""}`
|
||||||
if (entry.type === "QUEUE_CHANGED" && (p.queueName || p.queueId)) message = `Fila alterada${p.queueName ? ` para ${p.queueName}` : ""}`
|
if (entry.type === "QUEUE_CHANGED" && (p.queueName || p.queueId)) message = `Fila alterada${p.queueName ? ` para ${p.queueName}` : ""}`
|
||||||
|
if (entry.type === "CREATED" && (p.requesterName)) message = `Criado por ${p.requesterName}`
|
||||||
if (!message) return null
|
if (!message) return null
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-dashed bg-card px-3 py-2 text-sm text-muted-foreground">
|
<div className="rounded-lg border border-dashed bg-card px-3 py-2 text-sm text-muted-foreground">
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,16 @@ export function TicketsView() {
|
||||||
<div className="flex flex-col gap-6 px-4 lg:px-6">
|
<div className="flex flex-col gap-6 px-4 lg:px-6">
|
||||||
<TicketsFilters onChange={setFilters} queues={(queues ?? []).map((q: any) => q.name)} />
|
<TicketsFilters onChange={setFilters} queues={(queues ?? []).map((q: any) => q.name)} />
|
||||||
{ticketsRaw === undefined ? (
|
{ticketsRaw === undefined ? (
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground"><Spinner /> Carregando tickets…</div>
|
<div className="rounded-xl border bg-card p-4">
|
||||||
|
<div className="grid gap-3">
|
||||||
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
|
<div key={i} className="flex items-center justify-between gap-3">
|
||||||
|
<div className="h-4 w-48 animate-pulse rounded bg-muted" />
|
||||||
|
<div className="h-4 w-24 animate-pulse rounded bg-muted" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<TicketsTable tickets={filteredTickets as any} />
|
<TicketsTable tickets={filteredTickets as any} />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue