feat: enhance visit scheduling and closing flow
This commit is contained in:
parent
a7f9191e1d
commit
6473e8d40f
5 changed files with 577 additions and 243 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 { TimePicker } from "@/components/ui/time-picker"
|
||||
import { VISIT_KEYWORDS } from "@/lib/ticket-matchers"
|
||||
|
||||
type TriggerVariant = "button" | "card"
|
||||
|
|
@ -119,6 +120,7 @@ const schema = z.object({
|
|||
channel: z.enum(["EMAIL", "WHATSAPP", "CHAT", "PHONE", "API", "MANUAL"]).default("MANUAL"),
|
||||
queueName: z.string().nullable().optional(),
|
||||
visitDate: z.string().nullable().optional(),
|
||||
visitTime: z.string().nullable().optional(),
|
||||
assigneeId: z.string().nullable().optional(),
|
||||
companyId: z.string().optional(),
|
||||
requesterId: z.string().min(1, "Selecione um solicitante"),
|
||||
|
|
@ -144,6 +146,8 @@ export function NewTicketDialog({
|
|||
priority: "MEDIUM",
|
||||
channel: "MANUAL",
|
||||
queueName: null,
|
||||
visitDate: null,
|
||||
visitTime: null,
|
||||
assigneeId: null,
|
||||
companyId: NO_COMPANY_VALUE,
|
||||
requesterId: "",
|
||||
|
|
@ -286,6 +290,7 @@ export function NewTicketDialog({
|
|||
const priorityValue = form.watch("priority") as TicketPriority
|
||||
const queueValue = form.watch("queueName") ?? "NONE"
|
||||
const visitDateValue = form.watch("visitDate") ?? null
|
||||
const visitTimeValue = form.watch("visitTime") ?? null
|
||||
const assigneeValue = form.watch("assigneeId") ?? null
|
||||
const assigneeSelectValue = assigneeValue ?? "NONE"
|
||||
const requesterValue = form.watch("requesterId") ?? ""
|
||||
|
|
@ -301,12 +306,21 @@ export function NewTicketDialog({
|
|||
)
|
||||
const visitDate = useMemo(() => {
|
||||
if (!visitDateValue) return null
|
||||
const timeSegment =
|
||||
typeof visitTimeValue === "string" && visitTimeValue.length > 0 ? visitTimeValue : "00:00"
|
||||
const normalizedTime = timeSegment.length === 5 ? `${timeSegment}:00` : timeSegment
|
||||
try {
|
||||
return parseISO(visitDateValue)
|
||||
return parseISO(`${visitDateValue}T${normalizedTime}`)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}, [visitDateValue])
|
||||
}, [visitDateValue, visitTimeValue])
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisitQueue) return
|
||||
form.setValue("visitDate", null, { shouldDirty: false, shouldTouch: false })
|
||||
form.setValue("visitTime", null, { shouldDirty: false, shouldTouch: false })
|
||||
}, [form, isVisitQueue])
|
||||
|
||||
const companyOptions = useMemo(() => {
|
||||
const map = new Map<string, { id: string; name: string; isAvulso?: boolean; keywords: string[] }>()
|
||||
|
|
@ -532,6 +546,10 @@ export function NewTicketDialog({
|
|||
form.setError("visitDate", { type: "custom", message: "Informe a data da visita para chamados desta fila." })
|
||||
return
|
||||
}
|
||||
if (!values.visitTime) {
|
||||
form.setError("visitTime", { type: "custom", message: "Informe o horário da visita para chamados desta fila." })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let customFieldsPayload: Array<{ fieldId: Id<"ticketFields">; value: unknown }> = []
|
||||
|
|
@ -551,9 +569,10 @@ export function NewTicketDialog({
|
|||
const selectedAssignee = form.getValues("assigneeId") ?? null
|
||||
const requesterToSend = values.requesterId as Id<"users">
|
||||
let visitDateTimestamp: number | undefined
|
||||
if (isVisitQueueOnSubmit && values.visitDate) {
|
||||
if (isVisitQueueOnSubmit && values.visitDate && values.visitTime) {
|
||||
try {
|
||||
const parsed = parseISO(values.visitDate)
|
||||
const timeSegment = values.visitTime.length === 5 ? `${values.visitTime}:00` : values.visitTime
|
||||
const parsed = parseISO(`${values.visitDate}T${timeSegment}`)
|
||||
visitDateTimestamp = parsed.getTime()
|
||||
} catch {
|
||||
visitDateTimestamp = undefined
|
||||
|
|
@ -604,6 +623,7 @@ export function NewTicketDialog({
|
|||
categoryId: "",
|
||||
subcategoryId: "",
|
||||
visitDate: null,
|
||||
visitTime: null,
|
||||
})
|
||||
form.clearErrors()
|
||||
setSelectedFormKey("default")
|
||||
|
|
@ -960,55 +980,85 @@ export function NewTicketDialog({
|
|||
{isVisitQueue ? (
|
||||
<Field>
|
||||
<FieldLabel className="flex items-center gap-1">
|
||||
Data da visita <span className="text-destructive">*</span>
|
||||
Data e horário 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, {
|
||||
<div className="space-y-2">
|
||||
<div className="grid gap-2 sm:grid-cols-[minmax(0,1fr)_140px]">
|
||||
<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,
|
||||
})
|
||||
form.setValue("visitTime", 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>
|
||||
<div className="flex flex-col gap-1">
|
||||
<TimePicker
|
||||
value={typeof visitTimeValue === "string" ? visitTimeValue : ""}
|
||||
onChange={(value) =>
|
||||
form.setValue("visitTime", value || 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 }]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
stepMinutes={5}
|
||||
className="h-8 rounded-full border border-slate-300 bg-white text-sm font-medium text-neutral-800 shadow-sm"
|
||||
/>
|
||||
<FieldError
|
||||
errors={
|
||||
form.formState.errors.visitTime
|
||||
? [{ message: form.formState.errors.visitTime.message as string }]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FieldError
|
||||
errors={
|
||||
form.formState.errors.visitDate
|
||||
? [{ message: form.formState.errors.visitDate.message as string }]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
) : null}
|
||||
<Field>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue