feat: update ESLint and TypeScript configurations across frontend and backend; enhance component structure and improve data handling in various modules
This commit is contained in:
@@ -1,238 +1,244 @@
|
||||
<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 { 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';
|
||||
|
||||
type Ticket = Doc<"tickets">;
|
||||
type Ticket = Doc<'tickets'>;
|
||||
|
||||
const client = useConvexClient();
|
||||
const client = useConvexClient();
|
||||
|
||||
let submitLoading = $state(false);
|
||||
let resetSignal = $state(0);
|
||||
let feedback = $state<{ tipo: "success" | "error"; mensagem: string; numero?: string } | null>(
|
||||
null,
|
||||
);
|
||||
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,
|
||||
},
|
||||
]);
|
||||
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();
|
||||
});
|
||||
$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, {});
|
||||
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 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.");
|
||||
}
|
||||
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,
|
||||
};
|
||||
}
|
||||
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;
|
||||
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 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,
|
||||
});
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
// 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="relative overflow-hidden rounded-3xl border border-primary/30 bg-linear-to-br from-primary/10 via-base-100 to-secondary/20 p-10 shadow-2xl"
|
||||
>
|
||||
<div class="absolute -left-16 top-0 h-52 w-52 rounded-full bg-primary/20 blur-3xl"></div>
|
||||
<div class="absolute -bottom-20 right-0 h-64 w-64 rounded-full bg-secondary/20 blur-3xl"></div>
|
||||
<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="inline-flex items-center gap-2 rounded-full border border-primary/40 bg-primary/10 px-4 py-1 text-xs font-semibold uppercase tracking-[0.28em] text-primary"
|
||||
>
|
||||
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-4xl font-black leading-tight text-base-content sm:text-5xl">
|
||||
Abrir novo chamado
|
||||
</h1>
|
||||
<p class="text-base text-base-content/70 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="flex flex-wrap gap-3 text-sm text-base-content/70">
|
||||
<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>
|
||||
<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}
|
||||
{#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="rounded-3xl border-2 border-primary/20 bg-gradient-to-br from-base-100 via-base-100/95 to-primary/5 p-8 shadow-xl">
|
||||
<div class="mb-6 flex items-center gap-3">
|
||||
<div class="rounded-xl bg-primary/10 p-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 text-primary"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-base-content">Formulário</h2>
|
||||
<p class="text-sm text-base-content/60">
|
||||
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>
|
||||
<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">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-primary h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</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="rounded-3xl border-2 border-info/20 bg-gradient-to-br from-base-100 via-base-100/95 to-info/5 p-6 shadow-lg">
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
<div class="rounded-xl bg-info/10 p-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 text-info"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-base-content">Como funciona a timeline</h3>
|
||||
</div>
|
||||
<div class="mb-4 space-y-2 rounded-xl bg-base-200/50 p-4">
|
||||
<p class="text-sm font-medium text-base-content/80">
|
||||
Todas as etapas do ticket são monitoradas automaticamente.
|
||||
</p>
|
||||
<p class="text-xs text-base-content/60">
|
||||
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="rounded-xl border border-base-300 bg-base-100/80 p-4">
|
||||
<TicketTimeline timeline={exemploTimeline} />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</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">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-info 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 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user