Portal polishing: hide queue/priority for customers; use RTE + attachments in detail; filter list to requester only for collaborators
This commit is contained in:
parent
d1871ba232
commit
6df49ba956
3 changed files with 32 additions and 28 deletions
|
|
@ -268,6 +268,13 @@ export const list = query({
|
|||
.query("tickets")
|
||||
.withIndex("by_tenant_queue", (q) => q.eq("tenantId", args.tenantId).eq("queueId", args.queueId!))
|
||||
.collect();
|
||||
} else if (role === "COLLABORATOR") {
|
||||
// Colaborador: exibir apenas tickets onde ele é o solicitante
|
||||
const all = await ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant", (q) => q.eq("tenantId", args.tenantId))
|
||||
.collect()
|
||||
base = all.filter((t) => t.requesterId === args.viewerId)
|
||||
} else {
|
||||
base = await ctx.db
|
||||
.query("tickets")
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue