feat: link tickets in comments and align admin sidebars
This commit is contained in:
parent
c35eb673d3
commit
b0f57009ac
15 changed files with 1606 additions and 424 deletions
|
|
@ -42,6 +42,7 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
|
|||
const isManager = normalizedRole === "manager"
|
||||
const canStaffComment = hasAssignee || isManager
|
||||
const canComment = isRequester || (isStaff && canStaffComment)
|
||||
const allowTicketMentions = normalizedRole === "admin" || normalizedRole === "agent"
|
||||
const addComment = useMutation(api.tickets.addComment)
|
||||
const removeAttachment = useMutation(api.tickets.removeCommentAttachment)
|
||||
const updateComment = useMutation(api.tickets.updateComment)
|
||||
|
|
@ -303,6 +304,7 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
|
|||
}
|
||||
disabled={savingCommentId === commentId}
|
||||
placeholder="Edite o comentário..."
|
||||
ticketMention={{ enabled: allowTicketMentions }}
|
||||
/>
|
||||
<div className="mt-3 flex items-center justify-end gap-2">
|
||||
<Button
|
||||
|
|
@ -381,7 +383,14 @@ export function TicketComments({ ticket }: TicketCommentsProps) {
|
|||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit} className="mt-4 space-y-3">
|
||||
<RichTextEditor value={body} onChange={setBody} placeholder="Escreva um comentário..." className="rounded-2xl border border-slate-200" disabled={!canComment} />
|
||||
<RichTextEditor
|
||||
value={body}
|
||||
onChange={setBody}
|
||||
placeholder="Escreva um comentário..."
|
||||
className="rounded-2xl border border-slate-200"
|
||||
disabled={!canComment}
|
||||
ticketMention={{ enabled: allowTicketMentions }}
|
||||
/>
|
||||
<Dropzone
|
||||
onUploaded={(files) => setAttachmentsToSend((prev) => [...prev, ...files])}
|
||||
currentFileCount={attachmentsToSend.length}
|
||||
|
|
|
|||
|
|
@ -208,6 +208,8 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
const [pausing, setPausing] = useState(false)
|
||||
const [exportingPdf, setExportingPdf] = useState(false)
|
||||
const [closeOpen, setCloseOpen] = useState(false)
|
||||
const [assigneeChangeReason, setAssigneeChangeReason] = useState("")
|
||||
const [assigneeReasonError, setAssigneeReasonError] = useState<string | null>(null)
|
||||
const selectedCategoryId = categorySelection.categoryId
|
||||
const selectedSubcategoryId = categorySelection.subcategoryId
|
||||
const dirty = useMemo(
|
||||
|
|
@ -321,12 +323,25 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
setAssigneeSelection(currentAssigneeId)
|
||||
throw new Error("assignee-not-allowed")
|
||||
}
|
||||
const reasonValue = assigneeChangeReason.trim()
|
||||
if (reasonValue.length < 5) {
|
||||
setAssigneeReasonError("Descreva o motivo com pelo menos 5 caracteres.")
|
||||
toast.error("Informe um motivo para registrar a troca do responsável.", { id: "assignee" })
|
||||
return
|
||||
}
|
||||
if (reasonValue.length > 1000) {
|
||||
setAssigneeReasonError("Use no máximo 1.000 caracteres.")
|
||||
toast.error("Reduza o motivo para até 1.000 caracteres.", { id: "assignee" })
|
||||
return
|
||||
}
|
||||
setAssigneeReasonError(null)
|
||||
toast.loading("Atualizando responsável...", { id: "assignee" })
|
||||
try {
|
||||
await changeAssignee({
|
||||
ticketId: ticket.id as Id<"tickets">,
|
||||
assigneeId: assigneeSelection as Id<"users">,
|
||||
actorId: convexUserId as Id<"users">,
|
||||
reason: reasonValue,
|
||||
})
|
||||
toast.success("Responsável atualizado!", { id: "assignee" })
|
||||
if (assigneeSelection) {
|
||||
|
|
@ -341,6 +356,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
})
|
||||
}
|
||||
}
|
||||
setAssigneeChangeReason("")
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
toast.error("Não foi possível atualizar o responsável.", { id: "assignee" })
|
||||
|
|
@ -387,11 +403,15 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
})
|
||||
setQueueSelection(currentQueueName)
|
||||
setAssigneeSelection(currentAssigneeId)
|
||||
setAssigneeChangeReason("")
|
||||
setAssigneeReasonError(null)
|
||||
setEditing(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (editing) return
|
||||
setAssigneeChangeReason("")
|
||||
setAssigneeReasonError(null)
|
||||
setCategorySelection({
|
||||
categoryId: ticket.category?.id ?? "",
|
||||
subcategoryId: ticket.subcategory?.id ?? "",
|
||||
|
|
@ -1097,6 +1117,28 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
<span className={sectionValueClass}>{assigneeState?.name ?? "Não atribuído"}</span>
|
||||
)}
|
||||
</div>
|
||||
{editing && assigneeDirty ? (
|
||||
<div className="flex flex-col gap-2 sm:col-span-2 lg:col-span-3">
|
||||
<span className={sectionLabelClass}>Motivo da troca</span>
|
||||
<Textarea
|
||||
value={assigneeChangeReason}
|
||||
onChange={(event) => {
|
||||
setAssigneeChangeReason(event.target.value)
|
||||
if (assigneeReasonError) {
|
||||
setAssigneeReasonError(null)
|
||||
}
|
||||
}}
|
||||
placeholder="Explique brevemente por que o chamado será reatribuído..."
|
||||
className="min-h-[96px]"
|
||||
/>
|
||||
<p className="text-xs text-neutral-500">
|
||||
O motivo é registrado como comentário interno visível para administradores e agentes.
|
||||
</p>
|
||||
{assigneeReasonError ? (
|
||||
<p className="text-xs font-semibold text-rose-600">{assigneeReasonError}</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className={sectionLabelClass}>Criado em</span>
|
||||
<span className={sectionValueClass}>{format(ticket.createdAt, "dd/MM/yyyy HH:mm", { locale: ptBR })}</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue