Merge pull request #72 from killer-cf/first-deploy

chore: update package manager to bun@1.3.5 and streamline Dockerfile …
This commit is contained in:
Kilder Costa
2026-01-12 09:35:00 -03:00
committed by GitHub
8 changed files with 237 additions and 220 deletions

View File

@@ -35,21 +35,17 @@ FROM oven/bun:1-slim AS production
# Set working directory to match builder structure
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 --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 --from=builder --chown=sveltekit:sveltekit /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/build ./apps/web/build
COPY --from=builder /app/apps/web/package.json ./apps/web/package.json
# 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 --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
USER sveltekit

View File

@@ -5,7 +5,6 @@
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import AprovarAusencias from '$lib/components/AprovarAusencias.svelte';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { parseLocalDate } from '$lib/utils/datas';
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
@@ -15,6 +14,7 @@
import logoGovPE from '$lib/assets/logo_governo_PE.png';
import { FileDown, FileSpreadsheet } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import { SvelteDate } from 'svelte/reactivity';
const client = useConvexClient();
const currentUser = useQuery(api.auth.getCurrentUser, {});
@@ -34,7 +34,9 @@
const ausencias = $derived(todasAusenciasQuery?.data || []);
const funcionarios = $derived(
Array.isArray(funcionariosQuery?.data) ? funcionariosQuery.data : funcionariosQuery?.data?.data || []
Array.isArray(funcionariosQuery?.data)
? funcionariosQuery.data
: funcionariosQuery?.data?.data || []
);
// Filtrar solicitações
@@ -56,7 +58,7 @@
}
if (filtroPeriodoFim) {
const fimFiltro = new Date(filtroPeriodoFim);
const fimFiltro = new SvelteDate(filtroPeriodoFim);
fimFiltro.setHours(23, 59, 59, 999); // Incluir o dia inteiro
const fimAusencia = parseLocalDate(a.dataFim);
if (fimAusencia > fimFiltro) return false;
@@ -690,7 +692,7 @@
bind:value={filtroFuncionario}
>
<option value="">Todos</option>
{#each funcionarios as funcionario}
{#each funcionarios as funcionario (funcionario._id)}
<option value={funcionario._id}>{funcionario.nome}</option>
{/each}
</select>
@@ -761,7 +763,7 @@
</tr>
</thead>
<tbody>
{#each ausenciasFiltradas as ausencia}
{#each ausenciasFiltradas as ausencia (ausencia._id)}
<tr>
<td class="font-semibold">
{ausencia.funcionario?.nome || 'N/A'}
@@ -769,7 +771,7 @@
<td>
{#if ausencia.time}
<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
.cor}; color: {ausencia.time.cor}"
title={ausencia.time.nome}

View File

@@ -1,12 +1,21 @@
<script lang="ts">
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 { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import AprovarAusencias from '$lib/components/AprovarAusencias.svelte';
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 jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
@@ -15,6 +24,7 @@
import ExcelJS from 'exceljs';
import logoGovPE from '$lib/assets/logo_governo_PE.png';
import { toast } from 'svelte-sonner';
import { SvelteDate } from 'svelte/reactivity';
const client = useConvexClient();
const currentUser = useQuery(api.auth.getCurrentUser, {});
@@ -34,7 +44,9 @@
const ausencias = $derived(todasAusenciasQuery?.data || []);
const funcionarios = $derived(
Array.isArray(funcionariosQuery?.data) ? funcionariosQuery.data : funcionariosQuery?.data?.data || []
Array.isArray(funcionariosQuery?.data)
? funcionariosQuery.data
: funcionariosQuery?.data?.data || []
);
// Filtrar solicitações
@@ -56,7 +68,7 @@
}
if (filtroPeriodoFim) {
const fimFiltro = new Date(filtroPeriodoFim);
const fimFiltro = new SvelteDate(filtroPeriodoFim);
fimFiltro.setHours(23, 59, 59, 999); // Incluir o dia inteiro
const fimAusencia = parseLocalDate(a.dataFim);
if (fimAusencia > fimFiltro) return false;
@@ -679,7 +691,7 @@
<td>
{#if ausencia.time}
<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
.cor}; color: {ausencia.time.cor}"
title={ausencia.time.nome}

View File

@@ -6,7 +6,7 @@
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
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();

View File

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

View File

@@ -5,8 +5,17 @@
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import AprovarAusencias from '$lib/components/AprovarAusencias.svelte';
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 jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
@@ -15,6 +24,7 @@
import ExcelJS from 'exceljs';
import logoGovPE from '$lib/assets/logo_governo_PE.png';
import { toast } from 'svelte-sonner';
import { SvelteDate } from 'svelte/reactivity';
const client = useConvexClient();
const currentUser = useQuery(api.auth.getCurrentUser, {});
@@ -34,7 +44,9 @@
let ausencias = $derived(todasAusenciasQuery?.data || []);
let funcionarios = $derived(
Array.isArray(funcionariosQuery?.data) ? funcionariosQuery.data : funcionariosQuery?.data?.data || []
Array.isArray(funcionariosQuery?.data)
? funcionariosQuery.data
: funcionariosQuery?.data?.data || []
);
// Filtrar solicitações
@@ -56,7 +68,7 @@
}
if (filtroPeriodoFim) {
const fimFiltro = new Date(filtroPeriodoFim);
const fimFiltro = new SvelteDate(filtroPeriodoFim);
fimFiltro.setHours(23, 59, 59, 999); // Incluir o dia inteiro
const fimAusencia = parseLocalDate(a.dataFim);
if (fimAusencia > fimFiltro) return false;
@@ -612,7 +624,7 @@
bind:value={filtroFuncionario}
>
<option value="">Todos</option>
{#each funcionarios as funcionario}
{#each funcionarios as funcionario (funcionario._id)}
<option value={funcionario._id}>{funcionario.nome}</option>
{/each}
</select>
@@ -671,7 +683,7 @@
</tr>
</thead>
<tbody>
{#each ausenciasFiltradas as ausencia}
{#each ausenciasFiltradas as ausencia (ausencia._id)}
<tr>
<td class="font-semibold">
{ausencia.funcionario?.nome || 'N/A'}

View File

@@ -7,58 +7,54 @@
import { resolve } from '$app/paths';
import { FileText } from 'lucide-svelte';
const client = useConvexClient();
const currentUser = useQuery(
api.auth.getCurrentUser as FunctionReference<"query">,
);
const client = useConvexClient();
const currentUser = useQuery(api.auth.getCurrentUser as FunctionReference<'query'>);
let codigo = $state("");
let nome = $state("");
let titulo = $state("");
let corpo = $state("");
let categoria = $state<"email" | "chat" | "ambos">("email");
let variaveisTexto = $state("");
let tagsTexto = $state("");
let criando = $state(false);
let mensagem = $state<{
tipo: "success" | "error" | "info";
texto: string;
} | null>(null);
let codigo = $state('');
let nome = $state('');
let titulo = $state('');
let corpo = $state('');
let categoria = $state<'email' | 'chat' | 'ambos'>('email');
let variaveisTexto = $state('');
let tagsTexto = $state('');
let criando = $state(false);
let mensagem = $state<{
tipo: 'success' | 'error' | 'info';
texto: string;
} | null>(null);
function mostrarMensagem(tipo: "success" | "error" | "info", texto: string) {
mensagem = { tipo, texto };
setTimeout(() => {
mensagem = null;
}, 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;
function mostrarMensagem(tipo: 'success' | 'error' | 'info', texto: string) {
mensagem = { tipo, texto };
setTimeout(() => {
mensagem = null;
}, 5000);
}
if (!codigo.trim() || !nome.trim() || !titulo.trim() || !corpo.trim()) {
mostrarMensagem("error", "Preencha todos os campos obrigatórios.");
return;
function parseLista(input: string): string[] {
return input
.split(/[;,\n]/)
.map((v) => v.trim())
.filter((v) => v.length > 0);
}
const codigoNormalizado = codigo.trim().toUpperCase().replace(/\s+/g, "_");
const variaveis = parseLista(variaveisTexto);
const tags = parseLista(tagsTexto);
async function salvar() {
if (!currentUser.data) {
mostrarMensagem('error', 'Usuário não autenticado.');
return;
}
try {
criando = true;
const resultado = await client.mutation(
api.templatesMensagens.criarTemplate,
{
if (!codigo.trim() || !nome.trim() || !titulo.trim() || !corpo.trim()) {
mostrarMensagem('error', 'Preencha todos os campos obrigatórios.');
return;
}
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,
nome: nome.trim(),
titulo: titulo.trim(),
@@ -66,39 +62,38 @@ async function salvar() {
variaveis,
categoria,
tags,
criadoPorId: currentUser.data._id as Id<"usuarios">,
},
);
criadoPorId: currentUser.data._id as Id<'usuarios'>
});
if (resultado.sucesso) {
mostrarMensagem("success", "Template criado com sucesso!");
await goto(resolve("/ti/notificacoes/templates"));
} else {
mostrarMensagem("error", resultado.erro || "Erro ao criar template.");
if (resultado.sucesso) {
mostrarMensagem('success', 'Template criado com sucesso!');
await goto(resolve('/ti/notificacoes/templates'));
} else {
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>
<div class="container mx-auto max-w-4xl px-4 py-8">
<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="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} />
</div>
<div>
<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">
Defina o texto base que será usado em <span class="font-semibold">chat</span> e na versão
HTML de <span class="font-semibold">email</span> com o estilo padrão do SGSE.
Defina o texto base que será usado em <span class="font-semibold">chat</span> e na
versão HTML de <span class="font-semibold">email</span> com o estilo padrão do SGSE.
</p>
</div>
</div>
@@ -125,129 +120,131 @@ async function salvar() {
</div>
{/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="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">
<label class="label" for="codigo">
<span class="label-text font-medium">Código *</span>
<span class="label-text-alt">Ex: AVISO_IMPORTANTE</span>
<label class="label" for="titulo">
<span class="label-text font-medium">Título *</span>
</label>
<input
id="codigo"
id="titulo"
type="text"
bind:value={codigo}
bind:value={titulo}
class="input input-bordered"
maxlength="50"
maxlength="200"
/>
</div>
<div class="form-control">
<label class="label" for="nome">
<span class="label-text font-medium">Nome *</span>
<label class="label" for="categoria">
<span class="label-text font-medium">Categoria</span>
</label>
<input
id="nome"
type="text"
bind:value={nome}
class="input input-bordered"
maxlength="100"
/>
</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">
<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>
<div class="label">
<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 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>
<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 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>
<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>

View File

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