feat(company): define prazo de reabertura por empresa
Some checks failed
CI/CD Web + Desktop / Detect changes (push) Successful in 4s
Quality Checks / Lint, Test and Build (push) Successful in 3m22s
CI/CD Web + Desktop / Deploy (VPS Linux) (push) Successful in 3m25s
CI/CD Web + Desktop / Deploy Convex functions (push) Failing after 1m23s

- Adiciona campo reopenWindowDays no cadastro de empresa (padrao 7 dias)
- Ticket usa automaticamente o prazo da empresa ao ser resolvido
- Remove selecao de prazo do modal de encerramento de ticket
- Valor e gravado no ticket no momento da resolucao

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rever-tecnologia 2025-12-17 13:53:14 -03:00
parent d12dcf9512
commit 034f6f47ff
5 changed files with 47 additions and 32 deletions

View file

@ -82,6 +82,7 @@ export default defineSchema({
contacts: v.optional(v.any()), contacts: v.optional(v.any()),
locations: v.optional(v.any()), locations: v.optional(v.any()),
sla: v.optional(v.any()), sla: v.optional(v.any()),
reopenWindowDays: v.optional(v.number()),
tags: v.optional(v.array(v.string())), tags: v.optional(v.array(v.string())),
customFields: v.optional(v.any()), customFields: v.optional(v.any()),
notes: v.optional(v.string()), notes: v.optional(v.string()),

View file

@ -3175,7 +3175,18 @@ export async function resolveTicketHandler(
throw new ConvexError("Chamado vinculado não encontrado") throw new ConvexError("Chamado vinculado não encontrado")
} }
const reopenDays = resolveReopenWindowDays(reopenWindowDays) // Buscar prazo de reabertura da empresa do ticket (se existir)
let companyReopenDays: number | null = null
if (ticketDoc.companyId) {
const company = await ctx.db.get(ticketDoc.companyId)
if (company && typeof company.reopenWindowDays === "number") {
companyReopenDays = company.reopenWindowDays
}
}
// Prioridade: 1) valor passado explicitamente, 2) valor da empresa, 3) padrão
const effectiveReopenDays = reopenWindowDays ?? companyReopenDays
const reopenDays = resolveReopenWindowDays(effectiveReopenDays)
const reopenDeadline = computeReopenDeadline(now, reopenDays) const reopenDeadline = computeReopenDeadline(now, reopenDays)
const normalizedStatus = "RESOLVED" const normalizedStatus = "RESOLVED"
const relatedIdList = Array.from( const relatedIdList = Array.from(

View file

@ -238,6 +238,7 @@ function emptyCompany(tenantId: string): CompanyFormValues {
customFields: [], customFields: [],
notes: null, notes: null,
isAvulso: false, isAvulso: false,
reopenWindowDays: 7,
} }
} }
@ -1232,18 +1233,37 @@ export function CompanySheet({ tenantId, editor, onClose, onCreated, onUpdated }
/> />
<FieldError error={form.formState.errors.address?.message as string | undefined} /> <FieldError error={form.formState.errors.address?.message as string | undefined} />
</div> </div>
<div className="space-y-2"> <div className="grid gap-x-4 gap-y-3 md:grid-cols-2">
<Label htmlFor="contractedHoursPerMonth">Horas contratadas / mês</Label> <div className="space-y-2">
<Input <Label htmlFor="contractedHoursPerMonth">Horas contratadas / mês</Label>
id="contractedHoursPerMonth" <Input
type="number" id="contractedHoursPerMonth"
min={0} type="number"
step="0.5" min={0}
{...form.register("contractedHoursPerMonth", { valueAsNumber: true })} step="0.5"
/> {...form.register("contractedHoursPerMonth", { valueAsNumber: true })}
<FieldError />
error={form.formState.errors.contractedHoursPerMonth?.message as string | undefined} <FieldError
/> error={form.formState.errors.contractedHoursPerMonth?.message as string | undefined}
/>
</div>
<div className="space-y-2">
<Label htmlFor="reopenWindowDays">Prazo de reabertura (dias)</Label>
<Input
id="reopenWindowDays"
type="number"
min={1}
max={90}
placeholder="14"
{...form.register("reopenWindowDays", { valueAsNumber: true })}
/>
<p className="text-xs text-muted-foreground">
Dias que o cliente pode reabrir tickets resolvidos.
</p>
<FieldError
error={form.formState.errors.reopenWindowDays?.message as string | undefined}
/>
</div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Checkbox id="isAvulso" checked={form.watch("isAvulso")} onCheckedChange={(checked) => form.setValue("isAvulso", Boolean(checked))} /> <Checkbox id="isAvulso" checked={form.watch("isAvulso")} onCheckedChange={(checked) => form.setValue("isAvulso", Boolean(checked))} />

View file

@ -545,8 +545,7 @@ export function CloseTicketDialog({
Boolean(selectedTemplateId) || Boolean(selectedTemplateId) ||
shouldAdjustTime || shouldAdjustTime ||
adjustReason.trim().length > 0 || adjustReason.trim().length > 0 ||
linkedReference.trim().length > 0 || linkedReference.trim().length > 0
reopenWindowDays !== "14"
const canSaveDraft = hasFormChanges && !isSubmitting const canSaveDraft = hasFormChanges && !isSubmitting
@ -651,12 +650,10 @@ export function CloseTicketDialog({
onWorkSummaryAdjusted?.(result) onWorkSummaryAdjusted?.(result)
} }
const reopenDaysNumber = Number(reopenWindowDays)
await resolveTicketMutation({ await resolveTicketMutation({
ticketId: ticketId as unknown as Id<"tickets">, ticketId: ticketId as unknown as Id<"tickets">,
actorId, actorId,
resolvedWithTicketId: linkedTicketCandidate ? (linkedTicketCandidate.id as Id<"tickets">) : undefined, resolvedWithTicketId: linkedTicketCandidate ? (linkedTicketCandidate.id as Id<"tickets">) : undefined,
reopenWindowDays: Number.isFinite(reopenDaysNumber) ? reopenDaysNumber : undefined,
}) })
await addComment({ await addComment({
ticketId: ticketId as unknown as Id<"tickets">, ticketId: ticketId as unknown as Id<"tickets">,
@ -886,21 +883,6 @@ export function CloseTicketDialog({
</p> </p>
) : null} ) : null}
</div> </div>
<div className="space-y-2">
<Label htmlFor="reopen-window" className="text-sm font-medium text-neutral-800">
Reabertura permitida
</Label>
<Select value={reopenWindowDays} onValueChange={setReopenWindowDays} disabled={isSubmitting}>
<SelectTrigger id="reopen-window">
<SelectValue placeholder="Escolha o prazo" />
</SelectTrigger>
<SelectContent>
<SelectItem value="7">7 dias</SelectItem>
<SelectItem value="14">14 dias</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-neutral-500">Após esse período o ticket não poderá ser reaberto automaticamente.</p>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -384,6 +384,7 @@ export const companyFormSchema = z.object({
customFields: z.array(customFieldSchema).default([]), customFields: z.array(customFieldSchema).default([]),
notes: z.string().trim().nullable().optional(), notes: z.string().trim().nullable().optional(),
isAvulso: z.boolean().default(false), isAvulso: z.boolean().default(false),
reopenWindowDays: z.number().int().min(1).max(90).nullable().optional(),
}) })
export type CompanyFormValues = z.infer<typeof companyFormSchema> export type CompanyFormValues = z.infer<typeof companyFormSchema>