first-deploy #72

Merged
kilder merged 12 commits from first-deploy into master 2026-01-13 17:54:12 +00:00
8 changed files with 237 additions and 220 deletions
Showing only changes of commit 42b62b7959 - Show all commits

View File

@@ -35,21 +35,17 @@ FROM oven/bun:1-slim AS production
# Set working directory to match builder structure # Set working directory to match builder structure
WORKDIR /app WORKDIR /app
# Create non-root user
RUN addgroup --system --gid 1001 sveltekit
RUN adduser --system --uid 1001 sveltekit
# Copy root node_modules (contains hoisted dependencies) # Copy root node_modules (contains hoisted dependencies)
COPY --from=builder --chown=sveltekit:sveltekit /app/node_modules ./node_modules COPY --from=builder /app/node_modules ./node_modules
# Copy built application and workspace files # Copy built application and workspace files
COPY --from=builder --chown=sveltekit:sveltekit /app/apps/web/build ./apps/web/build COPY --from=builder /app/apps/web/build ./apps/web/build
COPY --from=builder --chown=sveltekit:sveltekit /app/apps/web/package.json ./apps/web/package.json COPY --from=builder /app/apps/web/package.json ./apps/web/package.json
# Copy workspace node_modules (contains symlinks to root node_modules) # Copy workspace node_modules (contains symlinks to root node_modules)
COPY --from=builder --chown=sveltekit:sveltekit /app/apps/web/node_modules ./apps/web/node_modules COPY --from=builder /app/apps/web/node_modules ./apps/web/node_modules
# Copy any additional files needed for runtime # Copy any additional files needed for runtime
COPY --from=builder --chown=sveltekit:sveltekit /app/apps/web/static ./apps/web/static COPY --from=builder /app/apps/web/static ./apps/web/static
# Switch to non-root user # Switch to non-root user
USER sveltekit USER sveltekit

View File

@@ -5,7 +5,6 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import AprovarAusencias from '$lib/components/AprovarAusencias.svelte'; import AprovarAusencias from '$lib/components/AprovarAusencias.svelte';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { parseLocalDate } from '$lib/utils/datas'; import { parseLocalDate } from '$lib/utils/datas';
import jsPDF from 'jspdf'; import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable'; import autoTable from 'jspdf-autotable';
@@ -15,13 +14,14 @@
import logoGovPE from '$lib/assets/logo_governo_PE.png'; import logoGovPE from '$lib/assets/logo_governo_PE.png';
import { FileDown, FileSpreadsheet } from 'lucide-svelte'; import { FileDown, FileSpreadsheet } from 'lucide-svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { SvelteDate } from 'svelte/reactivity';
const client = useConvexClient(); const client = useConvexClient();
const currentUser = useQuery(api.auth.getCurrentUser, {}); const currentUser = useQuery(api.auth.getCurrentUser, {});
// Buscar TODAS as solicitações de ausências // Buscar TODAS as solicitações de ausências
const todasAusenciasQuery = useQuery(api.ausencias.listarTodas, {}); const todasAusenciasQuery = useQuery(api.ausencias.listarTodas, {});
// Buscar funcionários para filtro // Buscar funcionários para filtro
const funcionariosQuery = useQuery(api.funcionarios.getAll, {}); const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
@@ -34,7 +34,9 @@
const ausencias = $derived(todasAusenciasQuery?.data || []); const ausencias = $derived(todasAusenciasQuery?.data || []);
const funcionarios = $derived( const funcionarios = $derived(
Array.isArray(funcionariosQuery?.data) ? funcionariosQuery.data : funcionariosQuery?.data?.data || [] Array.isArray(funcionariosQuery?.data)
? funcionariosQuery.data
: funcionariosQuery?.data?.data || []
); );
// Filtrar solicitações // Filtrar solicitações
@@ -42,26 +44,26 @@
ausencias.filter((a) => { ausencias.filter((a) => {
// Filtro de status // Filtro de status
if (filtroStatus !== 'todos' && a.status !== filtroStatus) return false; if (filtroStatus !== 'todos' && a.status !== filtroStatus) return false;
// Filtro por funcionário // Filtro por funcionário
if (filtroFuncionario) { if (filtroFuncionario) {
if (a.funcionario?._id !== filtroFuncionario) return false; if (a.funcionario?._id !== filtroFuncionario) return false;
} }
// Filtro por período // Filtro por período
if (filtroPeriodoInicio) { if (filtroPeriodoInicio) {
const inicioFiltro = new Date(filtroPeriodoInicio); const inicioFiltro = new Date(filtroPeriodoInicio);
const inicioAusencia = parseLocalDate(a.dataInicio); const inicioAusencia = parseLocalDate(a.dataInicio);
if (inicioAusencia < inicioFiltro) return false; if (inicioAusencia < inicioFiltro) return false;
} }
if (filtroPeriodoFim) { if (filtroPeriodoFim) {
const fimFiltro = new Date(filtroPeriodoFim); const fimFiltro = new SvelteDate(filtroPeriodoFim);
fimFiltro.setHours(23, 59, 59, 999); // Incluir o dia inteiro fimFiltro.setHours(23, 59, 59, 999); // Incluir o dia inteiro
const fimAusencia = parseLocalDate(a.dataFim); const fimAusencia = parseLocalDate(a.dataFim);
if (fimAusencia > fimFiltro) return false; if (fimAusencia > fimFiltro) return false;
} }
return true; return true;
}) })
); );
@@ -690,7 +692,7 @@
bind:value={filtroFuncionario} bind:value={filtroFuncionario}
> >
<option value="">Todos</option> <option value="">Todos</option>
{#each funcionarios as funcionario} {#each funcionarios as funcionario (funcionario._id)}
<option value={funcionario._id}>{funcionario.nome}</option> <option value={funcionario._id}>{funcionario.nome}</option>
{/each} {/each}
</select> </select>
@@ -761,7 +763,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each ausenciasFiltradas as ausencia} {#each ausenciasFiltradas as ausencia (ausencia._id)}
<tr> <tr>
<td class="font-semibold"> <td class="font-semibold">
{ausencia.funcionario?.nome || 'N/A'} {ausencia.funcionario?.nome || 'N/A'}
@@ -769,7 +771,7 @@
<td> <td>
{#if ausencia.time} {#if ausencia.time}
<div <div
class="badge badge-sm font-semibold max-w-full overflow-hidden text-ellipsis whitespace-nowrap" class="badge badge-sm max-w-full overflow-hidden font-semibold text-ellipsis whitespace-nowrap"
style="background-color: {ausencia.time.cor}20; border-color: {ausencia.time style="background-color: {ausencia.time.cor}20; border-color: {ausencia.time
.cor}; color: {ausencia.time.cor}" .cor}; color: {ausencia.time.cor}"
title={ausencia.time.nome} title={ausencia.time.nome}

View File

@@ -1,12 +1,21 @@
<script lang="ts"> <script lang="ts">
import { api } from '@sgse-app/backend/convex/_generated/api'; import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { useConvexClient, useQuery } from 'convex-svelte'; import { useConvexClient, useQuery } from 'convex-svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import AprovarAusencias from '$lib/components/AprovarAusencias.svelte'; import AprovarAusencias from '$lib/components/AprovarAusencias.svelte';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { Clock, ArrowLeft, FileText, CheckCircle, XCircle, Info, Eye, FileDown, FileSpreadsheet } from 'lucide-svelte'; import {
Clock,
ArrowLeft,
FileText,
CheckCircle,
XCircle,
Info,
Eye,
FileDown,
FileSpreadsheet
} from 'lucide-svelte';
import { parseLocalDate } from '$lib/utils/datas'; import { parseLocalDate } from '$lib/utils/datas';
import jsPDF from 'jspdf'; import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable'; import autoTable from 'jspdf-autotable';
@@ -15,13 +24,14 @@
import ExcelJS from 'exceljs'; import ExcelJS from 'exceljs';
import logoGovPE from '$lib/assets/logo_governo_PE.png'; import logoGovPE from '$lib/assets/logo_governo_PE.png';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { SvelteDate } from 'svelte/reactivity';
const client = useConvexClient(); const client = useConvexClient();
const currentUser = useQuery(api.auth.getCurrentUser, {}); const currentUser = useQuery(api.auth.getCurrentUser, {});
// Buscar TODAS as solicitações de ausências (Dashboard RH) // Buscar TODAS as solicitações de ausências (Dashboard RH)
const todasAusenciasQuery = useQuery(api.ausencias.listarTodas, {}); const todasAusenciasQuery = useQuery(api.ausencias.listarTodas, {});
// Buscar funcionários para filtro // Buscar funcionários para filtro
const funcionariosQuery = useQuery(api.funcionarios.getAll, {}); const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
@@ -34,7 +44,9 @@
const ausencias = $derived(todasAusenciasQuery?.data || []); const ausencias = $derived(todasAusenciasQuery?.data || []);
const funcionarios = $derived( const funcionarios = $derived(
Array.isArray(funcionariosQuery?.data) ? funcionariosQuery.data : funcionariosQuery?.data?.data || [] Array.isArray(funcionariosQuery?.data)
? funcionariosQuery.data
: funcionariosQuery?.data?.data || []
); );
// Filtrar solicitações // Filtrar solicitações
@@ -42,26 +54,26 @@
ausencias.filter((a) => { ausencias.filter((a) => {
// Filtro de status // Filtro de status
if (filtroStatus !== 'todos' && a.status !== filtroStatus) return false; if (filtroStatus !== 'todos' && a.status !== filtroStatus) return false;
// Filtro por funcionário // Filtro por funcionário
if (filtroFuncionario) { if (filtroFuncionario) {
if (a.funcionario?._id !== filtroFuncionario) return false; if (a.funcionario?._id !== filtroFuncionario) return false;
} }
// Filtro por período // Filtro por período
if (filtroPeriodoInicio) { if (filtroPeriodoInicio) {
const inicioFiltro = new Date(filtroPeriodoInicio); const inicioFiltro = new Date(filtroPeriodoInicio);
const inicioAusencia = parseLocalDate(a.dataInicio); const inicioAusencia = parseLocalDate(a.dataInicio);
if (inicioAusencia < inicioFiltro) return false; if (inicioAusencia < inicioFiltro) return false;
} }
if (filtroPeriodoFim) { if (filtroPeriodoFim) {
const fimFiltro = new Date(filtroPeriodoFim); const fimFiltro = new SvelteDate(filtroPeriodoFim);
fimFiltro.setHours(23, 59, 59, 999); // Incluir o dia inteiro fimFiltro.setHours(23, 59, 59, 999); // Incluir o dia inteiro
const fimAusencia = parseLocalDate(a.dataFim); const fimAusencia = parseLocalDate(a.dataFim);
if (fimAusencia > fimFiltro) return false; if (fimAusencia > fimFiltro) return false;
} }
return true; return true;
}) })
); );
@@ -679,7 +691,7 @@
<td> <td>
{#if ausencia.time} {#if ausencia.time}
<div <div
class="badge badge-sm font-semibold max-w-full overflow-hidden text-ellipsis whitespace-nowrap" class="badge badge-sm max-w-full overflow-hidden font-semibold text-ellipsis whitespace-nowrap"
style="background-color: {ausencia.time.cor}20; border-color: {ausencia.time style="background-color: {ausencia.time.cor}20; border-color: {ausencia.time
.cor}; color: {ausencia.time.cor}" .cor}; color: {ausencia.time.cor}"
title={ausencia.time.nome} title={ausencia.time.nome}

View File

@@ -6,7 +6,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import PrintModal from '$lib/components/PrintModal.svelte'; import PrintModal from '$lib/components/PrintModal.svelte';
import { Users, Plus, Filter, X, Inbox, MoreVertical, XCircle } from 'lucide-svelte'; import { Users, Plus, Filter, X, MoreVertical, XCircle } from 'lucide-svelte';
const client = useConvexClient(); const client = useConvexClient();

View File

@@ -2,17 +2,15 @@
import { api } from '@sgse-app/backend/convex/_generated/api'; import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { useConvexClient, useQuery } from 'convex-svelte'; import { useConvexClient, useQuery } from 'convex-svelte';
import { Edit, MapPin, Plus, Search, Trash2, X } from 'lucide-svelte'; import { Edit, Info, MapPin, Plus, Search, Trash2, X } from 'lucide-svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; import { page } from '$app/state';
import { MapPin, Plus, X, Edit, Trash2, Search, Info } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
const client = useConvexClient(); const client = useConvexClient();
let funcionarioId = $derived($page.params.funcionarioId as Id<'funcionarios'>); let funcionarioId = $derived(page.params.funcionarioId as Id<'funcionarios'>);
// Queries // Queries
const funcionarioQuery = useQuery( const funcionarioQuery = useQuery(

View File

@@ -5,8 +5,17 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import AprovarAusencias from '$lib/components/AprovarAusencias.svelte'; import AprovarAusencias from '$lib/components/AprovarAusencias.svelte';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; import {
import { Clock, ArrowLeft, FileText, CheckCircle, XCircle, Info, Eye, FileDown, FileSpreadsheet } from 'lucide-svelte'; Clock,
ArrowLeft,
FileText,
CheckCircle,
XCircle,
Info,
Eye,
FileDown,
FileSpreadsheet
} from 'lucide-svelte';
import { parseLocalDate } from '$lib/utils/datas'; import { parseLocalDate } from '$lib/utils/datas';
import jsPDF from 'jspdf'; import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable'; import autoTable from 'jspdf-autotable';
@@ -15,13 +24,14 @@
import ExcelJS from 'exceljs'; import ExcelJS from 'exceljs';
import logoGovPE from '$lib/assets/logo_governo_PE.png'; import logoGovPE from '$lib/assets/logo_governo_PE.png';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { SvelteDate } from 'svelte/reactivity';
const client = useConvexClient(); const client = useConvexClient();
const currentUser = useQuery(api.auth.getCurrentUser, {}); const currentUser = useQuery(api.auth.getCurrentUser, {});
// Buscar TODAS as solicitações de ausências // Buscar TODAS as solicitações de ausências
const todasAusenciasQuery = useQuery(api.ausencias.listarTodas, {}); const todasAusenciasQuery = useQuery(api.ausencias.listarTodas, {});
// Buscar funcionários para filtro // Buscar funcionários para filtro
const funcionariosQuery = useQuery(api.funcionarios.getAll, {}); const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
@@ -34,7 +44,9 @@
let ausencias = $derived(todasAusenciasQuery?.data || []); let ausencias = $derived(todasAusenciasQuery?.data || []);
let funcionarios = $derived( let funcionarios = $derived(
Array.isArray(funcionariosQuery?.data) ? funcionariosQuery.data : funcionariosQuery?.data?.data || [] Array.isArray(funcionariosQuery?.data)
? funcionariosQuery.data
: funcionariosQuery?.data?.data || []
); );
// Filtrar solicitações // Filtrar solicitações
@@ -42,26 +54,26 @@
ausencias.filter((a) => { ausencias.filter((a) => {
// Filtro de status // Filtro de status
if (filtroStatus !== 'todos' && a.status !== filtroStatus) return false; if (filtroStatus !== 'todos' && a.status !== filtroStatus) return false;
// Filtro por funcionário // Filtro por funcionário
if (filtroFuncionario) { if (filtroFuncionario) {
if (a.funcionario?._id !== filtroFuncionario) return false; if (a.funcionario?._id !== filtroFuncionario) return false;
} }
// Filtro por período // Filtro por período
if (filtroPeriodoInicio) { if (filtroPeriodoInicio) {
const inicioFiltro = new Date(filtroPeriodoInicio); const inicioFiltro = new Date(filtroPeriodoInicio);
const inicioAusencia = parseLocalDate(a.dataInicio); const inicioAusencia = parseLocalDate(a.dataInicio);
if (inicioAusencia < inicioFiltro) return false; if (inicioAusencia < inicioFiltro) return false;
} }
if (filtroPeriodoFim) { if (filtroPeriodoFim) {
const fimFiltro = new Date(filtroPeriodoFim); const fimFiltro = new SvelteDate(filtroPeriodoFim);
fimFiltro.setHours(23, 59, 59, 999); // Incluir o dia inteiro fimFiltro.setHours(23, 59, 59, 999); // Incluir o dia inteiro
const fimAusencia = parseLocalDate(a.dataFim); const fimAusencia = parseLocalDate(a.dataFim);
if (fimAusencia > fimFiltro) return false; if (fimAusencia > fimFiltro) return false;
} }
return true; return true;
}) })
); );
@@ -612,7 +624,7 @@
bind:value={filtroFuncionario} bind:value={filtroFuncionario}
> >
<option value="">Todos</option> <option value="">Todos</option>
{#each funcionarios as funcionario} {#each funcionarios as funcionario (funcionario._id)}
<option value={funcionario._id}>{funcionario.nome}</option> <option value={funcionario._id}>{funcionario.nome}</option>
{/each} {/each}
</select> </select>
@@ -671,7 +683,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each ausenciasFiltradas as ausencia} {#each ausenciasFiltradas as ausencia (ausencia._id)}
<tr> <tr>
<td class="font-semibold"> <td class="font-semibold">
{ausencia.funcionario?.nome || 'N/A'} {ausencia.funcionario?.nome || 'N/A'}

View File

@@ -7,58 +7,54 @@
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import { FileText } from 'lucide-svelte'; import { FileText } from 'lucide-svelte';
const client = useConvexClient(); const client = useConvexClient();
const currentUser = useQuery( const currentUser = useQuery(api.auth.getCurrentUser as FunctionReference<'query'>);
api.auth.getCurrentUser as FunctionReference<"query">,
);
let codigo = $state(""); let codigo = $state('');
let nome = $state(""); let nome = $state('');
let titulo = $state(""); let titulo = $state('');
let corpo = $state(""); let corpo = $state('');
let categoria = $state<"email" | "chat" | "ambos">("email"); let categoria = $state<'email' | 'chat' | 'ambos'>('email');
let variaveisTexto = $state(""); let variaveisTexto = $state('');
let tagsTexto = $state(""); let tagsTexto = $state('');
let criando = $state(false); let criando = $state(false);
let mensagem = $state<{ let mensagem = $state<{
tipo: "success" | "error" | "info"; tipo: 'success' | 'error' | 'info';
texto: string; texto: string;
} | null>(null); } | null>(null);
function mostrarMensagem(tipo: "success" | "error" | "info", texto: string) { function mostrarMensagem(tipo: 'success' | 'error' | 'info', texto: string) {
mensagem = { tipo, texto }; mensagem = { tipo, texto };
setTimeout(() => { setTimeout(() => {
mensagem = null; mensagem = null;
}, 5000); }, 5000);
}
function parseLista(input: string): string[] {
return input
.split(/[;,\n]/)
.map((v) => v.trim())
.filter((v) => v.length > 0);
}
async function salvar() {
if (!currentUser.data) {
mostrarMensagem("error", "Usuário não autenticado.");
return;
} }
if (!codigo.trim() || !nome.trim() || !titulo.trim() || !corpo.trim()) { function parseLista(input: string): string[] {
mostrarMensagem("error", "Preencha todos os campos obrigatórios."); return input
return; .split(/[;,\n]/)
.map((v) => v.trim())
.filter((v) => v.length > 0);
} }
const codigoNormalizado = codigo.trim().toUpperCase().replace(/\s+/g, "_"); async function salvar() {
const variaveis = parseLista(variaveisTexto); if (!currentUser.data) {
const tags = parseLista(tagsTexto); mostrarMensagem('error', 'Usuário não autenticado.');
return;
}
try { if (!codigo.trim() || !nome.trim() || !titulo.trim() || !corpo.trim()) {
criando = true; mostrarMensagem('error', 'Preencha todos os campos obrigatórios.');
const resultado = await client.mutation( return;
api.templatesMensagens.criarTemplate, }
{
const codigoNormalizado = codigo.trim().toUpperCase().replace(/\s+/g, '_');
const variaveis = parseLista(variaveisTexto);
const tags = parseLista(tagsTexto);
try {
criando = true;
const resultado = await client.mutation(api.templatesMensagens.criarTemplate, {
codigo: codigoNormalizado, codigo: codigoNormalizado,
nome: nome.trim(), nome: nome.trim(),
titulo: titulo.trim(), titulo: titulo.trim(),
@@ -66,39 +62,38 @@ async function salvar() {
variaveis, variaveis,
categoria, categoria,
tags, tags,
criadoPorId: currentUser.data._id as Id<"usuarios">, criadoPorId: currentUser.data._id as Id<'usuarios'>
}, });
);
if (resultado.sucesso) { if (resultado.sucesso) {
mostrarMensagem("success", "Template criado com sucesso!"); mostrarMensagem('success', 'Template criado com sucesso!');
await goto(resolve("/ti/notificacoes/templates")); await goto(resolve('/ti/notificacoes/templates'));
} else { } else {
mostrarMensagem("error", resultado.erro || "Erro ao criar template."); mostrarMensagem('error', resultado.erro || 'Erro ao criar template.');
}
} catch (error) {
const erro = error instanceof Error ? error.message : 'Erro desconhecido';
mostrarMensagem('error', `Erro ao criar template: ${erro}`);
} finally {
criando = false;
} }
} catch (error) {
const erro = error instanceof Error ? error.message : "Erro desconhecido";
mostrarMensagem("error", `Erro ao criar template: ${erro}`);
} finally {
criando = false;
} }
}
</script> </script>
<div class="container mx-auto max-w-4xl px-4 py-8"> <div class="container mx-auto max-w-4xl px-4 py-8">
<div <div
class="rounded-2xl bg-base-100/80 shadow-xl border border-base-200/60 p-6 lg:p-8 space-y-6 backdrop-blur" class="bg-base-100/80 border-base-200/60 space-y-6 rounded-2xl border p-6 shadow-xl backdrop-blur lg:p-8"
> >
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="bg-gradient-to-br from-info/15 via-primary/10 to-secondary/10 rounded-2xl p-3"> <div class="from-info/15 via-primary/10 to-secondary/10 rounded-2xl bg-linear-to-br p-3">
<FileText class="text-info h-9 w-9" strokeWidth={2} /> <FileText class="text-info h-9 w-9" strokeWidth={2} />
</div> </div>
<div> <div>
<h1 class="text-base-content text-3xl font-bold">Novo Template</h1> <h1 class="text-base-content text-3xl font-bold">Novo Template</h1>
<p class="text-base-content/60 mt-1 text-sm lg:text-base"> <p class="text-base-content/60 mt-1 text-sm lg:text-base">
Defina o texto base que será usado em <span class="font-semibold">chat</span> e na versão Defina o texto base que será usado em <span class="font-semibold">chat</span> e na
HTML de <span class="font-semibold">email</span> com o estilo padrão do SGSE. versão HTML de <span class="font-semibold">email</span> com o estilo padrão do SGSE.
</p> </p>
</div> </div>
</div> </div>
@@ -125,129 +120,131 @@ async function salvar() {
</div> </div>
{/if} {/if}
<div class="card bg-base-100 shadow-sm border border-base-200"> <div class="card bg-base-100 border-base-200 border shadow-sm">
<div class="card-body space-y-4"> <div class="card-body space-y-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control">
<label class="label" for="codigo">
<span class="label-text font-medium">Código *</span>
<span class="label-text-alt">Ex: AVISO_IMPORTANTE</span>
</label>
<input
id="codigo"
type="text"
bind:value={codigo}
class="input input-bordered"
maxlength="50"
/>
</div>
<div class="form-control">
<label class="label" for="nome">
<span class="label-text font-medium">Nome *</span>
</label>
<input
id="nome"
type="text"
bind:value={nome}
class="input input-bordered"
maxlength="100"
/>
</div>
</div>
<div class="form-control"> <div class="form-control">
<label class="label" for="codigo"> <label class="label" for="titulo">
<span class="label-text font-medium">Código *</span> <span class="label-text font-medium">Título *</span>
<span class="label-text-alt">Ex: AVISO_IMPORTANTE</span>
</label> </label>
<input <input
id="codigo" id="titulo"
type="text" type="text"
bind:value={codigo} bind:value={titulo}
class="input input-bordered" class="input input-bordered"
maxlength="50" maxlength="200"
/> />
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label" for="nome"> <label class="label" for="categoria">
<span class="label-text font-medium">Nome *</span> <span class="label-text font-medium">Categoria</span>
</label> </label>
<input <select id="categoria" bind:value={categoria} class="select select-bordered max-w-xs">
id="nome" <option value="email">Email</option>
type="text" <option value="chat">Chat</option>
bind:value={nome} <option value="ambos">Ambos</option>
class="input input-bordered" </select>
maxlength="100" <div class="label">
/>
</div>
</div>
<div class="form-control">
<label class="label" for="titulo">
<span class="label-text font-medium">Título *</span>
</label>
<input
id="titulo"
type="text"
bind:value={titulo}
class="input input-bordered"
maxlength="200"
/>
</div>
<div class="form-control">
<label class="label" for="categoria">
<span class="label-text font-medium">Categoria</span>
</label>
<select id="categoria" bind:value={categoria} class="select select-bordered max-w-xs">
<option value="email">Email</option>
<option value="chat">Chat</option>
<option value="ambos">Ambos</option>
</select>
<label class="label">
<span class="label-text-alt">
<span class="font-semibold">Chat:</span> usa o texto puro do corpo. <span class="font-semibold"
>Email:</span
> usa uma versão HTML profissional gerada automaticamente com cabeçalho e assinatura SGSE.
</span>
</label>
</div>
<div class="form-control">
<label class="label" for="corpo">
<span class="label-text font-medium">Corpo da Mensagem *</span>
</label>
<textarea
id="corpo"
bind:value={corpo}
class="textarea textarea-bordered h-40"
placeholder="Digite o conteúdo em TEXTO. Você pode usar &#123;&#123;variavel&#125;&#125; para valores dinâmicos."
></textarea>
<label class="label">
<span class="label-text-alt">
Este texto será usado diretamente nas mensagens de <span class="font-semibold">chat</span>.
Para <span class="font-semibold">email</span>, o sistema gera automaticamente um layout HTML
padronizado com logo e assinatura.
</span>
</label>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control">
<label class="label" for="variaveis">
<span class="label-text font-medium">Variáveis (opcional)</span>
</label>
<input
id="variaveis"
type="text"
bind:value={variaveisTexto}
class="input input-bordered"
placeholder="nome, data, valor"
/>
<label class="label" for="variaveis">
<span class="label-text-alt"> <span class="label-text-alt">
Liste as variáveis que podem ser usadas no corpo (separadas por vírgula ou ponto e <span class="font-semibold">Chat:</span> usa o texto puro do corpo.
vírgula). <span class="font-semibold">Email:</span> usa uma versão HTML profissional gerada automaticamente
com cabeçalho e assinatura SGSE.
</span> </span>
</label> </div>
</div> </div>
<div class="form-control">
<label class="label" for="tags">
<span class="label-text font-medium">Tags (opcional)</span>
</label>
<input
id="tags"
type="text"
bind:value={tagsTexto}
class="input input-bordered"
placeholder="avisos, chamados, rh"
/>
</div>
</div>
<div class="mt-6 flex justify-end gap-3"> <div class="form-control">
<a href={resolve('/ti/notificacoes/templates')} class="btn btn-ghost"> Cancelar </a> <label class="label" for="corpo">
<button class="btn btn-primary" onclick={salvar} disabled={criando}> <span class="label-text font-medium">Corpo da Mensagem *</span>
{#if criando} </label>
<span class="loading loading-spinner loading-sm"></span> <textarea
Salvando... id="corpo"
{:else} bind:value={corpo}
Salvar Template class="textarea textarea-bordered h-40"
{/if} placeholder="Digite o conteúdo em TEXTO. Você pode usar &#123;&#123;variavel&#125;&#125; para valores dinâmicos."
</button> ></textarea>
<div class="label">
<span class="label-text-alt">
Este texto será usado diretamente nas mensagens de <span class="font-semibold"
>chat</span
>. Para <span class="font-semibold">email</span>, o sistema gera automaticamente um
layout HTML padronizado com logo e assinatura.
</span>
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control">
<label class="label" for="variaveis">
<span class="label-text font-medium">Variáveis (opcional)</span>
</label>
<input
id="variaveis"
type="text"
bind:value={variaveisTexto}
class="input input-bordered"
placeholder="nome, data, valor"
/>
<label class="label" for="variaveis">
<span class="label-text-alt">
Liste as variáveis que podem ser usadas no corpo (separadas por vírgula ou ponto e
vírgula).
</span>
</label>
</div>
<div class="form-control">
<label class="label" for="tags">
<span class="label-text font-medium">Tags (opcional)</span>
</label>
<input
id="tags"
type="text"
bind:value={tagsTexto}
class="input input-bordered"
placeholder="avisos, chamados, rh"
/>
</div>
</div>
<div class="mt-6 flex justify-end gap-3">
<a href={resolve('/ti/notificacoes/templates')} class="btn btn-ghost"> Cancelar </a>
<button class="btn btn-primary" onclick={salvar} disabled={criando}>
{#if criando}
<span class="loading loading-spinner loading-sm"></span>
Salvando...
{:else}
Salvar Template
{/if}
</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -48,5 +48,5 @@
"svelte-chartjs": "^3.1.5", "svelte-chartjs": "^3.1.5",
"svelte-sonner": "^1.0.7" "svelte-sonner": "^1.0.7"
}, },
"packageManager": "bun@1.3.4" "packageManager": "bun@1.3.5"
} }