refactor: update menu and routing for ticket management

- Replaced references to "Solicitar Acesso" with "Abrir Chamado" across the application for consistency in terminology.
- Updated routing logic to reflect the new ticket management flow, ensuring that the dashboard and sidebar components point to the correct paths.
- Removed the obsolete "Solicitar Acesso" page, streamlining the user experience and reducing unnecessary navigation options.
- Enhanced backend schema to support new ticket functionalities, including ticket creation and management.
This commit is contained in:
2025-11-14 22:50:03 -03:00
parent 9b3b095c01
commit 118051ad56
17 changed files with 2353 additions and 358 deletions

View File

@@ -0,0 +1,249 @@
<script lang="ts">
import type { Doc, Id } from "@sgse-app/backend/convex/_generated/dataModel";
import { createEventDispatcher } from "svelte";
type SlaConfig = Doc<"slaConfigs">;
interface FormValues {
titulo: string;
descricao: string;
tipo: Doc<"tickets">["tipo"];
prioridade: Doc<"tickets">["prioridade"];
categoria: string;
slaConfigId?: Id<"slaConfigs">;
canalOrigem?: string;
anexos: File[];
}
interface Props {
slaConfigs?: Array<SlaConfig>;
loading?: boolean;
}
const dispatch = createEventDispatcher<{ submit: { values: FormValues } }>();
const props = $props<Props>();
const slaConfigs = $derived<Array<SlaConfig>>(props.slaConfigs ?? []);
const loading = $derived(props.loading ?? false);
let titulo = $state("");
let descricao = $state("");
let tipo = $state<Doc<"tickets">["tipo"]>("chamado");
let prioridade = $state<Doc<"tickets">["prioridade"]>("media");
let categoria = $state("");
let slaConfigId = $state<Id<"slaConfigs"> | "">("");
let canalOrigem = $state("Portal SGSE");
let anexos = $state<Array<File>>([]);
let errors = $state<Record<string, string>>({});
function validate(): boolean {
const novoErros: Record<string, string> = {};
if (!titulo.trim()) novoErros.titulo = "Informe um título para o chamado.";
if (!descricao.trim()) novoErros.descricao = "Descrição é obrigatória.";
if (!categoria.trim()) novoErros.categoria = "Informe uma categoria.";
errors = novoErros;
return Object.keys(novoErros).length === 0;
}
function handleFiles(event: Event) {
const target = event.target as HTMLInputElement;
const files = Array.from(target.files ?? []);
anexos = files.slice(0, 5); // limitar para 5 anexos
}
function removeFile(index: number) {
anexos = anexos.filter((_, idx) => idx !== index);
}
function resetForm() {
titulo = "";
descricao = "";
categoria = "";
tipo = "chamado";
prioridade = "media";
anexos = [];
errors = {};
}
function handleSubmit(event: SubmitEvent) {
event.preventDefault();
if (!validate()) return;
const slaSelecionada =
(slaConfigId && slaConfigId !== "" ? (slaConfigId as Id<"slaConfigs">) : slaConfigs[0]?._id) ??
undefined;
dispatch("submit", {
values: {
titulo: titulo.trim(),
descricao: descricao.trim(),
tipo,
prioridade,
categoria: categoria.trim(),
slaConfigId: slaSelecionada,
canalOrigem,
anexos,
},
});
}
</script>
<form class="space-y-8" onsubmit={handleSubmit}>
<section class="grid gap-6 md:grid-cols-2">
<div class="form-control md:col-span-2">
<label class="label">
<span class="label-text font-semibold">Título do chamado</span>
</label>
<input
type="text"
class="input input-bordered input-primary w-full"
placeholder="Ex: Erro ao acessar o módulo de licitações"
bind:value={titulo}
/>
{#if errors.titulo}
<span class="text-error mt-1 text-sm">{errors.titulo}</span>
{/if}
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Tipo de solicitação</span>
</label>
<div class="grid gap-2">
{#each ["chamado", "reclamacao", "elogio", "sugestao"] as opcao}
<label class="btn btn-outline btn-sm justify-start gap-2">
<input
type="radio"
name="tipo"
class="radio radio-primary"
value={opcao}
checked={tipo === opcao}
onclick={() => (tipo = opcao as typeof tipo)}
/>
{opcao.charAt(0).toUpperCase() + opcao.slice(1)}
</label>
{/each}
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Prioridade</span>
</label>
<select class="select select-bordered w-full" bind:value={prioridade}>
<option value="baixa">Baixa</option>
<option value="media">Média</option>
<option value="alta">Alta</option>
<option value="critica">Crítica</option>
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Categoria</span>
</label>
<input
type="text"
class="input input-bordered w-full"
placeholder="Ex: Infraestrutura, Sistemas, Acesso"
bind:value={categoria}
/>
{#if errors.categoria}
<span class="text-error mt-1 text-sm">{errors.categoria}</span>
{/if}
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Configuração de SLA</span>
</label>
<select class="select select-bordered w-full" bind:value={slaConfigId}>
{#each slaConfigs as sla (sla._id)}
<option value={sla._id}>
{sla.nome} • Resp. {sla.tempoRespostaHoras}h • Conc.
{sla.tempoConclusaoHoras}h
</option>
{:else}
<option value="">Padrão (24h)</option>
{/each}
</select>
</div>
</section>
<section class="form-control">
<label class="label">
<span class="label-text font-semibold">Descrição detalhada</span>
</label>
<textarea
class="textarea textarea-bordered textarea-lg min-h-[180px]"
placeholder="Descreva o problema, erro ou sugestão com o máximo de detalhes possível."
bind:value={descricao}
></textarea>
{#if errors.descricao}
<span class="text-error mt-1 text-sm">{errors.descricao}</span>
{/if}
</section>
<section class="space-y-4">
<div class="flex items-center justify-between">
<div>
<p class="font-semibold text-base-content">Anexos (opcional)</p>
<p class="text-base-content/60 text-sm">
Suporte a PDF e imagens (máx. 10MB por arquivo)
</p>
</div>
<label class="btn btn-outline btn-sm">
Selecionar arquivos
<input type="file" class="hidden" multiple accept=".pdf,.png,.jpg,.jpeg" onchange={handleFiles} />
</label>
</div>
{#if anexos.length > 0}
<div class="space-y-2 rounded-2xl border border-base-200 bg-base-100/70 p-4">
{#each anexos as file, index (file.name + index)}
<div class="flex items-center justify-between gap-3 rounded-xl border border-base-200 bg-base-100 px-3 py-2">
<div>
<p class="text-sm font-medium">{file.name}</p>
<p class="text-xs text-base-content/60">
{(file.size / 1024 / 1024).toFixed(2)} MB • {file.type}
</p>
</div>
<button
type="button"
class="btn btn-ghost btn-sm text-error"
onclick={() => removeFile(index)}
>
Remover
</button>
</div>
{/each}
</div>
{:else}
<div class="rounded-2xl border border-dashed border-base-300 bg-base-100/50 p-6 text-center text-sm text-base-content/60">
Nenhum arquivo selecionado.
</div>
{/if}
</section>
<section class="flex flex-wrap gap-3">
<button
type="submit"
class="btn btn-primary flex-1 min-w-[200px]"
disabled={loading}
>
{#if loading}
<span class="loading loading-spinner loading-sm"></span>
Enviando...
{:else}
Registrar chamado
{/if}
</button>
<button
type="button"
class="btn btn-ghost"
onclick={resetForm}
disabled={loading}
>
Limpar
</button>
</section>
</form>