- Changed all instances of "Sistema de Gerenciamento da Secretaria de Esportes" to "Sistema de Gerenciamento de Secretaria" for a more concise branding. - Enhanced the PrintModal component with a user-friendly interface for selecting sections to include in PDF generation. - Improved error handling and user feedback during PDF generation processes. - Updated various components and routes to reflect the new branding, ensuring consistency across the application.
309 lines
9.9 KiB
Svelte
309 lines
9.9 KiB
Svelte
<script lang="ts">
|
|
import type { Doc } from "@sgse-app/backend/convex/_generated/dataModel";
|
|
import { createEventDispatcher } from "svelte";
|
|
|
|
interface FormValues {
|
|
titulo: string;
|
|
descricao: string;
|
|
tipo: Doc<"tickets">["tipo"];
|
|
prioridade: Doc<"tickets">["prioridade"];
|
|
categoria: string;
|
|
canalOrigem?: string;
|
|
anexos: File[];
|
|
}
|
|
|
|
interface Props {
|
|
loading?: boolean;
|
|
}
|
|
|
|
const dispatch = createEventDispatcher<{ submit: { values: FormValues } }>();
|
|
const props = $props<Props>();
|
|
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 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;
|
|
|
|
dispatch("submit", {
|
|
values: {
|
|
titulo: titulo.trim(),
|
|
descricao: descricao.trim(),
|
|
tipo,
|
|
prioridade,
|
|
categoria: categoria.trim(),
|
|
canalOrigem,
|
|
anexos,
|
|
},
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<form class="space-y-8" onsubmit={handleSubmit}>
|
|
<!-- Título do Chamado -->
|
|
<section class="form-control">
|
|
<label class="label">
|
|
<span class="label-text font-semibold text-base-content">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}
|
|
</section>
|
|
|
|
<!-- Tipo de Solicitação e Prioridade -->
|
|
<section class="grid gap-6 md:grid-cols-2">
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text font-semibold text-base-content">Tipo de solicitação</span>
|
|
</label>
|
|
<div class="grid grid-cols-2 gap-2 rounded-xl border border-base-300 bg-base-200/30 p-3">
|
|
{#each [
|
|
{ value: "chamado", label: "Chamado", icon: "📋" },
|
|
{ value: "reclamacao", label: "Reclamação", icon: "⚠️" },
|
|
{ value: "elogio", label: "Elogio", icon: "⭐" },
|
|
{ value: "sugestao", label: "Sugestão", icon: "💡" }
|
|
] as opcao}
|
|
<label
|
|
class={`flex w-full cursor-pointer items-center justify-start gap-2 rounded-lg border-2 p-2.5 transition-all ${
|
|
tipo === opcao.value
|
|
? "border-primary bg-primary/10 shadow-md"
|
|
: "border-base-300 bg-base-100 hover:border-primary/50 hover:bg-base-200/50"
|
|
}`}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name="tipo"
|
|
class="radio radio-primary radio-sm shrink-0"
|
|
value={opcao.value}
|
|
checked={tipo === opcao.value}
|
|
onclick={() => (tipo = opcao.value as typeof tipo)}
|
|
/>
|
|
<span class="text-base shrink-0">{opcao.icon}</span>
|
|
<span class="text-sm font-medium flex-1 text-center">{opcao.label}</span>
|
|
</label>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text font-semibold text-base-content">Prioridade</span>
|
|
</label>
|
|
<div class="grid grid-cols-2 gap-2 rounded-xl border border-base-300 bg-base-200/30 p-3">
|
|
{#each [
|
|
{ value: "baixa", label: "Baixa", color: "badge-success" },
|
|
{ value: "media", label: "Média", color: "badge-info" },
|
|
{ value: "alta", label: "Alta", color: "badge-warning" },
|
|
{ value: "critica", label: "Crítica", color: "badge-error" }
|
|
] as opcao}
|
|
<label
|
|
class={`flex w-full cursor-pointer items-center justify-start gap-2 rounded-lg border-2 p-2.5 transition-all ${
|
|
prioridade === opcao.value
|
|
? "border-primary bg-primary/10 shadow-md"
|
|
: "border-base-300 bg-base-100 hover:border-primary/50 hover:bg-base-200/50"
|
|
}`}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name="prioridade"
|
|
class={`radio radio-sm shrink-0 ${
|
|
opcao.value === "baixa" ? "radio-success" :
|
|
opcao.value === "media" ? "radio-info" :
|
|
opcao.value === "alta" ? "radio-warning" :
|
|
"radio-error"
|
|
}`}
|
|
value={opcao.value}
|
|
checked={prioridade === opcao.value}
|
|
onclick={() => (prioridade = opcao.value as typeof prioridade)}
|
|
/>
|
|
<span class={`badge badge-sm ${opcao.color} flex-1 justify-center`}>{opcao.label}</span>
|
|
</label>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Categoria -->
|
|
<section class="form-control">
|
|
<label class="label">
|
|
<span class="label-text font-semibold text-base-content">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}
|
|
</section>
|
|
|
|
<!-- Descrição Detalhada -->
|
|
<section class="form-control">
|
|
<label class="label">
|
|
<span class="label-text font-semibold text-base-content">Descrição detalhada</span>
|
|
<span class="label-text-alt text-base-content/50">Obrigatório</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>
|
|
|
|
<!-- Anexos -->
|
|
<section class="space-y-4 rounded-xl border border-base-300 bg-base-200/30 p-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">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-4 w-4"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
|
/>
|
|
</svg>
|
|
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>
|
|
|
|
<!-- Ações do Formulário -->
|
|
<section class="flex flex-wrap gap-3 border-t border-base-300 pt-6">
|
|
<button
|
|
type="submit"
|
|
class="btn btn-primary flex-1 min-w-[200px] shadow-lg"
|
|
disabled={loading}
|
|
>
|
|
{#if loading}
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
Enviando...
|
|
{:else}
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-5 w-5"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
|
|
/>
|
|
</svg>
|
|
Registrar chamado
|
|
{/if}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost"
|
|
onclick={resetForm}
|
|
disabled={loading}
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-5 w-5"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M6 18L18 6M6 6l12 12"
|
|
/>
|
|
</svg>
|
|
Limpar
|
|
</button>
|
|
</section>
|
|
</form>
|
|
|