feat: status + queue updates, filters e UI

- Status renomeados e cores (Em andamento azul, Pausado amarelo)
- Transições automáticas: iniciar=Em andamento, pausar=Pausado
- Fila padrão: Chamados ao criar ticket
- Admin/Empresas: renomeia ‘Slug’ → ‘Apelido’ + mensagens
- Dashboard: últimos tickets priorizam sem responsável (mais antigos)
- Tickets: filtro por responsável + salvar filtro por usuário
- Encerrar ticket: adiciona botão ‘Cancelar’
- Strings atualizadas (PDF, relatórios, badges)
This commit is contained in:
codex-bot 2025-10-20 14:57:22 -03:00
parent e91192a1f6
commit 5535ba81e6
19 changed files with 399 additions and 86 deletions

View file

@ -104,6 +104,17 @@ export function NewTicketDialog({ triggerClassName }: { triggerClassName?: strin
setAssigneeInitialized(true)
}, [open, assigneeInitialized, convexUserId, form])
// Default queue to "Chamados" if available when opening
useEffect(() => {
if (!open) return
const current = form.getValues("queueName")
if (current) return
const hasChamados = queues.some((q) => q.name === "Chamados")
if (hasChamados) {
form.setValue("queueName", "Chamados", { shouldDirty: false, shouldTouch: false })
}
}, [open, queues, form])
const handleCategoryChange = (value: string) => {
const previous = form.getValues("categoryId") ?? ""
const next = value ?? ""
@ -166,6 +177,13 @@ export function NewTicketDialog({ triggerClassName }: { triggerClassName?: strin
})
const summaryFallback = values.summary?.trim() ?? ""
const bodyHtml = plainDescription.length > 0 ? sanitizedDescription : summaryFallback
const MAX_COMMENT_CHARS = 20000
const plainForLimit = (plainDescription.length > 0 ? plainDescription : summaryFallback).trim()
if (plainForLimit.length > MAX_COMMENT_CHARS) {
toast.error(`Descrição muito longa (máx. ${MAX_COMMENT_CHARS} caracteres)`, { id: "new-ticket" })
setLoading(false)
return
}
if (attachments.length > 0 || bodyHtml.trim().length > 0) {
const typedAttachments = attachments.map((a) => ({
storageId: a.storageId as unknown as Id<"_storage">,
@ -254,9 +272,15 @@ export function NewTicketDialog({ triggerClassName }: { triggerClassName?: strin
<FieldLabel htmlFor="summary">Resumo</FieldLabel>
<textarea
id="summary"
className="min-h-[96px] w-full rounded-lg border border-slate-300 bg-background p-3 text-sm shadow-sm focus-visible:border-[#00d6eb] focus-visible:outline-none"
className="min-h-[96px] w-full resize-none overflow-hidden rounded-lg border border-slate-300 bg-background p-3 text-sm shadow-sm focus-visible:border-[#00d6eb] focus-visible:outline-none"
maxLength={600}
{...form.register("summary")}
placeholder="Explique em poucas linhas o contexto do chamado."
onInput={(e) => {
const el = e.currentTarget
el.style.height = 'auto'
el.style.height = `${el.scrollHeight}px`
}}
/>
</Field>
<Field>