feat: dispositivos e ajustes de csat e relatórios
This commit is contained in:
parent
25d2a9b062
commit
e0ef66555d
86 changed files with 5811 additions and 992 deletions
|
|
@ -142,6 +142,22 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
: machineAssignedName && machineAssignedName.length > 0
|
||||
? machineAssignedName
|
||||
: null
|
||||
const viewerId = convexUserId ?? null
|
||||
const viewerRole = (role ?? "").toLowerCase()
|
||||
const [status, setStatus] = useState<TicketStatus>(ticket.status)
|
||||
const reopenDeadline = ticket.reopenDeadline ?? null
|
||||
const isRequester = Boolean(ticket.requester?.id && viewerId && ticket.requester.id === viewerId)
|
||||
const reopenWindowActive = reopenDeadline ? reopenDeadline > Date.now() : false
|
||||
const canReopenTicket =
|
||||
status === "RESOLVED" && reopenWindowActive && (isStaff || viewerRole === "manager" || isRequester)
|
||||
const reopenDeadlineLabel = useMemo(() => {
|
||||
if (!reopenDeadline) return null
|
||||
try {
|
||||
return new Date(reopenDeadline).toLocaleString("pt-BR")
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}, [reopenDeadline])
|
||||
const viewerEmail = session?.user?.email ?? machineContext?.assignedUserEmail ?? null
|
||||
const viewerAvatar = session?.user?.avatarUrl ?? null
|
||||
const viewerAgentMeta = useMemo(
|
||||
|
|
@ -165,6 +181,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
const startWork = useMutation(api.tickets.startWork)
|
||||
const pauseWork = useMutation(api.tickets.pauseWork)
|
||||
const updateCategories = useMutation(api.tickets.updateCategories)
|
||||
const reopenTicket = useMutation(api.tickets.reopenTicket)
|
||||
const agents = (useQuery(api.users.listAgents, { tenantId: ticket.tenantId }) as Doc<"users">[] | undefined) ?? []
|
||||
const queuesEnabled = Boolean(isStaff && convexUserId)
|
||||
const companiesRemote = useQuery(
|
||||
|
|
@ -227,7 +244,6 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
| null
|
||||
| undefined
|
||||
|
||||
const [status, setStatus] = useState<TicketStatus>(ticket.status)
|
||||
const [assigneeState, setAssigneeState] = useState(ticket.assignee ?? null)
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [subject, setSubject] = useState(ticket.subject)
|
||||
|
|
@ -242,6 +258,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
const [assigneeSelection, setAssigneeSelection] = useState(currentAssigneeId)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [pauseDialogOpen, setPauseDialogOpen] = useState(false)
|
||||
const [isReopening, setIsReopening] = useState(false)
|
||||
const [pauseReason, setPauseReason] = useState<string>(PAUSE_REASONS[0]?.value ?? "NO_CONTACT")
|
||||
const [pauseNote, setPauseNote] = useState("")
|
||||
const [pausing, setPausing] = useState(false)
|
||||
|
|
@ -326,8 +343,8 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
const assigneeDirty = useMemo(() => assigneeSelection !== currentAssigneeId, [assigneeSelection, currentAssigneeId])
|
||||
const requesterDirty = useMemo(() => requesterSelection !== ticket.requester.id, [requesterSelection, ticket.requester.id])
|
||||
const formDirty = dirty || categoryDirty || queueDirty || assigneeDirty || requesterDirty
|
||||
const assigneeReasonRequired = assigneeDirty && !isManager
|
||||
const assigneeReasonValid = !assigneeReasonRequired || assigneeChangeReason.trim().length >= 5
|
||||
const normalizedAssigneeReason = assigneeChangeReason.trim()
|
||||
const assigneeReasonValid = normalizedAssigneeReason.length === 0 || normalizedAssigneeReason.length >= 5
|
||||
const saveDisabled = !formDirty || saving || !assigneeReasonValid
|
||||
const companyLabel = useMemo(() => {
|
||||
if (ticket.company?.name) return ticket.company.name
|
||||
|
|
@ -488,9 +505,9 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
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" })
|
||||
if (reasonValue.length > 0 && reasonValue.length < 5) {
|
||||
setAssigneeReasonError("Descreva o motivo com pelo menos 5 caracteres ou deixe em branco.")
|
||||
toast.error("Informe ao menos 5 caracteres no motivo ou deixe o campo vazio.", { id: "assignee" })
|
||||
return
|
||||
}
|
||||
if (reasonValue.length > 1000) {
|
||||
|
|
@ -505,7 +522,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
ticketId: ticket.id as Id<"tickets">,
|
||||
assigneeId: assigneeSelection as Id<"users">,
|
||||
actorId: convexUserId as Id<"users">,
|
||||
reason: reasonValue,
|
||||
reason: reasonValue.length > 0 ? reasonValue : undefined,
|
||||
})
|
||||
toast.success("Responsável atualizado!", { id: "assignee" })
|
||||
if (assigneeSelection) {
|
||||
|
|
@ -1008,6 +1025,26 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
}
|
||||
}, [ticket.id, ticket.reference])
|
||||
|
||||
const handleReopenTicket = useCallback(async () => {
|
||||
if (!viewerId) {
|
||||
toast.error("Não foi possível identificar o usuário atual.")
|
||||
return
|
||||
}
|
||||
toast.dismiss("ticket-reopen")
|
||||
setIsReopening(true)
|
||||
toast.loading("Reabrindo ticket...", { id: "ticket-reopen" })
|
||||
try {
|
||||
await reopenTicket({ ticketId: ticket.id as Id<"tickets">, actorId: viewerId as Id<"users"> })
|
||||
toast.success("Ticket reaberto com sucesso!", { id: "ticket-reopen" })
|
||||
setStatus("AWAITING_ATTENDANCE")
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
toast.error("Não foi possível reabrir o ticket.", { id: "ticket-reopen" })
|
||||
} finally {
|
||||
setIsReopening(false)
|
||||
}
|
||||
}, [reopenTicket, ticket.id, viewerId])
|
||||
|
||||
return (
|
||||
<div className={cardClass}>
|
||||
<div className="absolute right-6 top-6 flex items-center gap-3">
|
||||
|
|
@ -1065,6 +1102,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
ticketId={ticket.id as unknown as string}
|
||||
tenantId={ticket.tenantId}
|
||||
actorId={convexUserId as Id<"users"> | null}
|
||||
ticketReference={ticket.reference ?? null}
|
||||
requesterName={ticket.requester?.name ?? ticket.requester?.email ?? null}
|
||||
agentName={agentName}
|
||||
workSummary={
|
||||
|
|
@ -1095,9 +1133,26 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
value={status}
|
||||
tenantId={ticket.tenantId}
|
||||
requesterName={ticket.requester?.name ?? ticket.requester?.email ?? null}
|
||||
ticketReference={ticket.reference ?? null}
|
||||
showCloseButton={false}
|
||||
onStatusChange={setStatus}
|
||||
/>
|
||||
{canReopenTicket ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-slate-200 bg-white text-sm font-semibold text-neutral-700 hover:bg-slate-50"
|
||||
onClick={handleReopenTicket}
|
||||
disabled={isReopening}
|
||||
>
|
||||
{isReopening ? <Spinner className="size-4 text-neutral-600" /> : null}
|
||||
Reabrir
|
||||
</Button>
|
||||
) : null}
|
||||
{canReopenTicket && reopenDeadlineLabel ? (
|
||||
<p className="text-xs text-neutral-500">Prazo para reabrir: {reopenDeadlineLabel}</p>
|
||||
) : null}
|
||||
{isPlaying ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
|
@ -1427,8 +1482,8 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
</p>
|
||||
{assigneeReasonError ? (
|
||||
<p className="text-xs font-semibold text-rose-600">{assigneeReasonError}</p>
|
||||
) : assigneeReasonRequired && assigneeChangeReason.trim().length < 5 ? (
|
||||
<p className="text-xs font-semibold text-rose-600">Descreva o motivo com pelo menos 5 caracteres.</p>
|
||||
) : normalizedAssigneeReason.length > 0 && normalizedAssigneeReason.length < 5 ? (
|
||||
<p className="text-xs font-semibold text-rose-600">Descreva o motivo com pelo menos 5 caracteres ou deixe em branco.</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue