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,6 +14,7 @@
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, {});
@@ -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
@@ -56,7 +58,7 @@
} }
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;
@@ -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,6 +24,7 @@
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, {});
@@ -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
@@ -56,7 +68,7 @@
} }
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;
@@ -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,6 +24,7 @@
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, {});
@@ -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
@@ -56,7 +68,7 @@
} }
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;
@@ -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[] { function parseLista(input: string): string[] {
return input return input
.split(/[;,\n]/) .split(/[;,\n]/)
.map((v) => v.trim()) .map((v) => v.trim())
.filter((v) => v.length > 0); .filter((v) => v.length > 0);
} }
async function salvar() { async function salvar() {
if (!currentUser.data) { if (!currentUser.data) {
mostrarMensagem("error", "Usuário não autenticado."); mostrarMensagem('error', 'Usuário não autenticado.');
return; return;
} }
if (!codigo.trim() || !nome.trim() || !titulo.trim() || !corpo.trim()) { if (!codigo.trim() || !nome.trim() || !titulo.trim() || !corpo.trim()) {
mostrarMensagem("error", "Preencha todos os campos obrigatórios."); mostrarMensagem('error', 'Preencha todos os campos obrigatórios.');
return; return;
} }
const codigoNormalizado = codigo.trim().toUpperCase().replace(/\s+/g, "_"); const codigoNormalizado = codigo.trim().toUpperCase().replace(/\s+/g, '_');
const variaveis = parseLista(variaveisTexto); const variaveis = parseLista(variaveisTexto);
const tags = parseLista(tagsTexto); const tags = parseLista(tagsTexto);
try { try {
criando = true; criando = true;
const resultado = await client.mutation( const resultado = await client.mutation(api.templatesMensagens.criarTemplate, {
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) { } catch (error) {
const erro = error instanceof Error ? error.message : "Erro desconhecido"; const erro = error instanceof Error ? error.message : 'Erro desconhecido';
mostrarMensagem("error", `Erro ao criar template: ${erro}`); mostrarMensagem('error', `Erro ao criar template: ${erro}`);
} finally { } finally {
criando = false; 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,7 +120,7 @@ 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"> <div class="form-control">
@@ -177,13 +172,13 @@ async function salvar() {
<option value="chat">Chat</option> <option value="chat">Chat</option>
<option value="ambos">Ambos</option> <option value="ambos">Ambos</option>
</select> </select>
<label class="label"> <div class="label">
<span class="label-text-alt"> <span class="label-text-alt">
<span class="font-semibold">Chat:</span> usa o texto puro do corpo. <span class="font-semibold" <span class="font-semibold">Chat:</span> usa o texto puro do corpo.
>Email:</span <span class="font-semibold">Email:</span> usa uma versão HTML profissional gerada automaticamente
> usa uma versão HTML profissional gerada automaticamente com cabeçalho e assinatura SGSE. com cabeçalho e assinatura SGSE.
</span> </span>
</label> </div>
</div> </div>
<div class="form-control"> <div class="form-control">
@@ -196,13 +191,14 @@ async function salvar() {
class="textarea textarea-bordered h-40" 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." placeholder="Digite o conteúdo em TEXTO. Você pode usar &#123;&#123;variavel&#125;&#125; para valores dinâmicos."
></textarea> ></textarea>
<label class="label"> <div class="label">
<span class="label-text-alt"> <span class="label-text-alt">
Este texto será usado diretamente nas mensagens de <span class="font-semibold">chat</span>. Este texto será usado diretamente nas mensagens de <span class="font-semibold"
Para <span class="font-semibold">email</span>, o sistema gera automaticamente um layout HTML >chat</span
padronizado com logo e assinatura. >. Para <span class="font-semibold">email</span>, o sistema gera automaticamente um
layout HTML padronizado com logo e assinatura.
</span> </span>
</label> </div>
</div> </div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
@@ -251,4 +247,5 @@ async function salvar() {
</div> </div>
</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"
} }