feat(ui,tickets): aplicar visual Rever (badges revertidas), header com play/pause, edição inline com cancelar, empty states e toasts centralizados; novas mutations Convex (updateSubject/updateSummary/toggleWork)
This commit is contained in:
parent
881bb7bfdd
commit
6c57c691f3
14 changed files with 512 additions and 307 deletions
|
|
@ -21,6 +21,7 @@ import { Dropzone } from "@/components/ui/dropzone"
|
|||
import { RichTextEditor, RichTextContent, sanitizeEditorHtml } from "@/components/ui/rich-text-editor"
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"
|
||||
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"
|
||||
|
||||
interface TicketCommentsProps {
|
||||
ticket: TicketWithDetails
|
||||
|
|
@ -57,7 +58,7 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
|
|||
setPending((p) => [optimistic, ...p])
|
||||
setBody("")
|
||||
setAttachmentsToSend([])
|
||||
toast.loading("Enviando comentário.", { id: "comment" })
|
||||
toast.loading("Enviando comentário...", { id: "comment" })
|
||||
try {
|
||||
const typedAttachments = attachments.map((a) => ({
|
||||
storageId: a.storageId as unknown as Id<"_storage">,
|
||||
|
|
@ -83,9 +84,15 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
|
|||
</CardHeader>
|
||||
<CardContent className="space-y-6 px-4 pb-6">
|
||||
{commentsAll.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Ainda sem comentários. Que tal registrar o próximo passo?
|
||||
</p>
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<IconMessage className="size-5" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Nenhum comentário ainda</EmptyTitle>
|
||||
<EmptyDescription>Registre o próximo passo abaixo.</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
) : (
|
||||
commentsAll.map((comment) => {
|
||||
const initials = comment.author.name
|
||||
|
|
@ -111,38 +118,38 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
|
|||
{formatDistanceToNow(comment.createdAt, { addSuffix: true, locale: ptBR })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="rounded-lg border border-dashed bg-card px-3 py-2 text-sm leading-relaxed text-muted-foreground break-words">
|
||||
<div className="break-words rounded-lg border border-dashed bg-card px-3 py-2 text-sm leading-relaxed text-muted-foreground">
|
||||
<RichTextContent html={comment.body} />
|
||||
</div>
|
||||
{comment.attachments?.length ? (
|
||||
<div className="grid max-w-xl grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-3">
|
||||
{comment.attachments.map((att) => {
|
||||
const isImg = (att?.url ?? "").match(/\.(png|jpe?g|gif|webp|svg)$/i)
|
||||
if (isImg && att.url) {
|
||||
return (
|
||||
<button
|
||||
key={att.id}
|
||||
type="button"
|
||||
onClick={() => setPreview(att.url || null)}
|
||||
className="group overflow-hidden rounded-lg border bg-card p-0.5 hover:shadow"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={att.url} alt={att.name} className="h-24 w-24 rounded-md object-cover" />
|
||||
<div className="mt-1 line-clamp-1 w-24 text-ellipsis text-center text-[11px] text-muted-foreground">
|
||||
{att.name}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<a key={att.id} href={att.url} download={att.name} target="_blank" className="flex items-center gap-2 rounded-md border px-2 py-1 text-xs hover:bg-muted">
|
||||
<FileIcon className="size-3.5" /> {att.name}
|
||||
{att.url ? <Download className="size-3.5" /> : null}
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{comment.attachments?.length ? (
|
||||
<div className="grid max-w-xl grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-3">
|
||||
{comment.attachments.map((att) => {
|
||||
const isImg = (att?.url ?? "").match(/\.(png|jpe?g|gif|webp|svg)$/i)
|
||||
if (isImg && att.url) {
|
||||
return (
|
||||
<button
|
||||
key={att.id}
|
||||
type="button"
|
||||
onClick={() => setPreview(att.url || null)}
|
||||
className="group overflow-hidden rounded-lg border bg-card p-0.5 hover:shadow"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={att.url} alt={att.name} className="h-24 w-24 rounded-md object-cover" />
|
||||
<div className="mt-1 line-clamp-1 w-24 text-ellipsis text-center text-[11px] text-muted-foreground">
|
||||
{att.name}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<a key={att.id} href={att.url} download={att.name} target="_blank" className="flex items-center gap-2 rounded-md border px-2 py-1 text-xs hover:bg-muted">
|
||||
<FileIcon className="size-3.5" /> {att.name}
|
||||
{att.url ? <Download className="size-3.5" /> : null}
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -154,7 +161,7 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
|
|||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
Visibilidade:
|
||||
<Select value={visibility} onValueChange={(v) => setVisibility(v as "PUBLIC" | "INTERNAL")}>
|
||||
<Select value={visibility} onValueChange={(v) => setVisibility(v as "PUBLIC" | "INTERNAL")}>
|
||||
<SelectTrigger className="h-8 w-[140px]"><SelectValue placeholder="Visibilidade" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="PUBLIC">Pública</SelectItem>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue