Files
sgse-app/apps/web/src/routes/(dashboard)/abrir-chamado/+page.svelte

220 lines
6.9 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import { useConvexClient } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
import TicketForm from '$lib/components/chamados/TicketForm.svelte';
import TicketTimeline from '$lib/components/chamados/TicketTimeline.svelte';
import { chamadosStore } from '$lib/stores/chamados';
import { resolve } from '$app/paths';
import { useConvexWithAuth } from '$lib/hooks/useConvexWithAuth';
import { LifeBuoy, Info } from 'lucide-svelte';
type Ticket = Doc<'tickets'>;
const client = useConvexClient();
let submitLoading = $state(false);
let resetSignal = $state(0);
let feedback = $state<{ tipo: 'success' | 'error'; mensagem: string; numero?: string } | null>(
null
);
const exemploTimeline = $state<NonNullable<Ticket['timeline']>>([
{
etapa: 'abertura',
status: 'concluido',
prazo: Date.now(),
concluidoEm: Date.now(),
observacao: 'Chamado criado'
},
{
etapa: 'resposta_inicial',
status: 'pendente',
prazo: Date.now() + 4 * 60 * 60 * 1000
},
{
etapa: 'conclusao',
status: 'pendente',
prazo: Date.now() + 24 * 60 * 60 * 1000
}
]);
$effect(() => {
// Garante que o cliente Convex use o token do usuário logado
useConvexWithAuth();
});
async function uploadArquivo(file: File) {
const uploadUrl = await client.mutation(api.chamados.generateUploadUrl, {});
const response = await fetch(uploadUrl, {
method: 'POST',
headers: { 'Content-Type': file.type },
body: file
});
const data = await response.json();
if (!data?.storageId) {
throw new Error('Falha ao enviar arquivo. Tente novamente.');
}
return {
arquivoId: data.storageId as Id<'_storage'>,
nome: file.name,
tipo: file.type,
tamanho: file.size
};
}
async function handleSubmit(event: CustomEvent<{ values: any }>) {
const { values } = event.detail;
try {
submitLoading = true;
feedback = null;
const anexos = [];
for (const file of values.anexos ?? []) {
const uploaded = await uploadArquivo(file);
anexos.push(uploaded);
}
const resultado = await client.mutation(api.chamados.abrirChamado, {
titulo: values.titulo,
descricao: values.descricao,
tipo: values.tipo,
categoria: values.categoria,
prioridade: values.prioridade,
canalOrigem: values.canalOrigem,
anexos
});
feedback = {
tipo: 'success',
mensagem: 'Chamado registrado com sucesso! Você pode acompanhar pelo seu perfil.',
numero: resultado.numero
};
resetSignal = resetSignal + 1;
// Atualizar store local
const novoTicket = await client.query(api.chamados.obterChamado, {
ticketId: resultado.ticketId
});
if (novoTicket?.ticket) {
chamadosStore.upsertTicket(novoTicket.ticket);
chamadosStore.setDetalhe(resultado.ticketId, novoTicket);
}
} catch (error) {
const mensagem =
error instanceof Error ? error.message : 'Erro ao enviar o chamado. Tente novamente.';
feedback = {
tipo: 'error',
mensagem
};
} finally {
submitLoading = false;
}
}
</script>
<main class="mx-auto w-full max-w-6xl space-y-10 px-4 py-8">
<section
class="border-primary/30 from-primary/10 via-base-100 to-secondary/20 relative overflow-hidden rounded-3xl border bg-linear-to-br p-10 shadow-2xl"
>
<div class="bg-primary/20 absolute top-0 -left-16 h-52 w-52 rounded-full blur-3xl"></div>
<div class="bg-secondary/20 absolute right-0 -bottom-20 h-64 w-64 rounded-full blur-3xl"></div>
<div class="relative z-10 space-y-4">
<span
class="border-primary/40 bg-primary/10 text-primary inline-flex items-center gap-2 rounded-full border px-4 py-1 text-xs font-semibold tracking-[0.28em] uppercase"
>
Central de Chamados
</span>
<div class="flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
<div class="max-w-3xl space-y-4">
<h1 class="text-base-content text-4xl leading-tight font-black sm:text-5xl">
Abrir novo chamado
</h1>
<p class="text-base-content/70 text-base sm:text-lg">
Registre reclamações, sugestões, elogios ou chamados técnicos. Toda interação gera
notificações automáticas via e-mail e chat com a assinatura do SGSE - Sistema de
Gerenciamento de Secretaria.
</p>
<div class="text-base-content/70 flex flex-wrap gap-3 text-sm">
<span class="badge badge-success badge-sm">Resposta ágil</span>
<span class="badge badge-info badge-sm">Timeline com SLA</span>
<span class="badge badge-warning badge-sm">Alertas de vencimento</span>
</div>
</div>
<a href={resolve('/perfil/chamados')} class="btn btn-outline btn-sm">
Acompanhar meus chamados
</a>
</div>
</div>
</section>
{#if feedback}
<div class={`alert ${feedback.tipo === 'success' ? 'alert-success' : 'alert-error'} shadow-lg`}>
<div>
<span class="font-semibold">{feedback.mensagem}</span>
{#if feedback.numero}
<p class="text-sm">Número do ticket: {feedback.numero}</p>
{/if}
</div>
</div>
{/if}
<div class="grid gap-8 lg:grid-cols-3">
<div class="lg:col-span-2">
<div
class="border-primary/20 from-base-100 via-base-100/95 to-primary/5 rounded-3xl border-2 bg-gradient-to-br p-8 shadow-xl"
>
<div class="mb-6 flex items-center gap-3">
<div class="bg-primary/10 rounded-xl p-2">
<LifeBuoy class="text-primary h-6 w-6" strokeWidth={2} />
</div>
<div>
<h2 class="text-base-content text-2xl font-bold">Formulário</h2>
<p class="text-base-content/60 text-sm">
Informe os detalhes para que nossa equipe possa priorizar o atendimento.
</p>
</div>
</div>
<div class="mt-6">
{#if resetSignal % 2 === 0}
<TicketForm loading={submitLoading} on:submit={handleSubmit} />
{:else}
<TicketForm loading={submitLoading} on:submit={handleSubmit} />
{/if}
</div>
</div>
</div>
<aside class="space-y-6">
<div
class="border-info/20 from-base-100 via-base-100/95 to-info/5 rounded-3xl border-2 bg-gradient-to-br p-6 shadow-lg"
>
<div class="mb-4 flex items-center gap-3">
<div class="bg-info/10 rounded-xl p-2">
<Info class="text-info h-5 w-5" strokeWidth={2} />
</div>
<h3 class="text-base-content text-lg font-bold">Como funciona a timeline</h3>
</div>
<div class="bg-base-200/50 mb-4 space-y-2 rounded-xl p-4">
<p class="text-base-content/80 text-sm font-medium">
Todas as etapas do ticket são monitoradas automaticamente.
</p>
<p class="text-base-content/60 text-xs">
Os prazos mudam de cor conforme o SLA: <span class="text-success">dentro do prazo</span
>, <span class="text-warning">próximo ao vencimento</span> ou
<span class="text-error">vencido</span>.
</p>
</div>
<div class="border-base-300 bg-base-100/80 rounded-xl border p-4">
<TicketTimeline timeline={exemploTimeline} />
</div>
</div>
</aside>
</div>
</main>