feat: portal reopen, reports, templates and remote access
This commit is contained in:
parent
6a75a0a9ed
commit
52c03ff1cf
16 changed files with 1387 additions and 16 deletions
|
|
@ -35,6 +35,7 @@ import { priorityStyles } from "@/lib/ticket-priority-style"
|
|||
import { normalizeCustomFieldInputs } from "@/lib/ticket-form-helpers"
|
||||
import type { TicketFormDefinition, TicketFormFieldDefinition } from "@/lib/ticket-form-types"
|
||||
import { Calendar as CalendarIcon } from "lucide-react"
|
||||
import { VISIT_KEYWORDS } from "@/lib/ticket-matchers"
|
||||
|
||||
type TriggerVariant = "button" | "card"
|
||||
|
||||
|
|
@ -108,6 +109,7 @@ const schema = z.object({
|
|||
priority: z.enum(["LOW", "MEDIUM", "HIGH", "URGENT"]).default("MEDIUM"),
|
||||
channel: z.enum(["EMAIL", "WHATSAPP", "CHAT", "PHONE", "API", "MANUAL"]).default("MANUAL"),
|
||||
queueName: z.string().nullable().optional(),
|
||||
visitDate: z.string().nullable().optional(),
|
||||
assigneeId: z.string().nullable().optional(),
|
||||
companyId: z.string().optional(),
|
||||
requesterId: z.string().min(1, "Selecione um solicitante"),
|
||||
|
|
@ -221,6 +223,7 @@ export function NewTicketDialog({
|
|||
const [selectedFormKey, setSelectedFormKey] = useState<string>("default")
|
||||
const [customFieldValues, setCustomFieldValues] = useState<Record<string, unknown>>({})
|
||||
const [openCalendarField, setOpenCalendarField] = useState<string | null>(null)
|
||||
const [visitDatePickerOpen, setVisitDatePickerOpen] = useState(false)
|
||||
|
||||
const selectedForm = useMemo(() => forms.find((formDef) => formDef.key === selectedFormKey) ?? forms[0], [forms, selectedFormKey])
|
||||
|
||||
|
|
@ -270,12 +273,29 @@ export function NewTicketDialog({
|
|||
)
|
||||
const priorityValue = form.watch("priority") as TicketPriority
|
||||
const queueValue = form.watch("queueName") ?? "NONE"
|
||||
const visitDateValue = form.watch("visitDate") ?? null
|
||||
const assigneeValue = form.watch("assigneeId") ?? null
|
||||
const assigneeSelectValue = assigneeValue ?? "NONE"
|
||||
const requesterValue = form.watch("requesterId") ?? ""
|
||||
const categoryIdValue = form.watch("categoryId")
|
||||
const subcategoryIdValue = form.watch("subcategoryId")
|
||||
const isSubmitted = form.formState.isSubmitted
|
||||
|
||||
const normalizedQueueName =
|
||||
typeof queueValue === "string" && queueValue !== "NONE" ? queueValue.toLowerCase() : ""
|
||||
const isVisitQueue = useMemo(
|
||||
() => VISIT_KEYWORDS.some((keyword) => normalizedQueueName.includes(keyword)),
|
||||
[normalizedQueueName]
|
||||
)
|
||||
const visitDate = useMemo(() => {
|
||||
if (!visitDateValue) return null
|
||||
try {
|
||||
return parseISO(visitDateValue)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}, [visitDateValue])
|
||||
|
||||
const companyOptions = useMemo(() => {
|
||||
const map = new Map<string, { id: string; name: string; isAvulso?: boolean; keywords: string[] }>()
|
||||
companies.forEach((company) => {
|
||||
|
|
@ -429,6 +449,7 @@ export function NewTicketDialog({
|
|||
if (!open) {
|
||||
setAssigneeInitialized(false)
|
||||
setOpenCalendarField(null)
|
||||
setVisitDatePickerOpen(false)
|
||||
return
|
||||
}
|
||||
if (assigneeInitialized) return
|
||||
|
|
@ -490,6 +511,17 @@ export function NewTicketDialog({
|
|||
return
|
||||
}
|
||||
|
||||
const currentQueueName = values.queueName ?? ""
|
||||
const isVisitQueueOnSubmit =
|
||||
typeof currentQueueName === "string" &&
|
||||
VISIT_KEYWORDS.some((keyword) => currentQueueName.toLowerCase().includes(keyword))
|
||||
if (isVisitQueueOnSubmit) {
|
||||
if (!values.visitDate) {
|
||||
form.setError("visitDate", { type: "custom", message: "Informe a data da visita para chamados desta fila." })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let customFieldsPayload: Array<{ fieldId: Id<"ticketFields">; value: unknown }> = []
|
||||
if (selectedFormKey !== "default" && selectedForm?.fields?.length) {
|
||||
const normalized = normalizeCustomFieldInputs(selectedForm.fields, customFieldValues)
|
||||
|
|
@ -506,6 +538,15 @@ export function NewTicketDialog({
|
|||
const sel = queues.find((q) => q.name === values.queueName)
|
||||
const selectedAssignee = form.getValues("assigneeId") ?? null
|
||||
const requesterToSend = values.requesterId as Id<"users">
|
||||
let visitDateTimestamp: number | undefined
|
||||
if (isVisitQueueOnSubmit && values.visitDate) {
|
||||
try {
|
||||
const parsed = parseISO(values.visitDate)
|
||||
visitDateTimestamp = parsed.getTime()
|
||||
} catch {
|
||||
visitDateTimestamp = undefined
|
||||
}
|
||||
}
|
||||
const id = await create({
|
||||
actorId: convexUserId as Id<"users">,
|
||||
tenantId: DEFAULT_TENANT_ID,
|
||||
|
|
@ -520,6 +561,7 @@ export function NewTicketDialog({
|
|||
subcategoryId: values.subcategoryId as Id<"ticketSubcategories">,
|
||||
formTemplate: selectedFormKey !== "default" ? selectedFormKey : undefined,
|
||||
customFields: customFieldsPayload.length > 0 ? customFieldsPayload : undefined,
|
||||
visitDate: visitDateTimestamp,
|
||||
})
|
||||
const summaryFallback = values.summary?.trim() ?? ""
|
||||
const bodyHtml = plainDescription.length > 0 ? sanitizedDescription : summaryFallback
|
||||
|
|
@ -551,6 +593,7 @@ export function NewTicketDialog({
|
|||
assigneeId: convexUserId ?? null,
|
||||
categoryId: "",
|
||||
subcategoryId: "",
|
||||
visitDate: null,
|
||||
})
|
||||
form.clearErrors()
|
||||
setSelectedFormKey("default")
|
||||
|
|
@ -919,6 +962,60 @@ export function NewTicketDialog({
|
|||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
{isVisitQueue ? (
|
||||
<Field>
|
||||
<FieldLabel className="flex items-center gap-1">
|
||||
Data da visita <span className="text-destructive">*</span>
|
||||
</FieldLabel>
|
||||
<Popover open={visitDatePickerOpen} onOpenChange={setVisitDatePickerOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex h-8 w-full items-center justify-between rounded-full border border-slate-300 bg-white px-3 text-left text-sm text-neutral-800 shadow-sm hover:bg-slate-50",
|
||||
!visitDate && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{visitDate
|
||||
? format(visitDate, "dd/MM/yyyy", { locale: ptBR })
|
||||
: "Selecione a data"}
|
||||
<CalendarIcon className="ml-2 size-4 text-neutral-500" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={visitDate ?? undefined}
|
||||
onSelect={(date) => {
|
||||
if (!date) {
|
||||
form.setValue("visitDate", null, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
shouldValidate: form.formState.isSubmitted,
|
||||
})
|
||||
return
|
||||
}
|
||||
const iso = date.toISOString().slice(0, 10)
|
||||
form.setValue("visitDate", iso, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
shouldValidate: form.formState.isSubmitted,
|
||||
})
|
||||
setVisitDatePickerOpen(false)
|
||||
}}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FieldError
|
||||
errors={
|
||||
form.formState.errors.visitDate
|
||||
? [{ message: form.formState.errors.visitDate.message as string }]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
) : null}
|
||||
<Field>
|
||||
<FieldLabel>Responsável</FieldLabel>
|
||||
<Select
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue