feat(visits): concluir/reabrir visita sem poluir agenda
This commit is contained in:
parent
8f2c00a75a
commit
66559eafbf
9 changed files with 264 additions and 31 deletions
|
|
@ -1,6 +1,5 @@
|
|||
import { NextResponse } from "next/server"
|
||||
import type { Id } from "@/convex/_generated/dataModel"
|
||||
import type { Prisma, UserRole } from "@/lib/prisma"
|
||||
import { api } from "@/convex/_generated/api"
|
||||
import { ConvexHttpClient } from "convex/browser"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
|
@ -14,7 +13,8 @@ function normalizeRole(input: string | null | undefined): RoleOption {
|
|||
return ((ROLE_OPTIONS as readonly string[]).includes(candidate) ? candidate : "agent") as RoleOption
|
||||
}
|
||||
|
||||
const USER_ROLE_OPTIONS: ReadonlyArray<UserRole> = ["ADMIN", "MANAGER", "AGENT", "COLLABORATOR"]
|
||||
const USER_ROLE_OPTIONS = ["ADMIN", "MANAGER", "AGENT", "COLLABORATOR"] as const
|
||||
type UserRole = (typeof USER_ROLE_OPTIONS)[number]
|
||||
|
||||
function mapToUserRole(role: RoleOption): UserRole {
|
||||
const candidate = role.toUpperCase() as UserRole
|
||||
|
|
@ -207,7 +207,7 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
|
|||
}
|
||||
|
||||
if (domainUser) {
|
||||
const updateData: Prisma.UserUncheckedUpdateInput = {
|
||||
const updateData: Record<string, unknown> = {
|
||||
email: nextEmail,
|
||||
name: nextName || domainUser.name,
|
||||
role: mapToUserRole(nextRole),
|
||||
|
|
@ -225,30 +225,34 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
|
|||
data: updateData,
|
||||
})
|
||||
} else {
|
||||
const upsertUpdate: Prisma.UserUncheckedUpdateInput = {
|
||||
const upsertUpdate = {
|
||||
name: nextName || nextEmail,
|
||||
role: mapToUserRole(nextRole),
|
||||
tenantId: nextTenant,
|
||||
companyId: companyId ?? null,
|
||||
jobTitle: hasJobTitleField ? jobTitle ?? null : undefined,
|
||||
managerId: hasManagerField ? managerRecord?.id ?? null : undefined,
|
||||
}
|
||||
if (hasJobTitleField) {
|
||||
upsertUpdate.jobTitle = jobTitle ?? null
|
||||
// noop: já definido acima
|
||||
}
|
||||
if (hasManagerField) {
|
||||
upsertUpdate.managerId = managerRecord?.id ?? null
|
||||
// noop: já definido acima
|
||||
}
|
||||
const upsertCreate: Prisma.UserUncheckedCreateInput = {
|
||||
const upsertCreate = {
|
||||
email: nextEmail,
|
||||
name: nextName || nextEmail,
|
||||
role: mapToUserRole(nextRole),
|
||||
tenantId: nextTenant,
|
||||
companyId: companyId ?? null,
|
||||
jobTitle: hasJobTitleField ? jobTitle ?? null : undefined,
|
||||
managerId: hasManagerField ? managerRecord?.id ?? null : undefined,
|
||||
}
|
||||
if (hasJobTitleField) {
|
||||
upsertCreate.jobTitle = jobTitle ?? null
|
||||
// noop: já definido acima
|
||||
}
|
||||
if (hasManagerField) {
|
||||
upsertCreate.managerId = managerRecord?.id ?? null
|
||||
// noop: já definido acima
|
||||
}
|
||||
await prisma.user.upsert({
|
||||
where: { email: nextEmail },
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ interface TicketQueueSummaryProps {
|
|||
|
||||
function resolveSlaTone(percent: number) {
|
||||
if (percent < 25) {
|
||||
return { indicatorClass: "bg-[#00e8ff]", textClass: "text-[#00e8ff]" }
|
||||
return { indicatorClass: "bg-[#00e8ff]", textClass: "text-neutral-500" }
|
||||
}
|
||||
if (percent < 50) {
|
||||
return { indicatorClass: "bg-emerald-400", textClass: "text-emerald-400" }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { format, formatDistanceToNow, parseISO } from "date-fns"
|
||||
import { ptBR } from "date-fns/locale"
|
||||
|
|
@ -108,6 +109,22 @@ const PAUSE_REASONS = [
|
|||
{ value: "IN_PROCEDURE", label: "Em procedimento" },
|
||||
{ value: "LUNCH_BREAK", label: "Intervalo de almoço" },
|
||||
]
|
||||
const VISIT_STATUS_LABELS: Record<string, string> = {
|
||||
scheduled: "Agendada",
|
||||
en_route: "Em deslocamento",
|
||||
in_service: "Em atendimento",
|
||||
done: "Concluída",
|
||||
no_show: "No-show",
|
||||
canceled: "Cancelada",
|
||||
}
|
||||
const VISIT_STATUS_TONES: Record<string, string> = {
|
||||
scheduled: "bg-slate-100 text-neutral-700 border-slate-200",
|
||||
en_route: "bg-sky-100 text-sky-800 border-sky-200",
|
||||
in_service: "bg-amber-100 text-amber-800 border-amber-200",
|
||||
done: "bg-emerald-100 text-emerald-800 border-emerald-200",
|
||||
no_show: "bg-rose-100 text-rose-800 border-rose-200",
|
||||
canceled: "bg-slate-100 text-neutral-600 border-slate-200",
|
||||
}
|
||||
|
||||
type CustomerOption = {
|
||||
id: string
|
||||
|
|
@ -140,6 +157,7 @@ function formatDuration(durationMs: number) {
|
|||
|
||||
export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
||||
const { convexUserId, role, isStaff, session, machineContext } = useAuth()
|
||||
const router = useRouter()
|
||||
const normalizedRole = (role ?? "").toLowerCase()
|
||||
const isManager = normalizedRole === "manager"
|
||||
const isAdmin = normalizedRole === "admin"
|
||||
|
|
@ -157,6 +175,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
const viewerEmailRaw = session?.user?.email ?? machineContext?.assignedUserEmail ?? null
|
||||
const viewerEmail = (viewerEmailRaw ?? "").trim().toLowerCase()
|
||||
const [status, setStatus] = useState<TicketStatus>(ticket.status)
|
||||
const [visitStatusLoading, setVisitStatusLoading] = useState(false)
|
||||
const rawReopenDeadline = ticket.reopenDeadline ?? null
|
||||
const fallbackClosedMs = ticket.closedAt?.getTime() ?? ticket.resolvedAt?.getTime() ?? null
|
||||
const DEFAULT_REOPEN_DAYS = 7
|
||||
|
|
@ -215,6 +234,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
const updateCategories = useMutation(api.tickets.updateCategories)
|
||||
const reopenTicket = useMutation(api.tickets.reopenTicket)
|
||||
const updateVisitSchedule = useMutation(api.tickets.updateVisitSchedule)
|
||||
const updateVisitStatus = useMutation(api.tickets.updateVisitStatus)
|
||||
const agents = (useQuery(api.users.listAgents, { tenantId: ticket.tenantId }) as Doc<"users">[] | undefined) ?? []
|
||||
const normalizedTicketQueue = useMemo(() => (ticket.queue ?? "").toLowerCase(), [ticket.queue])
|
||||
const isVisitQueueTicket = useMemo(
|
||||
|
|
@ -379,6 +399,8 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
[normalizedQueueSelection],
|
||||
)
|
||||
const visitSectionEnabled = editing ? isVisitQueueSelected : isVisitQueueTicket
|
||||
const visitStatusValue = (ticket.visitStatus as string | null) ?? (ticket.dueAt ? "scheduled" : null)
|
||||
const visitPerformedAt = ticket.visitPerformedAt ?? null
|
||||
const visitDirty = useMemo(() => {
|
||||
if (!visitSectionEnabled) {
|
||||
return false
|
||||
|
|
@ -469,6 +491,35 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
const normalizedAssigneeReason = assigneeChangeReason.trim()
|
||||
const assigneeReasonValid = normalizedAssigneeReason.length === 0 || normalizedAssigneeReason.length >= 5
|
||||
const saveDisabled = !formDirty || saving || !assigneeReasonValid || visitHasInvalid
|
||||
const handleVisitStatusChange = useCallback(
|
||||
async (nextStatus: string) => {
|
||||
if (!visitSectionEnabled) return
|
||||
if (!viewerId) {
|
||||
toast.error("É necessário estar autenticado para atualizar a visita.")
|
||||
return
|
||||
}
|
||||
const normalizedStatus = nextStatus.toLowerCase()
|
||||
setVisitStatusLoading(true)
|
||||
const toastId = "visit-status"
|
||||
toast.loading("Atualizando status da visita...", { id: toastId })
|
||||
try {
|
||||
await updateVisitStatus({
|
||||
ticketId: ticket.id as Id<"tickets">,
|
||||
actorId: viewerId as Id<"users">,
|
||||
status: normalizedStatus,
|
||||
performedAt: Date.now(),
|
||||
})
|
||||
toast.success("Visita atualizada com sucesso.", { id: toastId })
|
||||
router.refresh()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
toast.error("Não foi possível atualizar o status da visita.", { id: toastId })
|
||||
} finally {
|
||||
setVisitStatusLoading(false)
|
||||
}
|
||||
},
|
||||
[router, ticket.id, updateVisitStatus, viewerId, visitSectionEnabled],
|
||||
)
|
||||
const companyLabel = useMemo(() => {
|
||||
if (ticket.company?.name) return ticket.company.name
|
||||
if (isAvulso) return "Cliente avulso"
|
||||
|
|
@ -1810,9 +1861,49 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<span className={sectionValueClass}>
|
||||
{ticket.dueAt ? format(ticket.dueAt, "dd/MM/yyyy HH:mm", { locale: ptBR }) : "Sem data definida"}
|
||||
</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className={sectionValueClass}>
|
||||
{ticket.dueAt ? format(ticket.dueAt, "dd/MM/yyyy HH:mm", { locale: ptBR }) : "Sem data definida"}
|
||||
</span>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{visitStatusValue ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"rounded-full border px-2.5 py-1 text-[11px] font-semibold",
|
||||
VISIT_STATUS_TONES[visitStatusValue] ?? "bg-slate-100 text-neutral-700 border-slate-200",
|
||||
)}
|
||||
>
|
||||
{VISIT_STATUS_LABELS[visitStatusValue] ?? visitStatusValue}
|
||||
</Badge>
|
||||
) : null}
|
||||
{!isManager && visitStatusValue !== "done" ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleVisitStatusChange("done")}
|
||||
disabled={visitStatusLoading}
|
||||
>
|
||||
Marcar visita realizada
|
||||
</Button>
|
||||
) : null}
|
||||
{!isManager && visitStatusValue && visitStatusValue !== "scheduled" && visitStatusValue !== "in_service" ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleVisitStatusChange("scheduled")}
|
||||
disabled={visitStatusLoading}
|
||||
>
|
||||
Reabrir visita
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{visitStatusValue === "done" && visitPerformedAt ? (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Concluída em {format(visitPerformedAt, "dd/MM/yyyy HH:mm", { locale: ptBR })}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : slaDueDate ? (
|
||||
|
|
|
|||
|
|
@ -14,10 +14,17 @@ import {
|
|||
|
||||
import type { Ticket, TicketPriority } from "@/lib/schemas/ticket"
|
||||
import type { AgendaFilterState, AgendaPeriod } from "@/components/agenda/agenda-filters"
|
||||
import { getSlaDisplayStatus, getSlaDueDate } from "@/lib/sla-utils"
|
||||
import { getSlaDueDate } from "@/lib/sla-utils"
|
||||
import { isVisitTicket } from "@/lib/ticket-matchers"
|
||||
|
||||
export type AgendaSlaStatus = "on_track" | "at_risk" | "breached" | "met"
|
||||
type VisitStatus = NonNullable<Ticket["visitStatus"]>
|
||||
type VisitState = {
|
||||
status: VisitStatus | null
|
||||
scheduledAt: Date | null
|
||||
performedAt: Date | null
|
||||
completed: boolean
|
||||
}
|
||||
|
||||
export type AgendaTicketSummary = {
|
||||
id: string
|
||||
|
|
@ -30,6 +37,7 @@ export type AgendaTicketSummary = {
|
|||
startAt: Date | null
|
||||
endAt: Date | null
|
||||
slaStatus: AgendaSlaStatus
|
||||
visitStatus?: VisitStatus | null
|
||||
completedAt?: Date | null
|
||||
href: string
|
||||
}
|
||||
|
|
@ -85,8 +93,9 @@ export function buildAgendaDataset(tickets: Ticket[], filters: AgendaFilterState
|
|||
|
||||
const enriched = filteredTickets.map((ticket) => {
|
||||
const schedule = deriveScheduleWindow(ticket)
|
||||
const slaStatus = computeSlaStatus(ticket, now)
|
||||
return { ticket, schedule, slaStatus }
|
||||
const visitState = deriveVisitState(ticket)
|
||||
const slaStatus = computeVisitSlaStatus(visitState, now)
|
||||
return { ticket, schedule, visitState, slaStatus }
|
||||
})
|
||||
|
||||
const summarySections = {
|
||||
|
|
@ -99,12 +108,21 @@ export function buildAgendaDataset(tickets: Ticket[], filters: AgendaFilterState
|
|||
const upcomingWindowEnd = addMonths(now, 12)
|
||||
|
||||
for (const entry of enriched) {
|
||||
const summary = buildSummary(entry.ticket, entry.schedule, entry.slaStatus)
|
||||
const summary = buildSummary(entry.ticket, entry.schedule, entry.visitState, entry.slaStatus)
|
||||
const dueDate = entry.schedule.startAt
|
||||
const createdAt = entry.ticket.createdAt
|
||||
const resolvedAt = entry.ticket.resolvedAt
|
||||
|
||||
if (dueDate && !entry.ticket.resolvedAt) {
|
||||
const isCompleted = entry.visitState.completed
|
||||
const visitCompletedAt = entry.visitState.completed ? entry.visitState.performedAt ?? entry.visitState.scheduledAt : null
|
||||
|
||||
if (isCompleted) {
|
||||
if (visitCompletedAt && isWithinRange(visitCompletedAt, range)) {
|
||||
summarySections.completed.push(summary)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (dueDate) {
|
||||
const isFuture = isAfter(dueDate, now)
|
||||
if (isFuture && isBefore(dueDate, upcomingWindowEnd)) {
|
||||
summarySections.upcoming.push(summary)
|
||||
|
|
@ -117,10 +135,6 @@ export function buildAgendaDataset(tickets: Ticket[], filters: AgendaFilterState
|
|||
if (!dueDate && entry.ticket.status !== "RESOLVED" && isWithinRange(createdAt, range)) {
|
||||
summarySections.unscheduled.push(summary)
|
||||
}
|
||||
|
||||
if (resolvedAt && isWithinRange(resolvedAt, range)) {
|
||||
summarySections.completed.push(summary)
|
||||
}
|
||||
}
|
||||
|
||||
summarySections.upcoming.sort((a, b) => compareNullableDate(a.startAt, b.startAt, 1))
|
||||
|
|
@ -129,7 +143,10 @@ export function buildAgendaDataset(tickets: Ticket[], filters: AgendaFilterState
|
|||
summarySections.completed.sort((a, b) => compareNullableDate(a.completedAt ?? null, b.completedAt ?? null, -1))
|
||||
|
||||
const calendarEvents = enriched
|
||||
.filter((entry): entry is typeof entry & { schedule: { startAt: Date; endAt: Date } } => Boolean(entry.schedule.startAt && entry.schedule.endAt))
|
||||
.filter(
|
||||
(entry): entry is typeof entry & { schedule: { startAt: Date; endAt: Date } } =>
|
||||
Boolean(entry.schedule.startAt && entry.schedule.endAt),
|
||||
)
|
||||
.map((entry) => ({
|
||||
id: `${entry.ticket.id}-event`,
|
||||
ticketId: entry.ticket.id,
|
||||
|
|
@ -217,15 +234,51 @@ function deriveScheduleWindow(ticket: Ticket) {
|
|||
return { startAt, endAt }
|
||||
}
|
||||
|
||||
function computeSlaStatus(ticket: Ticket, now: Date): AgendaSlaStatus {
|
||||
const status = getSlaDisplayStatus(ticket, "solution", now)
|
||||
if (status === "n/a") {
|
||||
return "on_track"
|
||||
function deriveVisitState(ticket: Ticket): VisitState {
|
||||
const scheduledAt = ticket.dueAt ?? null
|
||||
const rawStatus = (ticket.visitStatus ?? null) as VisitStatus | null
|
||||
const performedAt = ticket.visitPerformedAt ?? null
|
||||
// Se já existe registro de execução, considere concluída mesmo que o status não tenha sido persistido.
|
||||
if (!rawStatus && performedAt) {
|
||||
return { status: "done", scheduledAt, performedAt, completed: true }
|
||||
}
|
||||
if (!rawStatus) {
|
||||
return { status: null, scheduledAt, performedAt: null, completed: false }
|
||||
}
|
||||
const normalized = rawStatus.toLowerCase() as VisitStatus
|
||||
const completed = normalized === "done" || normalized === "no_show" || normalized === "canceled"
|
||||
// Se reabrir (status != concluído), zera performedAt.
|
||||
const effectivePerformed = completed ? performedAt : null
|
||||
return {
|
||||
status: normalized,
|
||||
scheduledAt,
|
||||
performedAt: effectivePerformed,
|
||||
completed,
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
function buildSummary(ticket: Ticket, schedule: { startAt: Date | null; endAt: Date | null }, slaStatus: AgendaSlaStatus): AgendaTicketSummary {
|
||||
function computeVisitSlaStatus(visitState: VisitState, now: Date): AgendaSlaStatus {
|
||||
if (visitState.completed) {
|
||||
if (visitState.status === "done") return "met"
|
||||
if (visitState.status === "canceled") return "on_track"
|
||||
return "breached"
|
||||
}
|
||||
if (visitState.scheduledAt) {
|
||||
if (isBefore(visitState.scheduledAt, now)) {
|
||||
return "breached"
|
||||
}
|
||||
return "on_track"
|
||||
}
|
||||
return "on_track"
|
||||
}
|
||||
|
||||
function buildSummary(
|
||||
ticket: Ticket,
|
||||
schedule: { startAt: Date | null; endAt: Date | null },
|
||||
visitState: VisitState,
|
||||
slaStatus: AgendaSlaStatus,
|
||||
): AgendaTicketSummary {
|
||||
const completedAt = visitState.completed ? visitState.performedAt ?? visitState.scheduledAt : ticket.resolvedAt ?? null
|
||||
return {
|
||||
id: ticket.id,
|
||||
reference: ticket.reference,
|
||||
|
|
@ -237,7 +290,8 @@ function buildSummary(ticket: Ticket, schedule: { startAt: Date | null; endAt: D
|
|||
startAt: schedule.startAt,
|
||||
endAt: ticket.resolvedAt ?? schedule.endAt,
|
||||
slaStatus,
|
||||
completedAt: ticket.resolvedAt ?? null,
|
||||
visitStatus: visitState.status ?? null,
|
||||
completedAt,
|
||||
href: `/tickets/${ticket.id}`,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,6 +111,8 @@ const serverTicketSchema = z.object({
|
|||
slaPausedBy: z.string().nullable().optional(),
|
||||
slaPausedMs: z.number().nullable().optional(),
|
||||
dueAt: z.number().nullable().optional(),
|
||||
visitStatus: z.string().nullable().optional(),
|
||||
visitPerformedAt: z.number().nullable().optional(),
|
||||
firstResponseAt: z.number().nullable().optional(),
|
||||
resolvedAt: z.number().nullable().optional(),
|
||||
closedAt: z.number().nullable().optional(),
|
||||
|
|
@ -280,6 +282,8 @@ export function mapTicketFromServer(input: unknown) {
|
|||
slaPausedAt: s.slaPausedAt ? new Date(s.slaPausedAt) : null,
|
||||
slaPausedBy: s.slaPausedBy ?? null,
|
||||
slaPausedMs: typeof s.slaPausedMs === "number" ? s.slaPausedMs : null,
|
||||
visitStatus: typeof s.visitStatus === "string" ? s.visitStatus : null,
|
||||
visitPerformedAt: s.visitPerformedAt ? new Date(s.visitPerformedAt) : null,
|
||||
workSummary: s.workSummary
|
||||
? {
|
||||
totalWorkedMs: s.workSummary.totalWorkedMs,
|
||||
|
|
@ -362,6 +366,8 @@ export function mapTicketWithDetailsFromServer(input: unknown) {
|
|||
updatedAt: new Date(base.updatedAt),
|
||||
createdAt: new Date(base.createdAt),
|
||||
dueAt: base.dueAt ? new Date(base.dueAt) : null,
|
||||
visitStatus: typeof base.visitStatus === "string" ? base.visitStatus : null,
|
||||
visitPerformedAt: base.visitPerformedAt ? new Date(base.visitPerformedAt) : null,
|
||||
firstResponseAt: base.firstResponseAt ? new Date(base.firstResponseAt) : null,
|
||||
resolvedAt: base.resolvedAt ? new Date(base.resolvedAt) : null,
|
||||
closedAt: base.closedAt ? new Date(base.closedAt) : null,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ export type TicketStatus = z.infer<typeof ticketStatusSchema>
|
|||
|
||||
const slaStatusSchema = z.enum(["pending", "met", "breached", "n/a"])
|
||||
const slaTimeModeSchema = z.enum(["business", "calendar"])
|
||||
export const visitStatusSchema = z.enum(["scheduled", "en_route", "in_service", "done", "no_show", "canceled"])
|
||||
export type VisitStatus = z.infer<typeof visitStatusSchema>
|
||||
|
||||
export const ticketSlaSnapshotSchema = z.object({
|
||||
categoryId: z.string().optional(),
|
||||
|
|
@ -163,6 +165,8 @@ export const ticketSchema = z.object({
|
|||
slaPausedBy: z.string().nullable().optional(),
|
||||
slaPausedMs: z.number().nullable().optional(),
|
||||
dueAt: z.coerce.date().nullable(),
|
||||
visitStatus: visitStatusSchema.nullable().optional(),
|
||||
visitPerformedAt: z.coerce.date().nullable().optional(),
|
||||
firstResponseAt: z.coerce.date().nullable(),
|
||||
resolvedAt: z.coerce.date().nullable(),
|
||||
closedAt: z.coerce.date().nullable().optional(),
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export const TICKET_TIMELINE_LABELS: Record<string, string> = {
|
|||
MANAGER_NOTIFIED: "Gestor notificado",
|
||||
VISIT_SCHEDULED: "Visita agendada",
|
||||
VISIT_SCHEDULE_CHANGED: "Data da visita alterada",
|
||||
VISIT_STATUS_CHANGED: "Status da visita atualizado",
|
||||
CSAT_RECEIVED: "CSAT recebido",
|
||||
CSAT_RATED: "CSAT avaliado",
|
||||
TICKET_LINKED: "Chamado vinculado",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue