Portal polishing: hide queue/priority for customers; use RTE + attachments in detail; filter list to requester only for collaborators

This commit is contained in:
Esdras Renan 2025-10-14 22:29:38 -03:00
parent d1871ba232
commit 6df49ba956
3 changed files with 32 additions and 28 deletions

View file

@ -7,6 +7,7 @@ import Link from "next/link"
import { Tag } from "lucide-react"
import type { Ticket } from "@/lib/schemas/ticket"
import { useAuth } from "@/lib/auth-client"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { cn } from "@/lib/utils"
@ -44,6 +45,7 @@ interface PortalTicketCardProps {
}
export function PortalTicketCard({ ticket }: PortalTicketCardProps) {
const { isCustomer } = useAuth()
const updatedAgo = formatDistanceToNow(ticket.updatedAt, {
addSuffix: true,
locale: ptBR,
@ -68,16 +70,20 @@ export function PortalTicketCard({ ticket }: PortalTicketCardProps) {
<Badge className={cn("rounded-full px-3 py-1 text-xs font-semibold uppercase", statusTone[ticket.status])}>
{statusLabel[ticket.status]}
</Badge>
<Badge className={cn("rounded-full px-3 py-1 text-xs font-semibold uppercase", priorityTone[ticket.priority])}>
{priorityLabel[ticket.priority]}
</Badge>
{!isCustomer ? (
<Badge className={cn("rounded-full px-3 py-1 text-xs font-semibold uppercase", priorityTone[ticket.priority])}>
{priorityLabel[ticket.priority]}
</Badge>
) : null}
</div>
</CardHeader>
<CardContent className="flex flex-wrap items-center justify-between gap-4 border-t border-slate-100 px-5 py-4 text-sm text-neutral-600">
<div className="flex flex-col gap-1">
<span className="text-xs uppercase tracking-wide text-neutral-500">Fila</span>
<span className="font-medium text-neutral-800">{ticket.queue ?? "Sem fila"}</span>
</div>
{!isCustomer ? (
<div className="flex flex-col gap-1">
<span className="text-xs uppercase tracking-wide text-neutral-500">Fila</span>
<span className="font-medium text-neutral-800">{ticket.queue ?? "Sem fila"}</span>
</div>
) : null}
<div className="flex flex-col gap-1">
<span className="text-xs uppercase tracking-wide text-neutral-500">Status</span>
<span className="font-medium text-neutral-800">{statusLabel[ticket.status]}</span>

View file

@ -14,12 +14,12 @@ import type { TicketWithDetails } from "@/lib/schemas/ticket"
import { useAuth } from "@/lib/auth-client"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Dropzone } from "@/components/ui/dropzone"
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"
import { Skeleton } from "@/components/ui/skeleton"
import { Textarea } from "@/components/ui/textarea"
import { RichTextEditor } from "@/components/ui/textarea"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { sanitizeEditorHtml } from "@/components/ui/rich-text-editor"
import { sanitizeEditorHtml, RichTextEditor } from "@/components/ui/rich-text-editor"
const statusLabel: Record<TicketWithDetails["status"], string> = {
PENDING: "Pendente",
@ -69,7 +69,7 @@ interface PortalTicketDetailProps {
export function PortalTicketDetail({ ticketId }: PortalTicketDetailProps) {
const { convexUserId, session } = useAuth()
const addComment = useMutation(api.tickets.addComment)
const [comment, setComment] = useState("")
const [comment, setComment] = useState(""); const [attachments, setAttachments] = useState<Array<{ storageId: string; name: string; size?: number; type?: string }>>([])
const ticketRaw = useQuery(
api.tickets.getById,
@ -133,7 +133,7 @@ export function PortalTicketDetail({ ticketId }: PortalTicketDetailProps) {
authorId: convexUserId as Id<"users">,
visibility: "PUBLIC",
body: htmlBody,
attachments: [],
attachments: attachments.map((f) => ({ storageId: f.storageId as Id<"_storage">, name: f.name, size: f.size, type: f.type, })),
})
setComment("")
toast.success("Comentário enviado!", { id: toastId })
@ -186,22 +186,7 @@ export function PortalTicketDetail({ ticketId }: PortalTicketDetailProps) {
</CardHeader>
<CardContent className="space-y-6 px-5 pb-6">
<form onSubmit={handleSubmit} className="space-y-3">
<label htmlFor="comment" className="text-sm font-medium text-neutral-800">
Enviar uma mensagem para a equipe
</label>
<Textarea
id="comment"
value={comment}
onChange={(event) => setComment(event.target.value)}
placeholder="Descreva o que aconteceu, envie atualizações ou compartilhe novas informações."
className="min-h-[120px] resize-y rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-neutral-800 shadow-sm focus-visible:border-neutral-900 focus-visible:ring-neutral-900/20"
/>
<div className="flex justify-end">
<Button type="submit" className="rounded-full bg-neutral-900 px-6 text-sm font-semibold text-white hover:bg-neutral-900/90">
Enviar comentário
</Button>
</div>
</form>
<label htmlFor="comment" className="text-sm font-medium text-neutral-800">\n Enviar uma mensagem para a equipe\n </label>\n <RichTextEditor\n value={comment}\n onChange={(html) => setComment(html)}\n placeholder="Descreva o que aconteceu, envie atualizações ou compartilhe novas informações."\n className="rounded-2xl border border-slate-200 shadow-sm focus-within:border-neutral-900 focus-within:ring-neutral-900/20"\n />\n <div>\n <Dropzone\n onUploaded={(files) => setAttachments((prev) => [...prev, ...files])}\n className="rounded-xl border border-dashed border-slate-300 bg-slate-50 px-3 py-4 text-sm text-neutral-600 shadow-inner"\n />\n <p className="mt-1 text-xs text-neutral-500">Máximo 10MB Até 5 arquivos</p>\n </div>\n <div className="flex justify-end">\n <Button type="submit" className="rounded-full bg-neutral-900 px-6 text-sm font-semibold text-white hover:bg-neutral-900/90">\n Enviar comentário\n </Button>\n </div>\n </form>
<div className="space-y-5">
{ticket.comments.length === 0 ? (
@ -300,3 +285,9 @@ function DetailItem({ label, value, subtitle }: DetailItemProps) {
</div>
)
}