first-deploy #72
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
@@ -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 {{variavel}} 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 {{variavel}} 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>
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user