Implement company provisioning codes and session tweaks

This commit is contained in:
Esdras Renan 2025-10-15 20:45:25 -03:00
parent 0fb9bf59b2
commit 2cba553efa
28 changed files with 1407 additions and 534 deletions

View file

@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { formatDistanceToNow } from "date-fns"
import { ptBR } from "date-fns/locale"
import { IconLock, IconMessage, IconFileText } from "@tabler/icons-react"
import { FileIcon, Image as ImageIcon, PencilLine, Trash2, X } from "lucide-react"
import { Download, FileIcon, Image as ImageIcon, PencilLine, Trash2, X } from "lucide-react"
import { useAction, useMutation, useQuery } from "convex/react"
import { api } from "@/convex/_generated/api"
import { useAuth } from "@/lib/auth-client"
@ -43,7 +43,7 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
const [attachmentsToSend, setAttachmentsToSend] = useState<Array<{ storageId: string; name: string; size?: number; type?: string; previewUrl?: string }>>([])
const [preview, setPreview] = useState<string | null>(null)
const [pending, setPending] = useState<Pick<TicketWithDetails["comments"][number], "id" | "author" | "visibility" | "body" | "attachments" | "createdAt" | "updatedAt">[]>([])
const [visibility, setVisibility] = useState<"PUBLIC" | "INTERNAL">("PUBLIC")
const [visibility, setVisibility] = useState<"PUBLIC" | "INTERNAL">("INTERNAL")
const [attachmentToRemove, setAttachmentToRemove] = useState<{ commentId: string; attachmentId: string; name: string } | null>(null)
const [removingAttachment, setRemovingAttachment] = useState(false)
const [editingComment, setEditingComment] = useState<{ id: string; value: string } | null>(null)
@ -228,16 +228,17 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
const canEdit = Boolean(convexUserId && String(comment.author.id) === convexUserId && !isPending)
const hasBody = bodyPlain.length > 0 || isEditing
const isInternal = comment.visibility === "INTERNAL" && canSeeInternalComments
const containerClass = isInternal
const isPublic = comment.visibility === "PUBLIC"
const containerClass = isPublic
? "group/comment flex gap-3 rounded-2xl border border-amber-200/80 bg-amber-50/80 px-3 py-3 shadow-[0_0_0_1px_rgba(217,119,6,0.15)]"
: "group/comment flex gap-3"
const bodyClass = isInternal
: "group/comment flex gap-3 rounded-2xl border border-slate-200 bg-white px-3 py-3"
const bodyClass = isPublic
? "relative break-words rounded-xl border border-amber-200/80 bg-white px-3 py-2 text-sm leading-relaxed text-neutral-700 shadow-[0_0_0_1px_rgba(217,119,6,0.08)]"
: "relative break-words rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm leading-relaxed text-neutral-700"
const bodyEditButtonClass = isInternal
const bodyEditButtonClass = isPublic
? "absolute right-2 top-2 inline-flex size-7 items-center justify-center rounded-full border border-amber-200 bg-white text-amber-700 opacity-0 transition hover:border-amber-500 hover:text-amber-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/30 group-hover/comment:opacity-100"
: "absolute right-2 top-2 inline-flex size-7 items-center justify-center rounded-full border border-slate-300 bg-white text-neutral-700 opacity-0 transition hover:border-black hover:text-black focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-black/20 group-hover/comment:opacity-100"
const addContentButtonClass = isInternal
const addContentButtonClass = isPublic
? "inline-flex items-center gap-2 text-sm font-medium text-amber-700 transition hover:text-amber-900"
: "inline-flex items-center gap-2 text-sm font-medium text-neutral-600 transition hover:text-neutral-900"
@ -263,6 +264,10 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
<span className="text-xs font-semibold tracking-wide text-amber-700/80">
Comentário interno visível apenas para administradores e agentes
</span>
) : comment.visibility === "PUBLIC" ? (
<span className="text-xs font-semibold tracking-wide text-amber-700/80">
Comentário visível para o cliente
</span>
) : null}
{isEditing ? (
<div
@ -596,10 +601,20 @@ function CommentAttachmentCard({
const handleDownload = useCallback(async () => {
const target = url ?? (await ensureUrl())
if (target) {
if (!target) return
try {
const link = document.createElement("a")
link.href = target
link.download = attachment.name ?? "anexo"
link.rel = "noopener noreferrer"
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} catch (error) {
console.error("Failed to download attachment", error)
window.open(target, "_blank", "noopener,noreferrer")
}
}, [ensureUrl, url])
}, [attachment.name, ensureUrl, url])
const name = attachment.name ?? ""
const urlLooksImage = url ? /\.(png|jpe?g|gif|webp|svg)$/i.test(url) : false
@ -642,6 +657,14 @@ function CommentAttachmentCard({
</span>
</button>
)}
<button
type="button"
onClick={handleDownload}
aria-label={`Baixar ${name}`}
className="absolute left-1.5 top-1.5 inline-flex size-7 items-center justify-center rounded-full border border-slate-300 bg-white text-neutral-700 opacity-0 transition hover:border-black hover:text-black focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-black/20 focus-visible:opacity-100 group-hover:opacity-100"
>
<Download className="size-3.5" />
</button>
<button
type="button"
onClick={onRequestRemoval}