feat: enhance vacation management system with new employee association functionality, improved email notification handling, and comprehensive documentation; update dependencies and UI components for better user experience
This commit is contained in:
@@ -142,7 +142,7 @@
|
||||
</script>
|
||||
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<label class="label" for="file-upload-input">
|
||||
<span class="label-text font-medium flex items-center gap-2">
|
||||
{label}
|
||||
{#if helpUrl}
|
||||
@@ -164,6 +164,7 @@
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="file-upload-input"
|
||||
type="file"
|
||||
bind:this={fileInput}
|
||||
onchange={handleFileSelect}
|
||||
@@ -265,9 +266,9 @@
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-error">{error}</span>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
<!-- Badge de status online -->
|
||||
<div class="absolute top-1 right-1 w-3 h-3 bg-success rounded-full border-2 border-white shadow-lg" style="animation: pulse-dot 2s ease-in-out infinite;"></div>
|
||||
</button>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow-xl bg-base-100 rounded-box w-52 mt-4 border border-primary/20">
|
||||
<li class="menu-title">
|
||||
<span class="text-primary font-bold">{authStore.usuario?.nome}</span>
|
||||
@@ -470,6 +471,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<form method="dialog" class="modal-backdrop" onclick={closeLoginModal}>
|
||||
<button type="button">close</button>
|
||||
</form>
|
||||
|
||||
@@ -180,6 +180,7 @@
|
||||
</button>
|
||||
|
||||
{#if dropdownOpen}
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<div
|
||||
tabindex="0"
|
||||
class="dropdown-content z-50 mt-3 w-80 max-h-96 overflow-auto rounded-box bg-base-100 p-2 shadow-2xl border border-base-300"
|
||||
|
||||
@@ -99,19 +99,21 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="fixed inset-0 z-[100] flex items-center justify-center bg-black/50"
|
||||
onclick={onClose}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
onkeydown={(e) => e.key === 'Escape' && onClose()}
|
||||
tabindex="-1"
|
||||
>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="bg-base-100 rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col m-4"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
role="document"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
tabindex="-1"
|
||||
>
|
||||
<!-- Header ULTRA MODERNO -->
|
||||
<div class="flex items-center justify-between px-6 py-5 relative overflow-hidden" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);">
|
||||
@@ -181,10 +183,11 @@
|
||||
placeholder="Digite a mensagem..."
|
||||
bind:value={mensagem}
|
||||
maxlength="500"
|
||||
aria-describedby="char-count"
|
||||
></textarea>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">{mensagem.length}/500</span>
|
||||
</label>
|
||||
<div class="label">
|
||||
<span id="char-count" class="label-text-alt">{mensagem.length}/500</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
|
||||
393
apps/web/src/lib/components/ferias/CalendarioFerias.svelte
Normal file
393
apps/web/src/lib/components/ferias/CalendarioFerias.svelte
Normal file
@@ -0,0 +1,393 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { Calendar } from "@fullcalendar/core";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import multiMonthPlugin from "@fullcalendar/multimonth";
|
||||
import ptBrLocale from "@fullcalendar/core/locales/pt-br";
|
||||
|
||||
interface Props {
|
||||
periodosExistentes?: Array<{ dataInicio: string; dataFim: string; dias: number }>;
|
||||
onPeriodoAdicionado?: (periodo: { dataInicio: string; dataFim: string; dias: number }) => void;
|
||||
onPeriodoRemovido?: (index: number) => void;
|
||||
maxPeriodos?: number;
|
||||
minDiasPorPeriodo?: number;
|
||||
modoVisualizacao?: "month" | "multiMonth";
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
periodosExistentes = [],
|
||||
onPeriodoAdicionado,
|
||||
onPeriodoRemovido,
|
||||
maxPeriodos = 3,
|
||||
minDiasPorPeriodo = 5,
|
||||
modoVisualizacao = "month",
|
||||
readonly = false,
|
||||
}: Props = $props();
|
||||
|
||||
let calendarEl: HTMLDivElement;
|
||||
let calendar: Calendar | null = null;
|
||||
let selecaoInicio: Date | null = null;
|
||||
let eventos: any[] = $state([]);
|
||||
|
||||
// Cores dos períodos
|
||||
const coresPeriodos = [
|
||||
{ bg: "#667eea", border: "#5568d3", text: "#ffffff" }, // Roxo
|
||||
{ bg: "#f093fb", border: "#c75ce6", text: "#ffffff" }, // Rosa
|
||||
{ bg: "#4facfe", border: "#00c6ff", text: "#ffffff" }, // Azul
|
||||
];
|
||||
|
||||
// Converter períodos existentes em eventos
|
||||
function atualizarEventos() {
|
||||
eventos = periodosExistentes.map((periodo, index) => ({
|
||||
id: `periodo-${index}`,
|
||||
title: `Período ${index + 1} (${periodo.dias} dias)`,
|
||||
start: periodo.dataInicio,
|
||||
end: calcularDataFim(periodo.dataFim),
|
||||
backgroundColor: coresPeriodos[index % coresPeriodos.length].bg,
|
||||
borderColor: coresPeriodos[index % coresPeriodos.length].border,
|
||||
textColor: coresPeriodos[index % coresPeriodos.length].text,
|
||||
display: "block",
|
||||
extendedProps: {
|
||||
index,
|
||||
dias: periodo.dias,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
// Helper: Adicionar 1 dia à data fim (FullCalendar usa exclusive end)
|
||||
function calcularDataFim(dataFim: string): string {
|
||||
const data = new Date(dataFim);
|
||||
data.setDate(data.getDate() + 1);
|
||||
return data.toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
// Helper: Calcular dias entre datas (inclusivo)
|
||||
function calcularDias(inicio: Date, fim: Date): number {
|
||||
const diffTime = Math.abs(fim.getTime() - inicio.getTime());
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
||||
return diffDays;
|
||||
}
|
||||
|
||||
// Atualizar eventos quando períodos mudam
|
||||
$effect(() => {
|
||||
atualizarEventos();
|
||||
if (calendar) {
|
||||
calendar.removeAllEvents();
|
||||
calendar.addEventSource(eventos);
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (!calendarEl) return;
|
||||
|
||||
atualizarEventos();
|
||||
|
||||
calendar = new Calendar(calendarEl, {
|
||||
plugins: [dayGridPlugin, interactionPlugin, multiMonthPlugin],
|
||||
initialView: modoVisualizacao === "multiMonth" ? "multiMonthYear" : "dayGridMonth",
|
||||
locale: ptBrLocale,
|
||||
headerToolbar: {
|
||||
left: "prev,next today",
|
||||
center: "title",
|
||||
right: modoVisualizacao === "multiMonth" ? "multiMonthYear" : "dayGridMonth",
|
||||
},
|
||||
height: "auto",
|
||||
selectable: !readonly,
|
||||
selectMirror: true,
|
||||
unselectAuto: false,
|
||||
events: eventos,
|
||||
|
||||
// Estilo customizado
|
||||
buttonText: {
|
||||
today: "Hoje",
|
||||
month: "Mês",
|
||||
multiMonthYear: "Ano",
|
||||
},
|
||||
|
||||
// Seleção de período
|
||||
select: (info) => {
|
||||
if (readonly) return;
|
||||
|
||||
const inicio = new Date(info.startStr);
|
||||
const fim = new Date(info.endStr);
|
||||
fim.setDate(fim.getDate() - 1); // FullCalendar usa exclusive end
|
||||
|
||||
const dias = calcularDias(inicio, fim);
|
||||
|
||||
// Validar número de períodos
|
||||
if (periodosExistentes.length >= maxPeriodos) {
|
||||
alert(`Máximo de ${maxPeriodos} períodos permitidos`);
|
||||
calendar?.unselect();
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar mínimo de dias
|
||||
if (dias < minDiasPorPeriodo) {
|
||||
alert(`Período deve ter no mínimo ${minDiasPorPeriodo} dias`);
|
||||
calendar?.unselect();
|
||||
return;
|
||||
}
|
||||
|
||||
// Adicionar período
|
||||
const novoPeriodo = {
|
||||
dataInicio: info.startStr,
|
||||
dataFim: fim.toISOString().split("T")[0],
|
||||
dias,
|
||||
};
|
||||
|
||||
if (onPeriodoAdicionado) {
|
||||
onPeriodoAdicionado(novoPeriodo);
|
||||
}
|
||||
|
||||
calendar?.unselect();
|
||||
},
|
||||
|
||||
// Click em evento para remover
|
||||
eventClick: (info) => {
|
||||
if (readonly) return;
|
||||
|
||||
const index = info.event.extendedProps.index;
|
||||
if (
|
||||
confirm(
|
||||
`Deseja remover o Período ${index + 1} (${info.event.extendedProps.dias} dias)?`
|
||||
)
|
||||
) {
|
||||
if (onPeriodoRemovido) {
|
||||
onPeriodoRemovido(index);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Tooltip ao passar mouse
|
||||
eventDidMount: (info) => {
|
||||
info.el.title = `Click para remover\n${info.event.title}`;
|
||||
info.el.style.cursor = readonly ? "default" : "pointer";
|
||||
},
|
||||
|
||||
// Desabilitar datas passadas
|
||||
selectAllow: (selectInfo) => {
|
||||
const hoje = new Date();
|
||||
hoje.setHours(0, 0, 0, 0);
|
||||
return new Date(selectInfo.start) >= hoje;
|
||||
},
|
||||
|
||||
// Highlight de fim de semana
|
||||
dayCellClassNames: (arg) => {
|
||||
if (arg.date.getDay() === 0 || arg.date.getDay() === 6) {
|
||||
return ["fc-day-weekend-custom"];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
|
||||
return () => {
|
||||
calendar?.destroy();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="calendario-ferias-wrapper">
|
||||
<!-- Header com instruções -->
|
||||
{#if !readonly}
|
||||
<div class="alert alert-info mb-4 shadow-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-current shrink-0 w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<div class="text-sm">
|
||||
<p class="font-bold">Como usar:</p>
|
||||
<ul class="list-disc list-inside mt-1">
|
||||
<li>Clique e arraste no calendário para selecionar um período de férias</li>
|
||||
<li>Clique em um período colorido para removê-lo</li>
|
||||
<li>
|
||||
Você pode adicionar até {maxPeriodos} períodos (mínimo {minDiasPorPeriodo} dias cada)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Calendário -->
|
||||
<div
|
||||
bind:this={calendarEl}
|
||||
class="calendario-ferias shadow-2xl rounded-2xl overflow-hidden border-2 border-primary/10"
|
||||
></div>
|
||||
|
||||
<!-- Legenda de períodos -->
|
||||
{#if periodosExistentes.length > 0}
|
||||
<div class="mt-6 grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{#each periodosExistentes as periodo, index}
|
||||
<div
|
||||
class="stat bg-base-100 shadow-lg rounded-xl border-2 transition-all hover:scale-105"
|
||||
style="border-color: {coresPeriodos[index % coresPeriodos.length].border}"
|
||||
>
|
||||
<div
|
||||
class="stat-figure text-white w-12 h-12 rounded-full flex items-center justify-center text-xl font-bold"
|
||||
style="background: {coresPeriodos[index % coresPeriodos.length].bg}"
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
<div class="stat-title">Período {index + 1}</div>
|
||||
<div class="stat-value text-2xl" style="color: {coresPeriodos[index % coresPeriodos.length].bg}">
|
||||
{periodo.dias} dias
|
||||
</div>
|
||||
<div class="stat-desc">
|
||||
{new Date(periodo.dataInicio).toLocaleDateString("pt-BR")} até
|
||||
{new Date(periodo.dataFim).toLocaleDateString("pt-BR")}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Calendário Premium */
|
||||
.calendario-ferias {
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
/* Toolbar moderna */
|
||||
:global(.fc .fc-toolbar) {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 1rem;
|
||||
border-radius: 1rem 1rem 0 0;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
:global(.fc .fc-toolbar-title) {
|
||||
color: white !important;
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
:global(.fc .fc-button) {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3) !important;
|
||||
color: white !important;
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.fc .fc-button:hover) {
|
||||
background: rgba(255, 255, 255, 0.3) !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
:global(.fc .fc-button-active) {
|
||||
background: rgba(255, 255, 255, 0.4) !important;
|
||||
}
|
||||
|
||||
/* Cabeçalho dos dias */
|
||||
:global(.fc .fc-col-header-cell) {
|
||||
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 0.75rem 0.5rem;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
/* Células dos dias */
|
||||
:global(.fc .fc-daygrid-day) {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:global(.fc .fc-daygrid-day:hover) {
|
||||
background: rgba(102, 126, 234, 0.05);
|
||||
}
|
||||
|
||||
:global(.fc .fc-daygrid-day-number) {
|
||||
padding: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
/* Fim de semana */
|
||||
:global(.fc .fc-day-weekend-custom) {
|
||||
background: rgba(255, 193, 7, 0.05);
|
||||
}
|
||||
|
||||
/* Hoje */
|
||||
:global(.fc .fc-day-today) {
|
||||
background: rgba(102, 126, 234, 0.1) !important;
|
||||
border: 2px solid #667eea !important;
|
||||
}
|
||||
|
||||
/* Eventos (períodos selecionados) */
|
||||
:global(.fc .fc-event) {
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global(.fc .fc-event:hover) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
/* Seleção (arrastar) */
|
||||
:global(.fc .fc-highlight) {
|
||||
background: rgba(102, 126, 234, 0.3) !important;
|
||||
border: 2px dashed #667eea;
|
||||
}
|
||||
|
||||
/* Datas desabilitadas (passado) */
|
||||
:global(.fc .fc-day-past .fc-daygrid-day-number) {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* Remover bordas padrão */
|
||||
:global(.fc .fc-scrollgrid) {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
:global(.fc .fc-scrollgrid-section > td) {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* Grid moderno */
|
||||
:global(.fc .fc-daygrid-day-frame) {
|
||||
border: 1px solid #e9ecef;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
/* Responsivo */
|
||||
@media (max-width: 768px) {
|
||||
:global(.fc .fc-toolbar) {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
:global(.fc .fc-toolbar-title) {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
:global(.fc .fc-button) {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
394
apps/web/src/lib/components/ferias/DashboardFerias.svelte
Normal file
394
apps/web/src/lib/components/ferias/DashboardFerias.svelte
Normal file
@@ -0,0 +1,394 @@
|
||||
<script lang="ts">
|
||||
import { useQuery } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
interface Props {
|
||||
funcionarioId: Id<"funcionarios">;
|
||||
}
|
||||
|
||||
let { funcionarioId }: Props = $props();
|
||||
|
||||
// Queries
|
||||
const saldosQuery = useQuery(api.saldoFerias.listarSaldos, { funcionarioId });
|
||||
const solicitacoesQuery = useQuery(api.ferias.listarMinhasSolicitacoes, { funcionarioId });
|
||||
|
||||
const saldos = $derived(saldosQuery.data || []);
|
||||
const solicitacoes = $derived(solicitacoesQuery.data || []);
|
||||
|
||||
// Estatísticas derivadas
|
||||
const saldoAtual = $derived(saldos.find((s) => s.anoReferencia === new Date().getFullYear()));
|
||||
const totalSolicitacoes = $derived(solicitacoes.length);
|
||||
const aprovadas = $derived(solicitacoes.filter((s) => s.status === "aprovado" || s.status === "data_ajustada_aprovada").length);
|
||||
const pendentes = $derived(solicitacoes.filter((s) => s.status === "aguardando_aprovacao").length);
|
||||
const reprovadas = $derived(solicitacoes.filter((s) => s.status === "reprovado").length);
|
||||
|
||||
// Canvas para gráfico de pizza
|
||||
let canvasSaldo = $state<HTMLCanvasElement>();
|
||||
let canvasStatus = $state<HTMLCanvasElement>();
|
||||
|
||||
// Função para desenhar gráfico de pizza moderno
|
||||
function desenharGraficoPizza(
|
||||
canvas: HTMLCanvasElement,
|
||||
dados: { label: string; valor: number; cor: string }[]
|
||||
) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
const radius = Math.min(width, height) / 2 - 20;
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
const total = dados.reduce((acc, d) => acc + d.valor, 0);
|
||||
if (total === 0) return;
|
||||
|
||||
let startAngle = -Math.PI / 2;
|
||||
|
||||
dados.forEach((item) => {
|
||||
const sliceAngle = (2 * Math.PI * item.valor) / total;
|
||||
|
||||
// Desenhar fatia com sombra
|
||||
ctx.save();
|
||||
ctx.shadowColor = "rgba(0, 0, 0, 0.2)";
|
||||
ctx.shadowBlur = 15;
|
||||
ctx.shadowOffsetX = 5;
|
||||
ctx.shadowOffsetY = 5;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX, centerY);
|
||||
ctx.arc(centerX, centerY, radius, startAngle, startAngle + sliceAngle);
|
||||
ctx.closePath();
|
||||
|
||||
ctx.fillStyle = item.cor;
|
||||
ctx.fill();
|
||||
|
||||
ctx.restore();
|
||||
|
||||
// Desenhar borda branca
|
||||
ctx.strokeStyle = "#ffffff";
|
||||
ctx.lineWidth = 3;
|
||||
ctx.stroke();
|
||||
|
||||
startAngle += sliceAngle;
|
||||
});
|
||||
|
||||
// Desenhar círculo branco no centro (efeito donut)
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius * 0.6, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Atualizar gráficos quando dados mudarem
|
||||
$effect(() => {
|
||||
if (canvasSaldo && saldoAtual) {
|
||||
desenharGraficoPizza(canvasSaldo, [
|
||||
{ label: "Usado", valor: saldoAtual.diasUsados, cor: "#ff6b6b" },
|
||||
{ label: "Pendente", valor: saldoAtual.diasPendentes, cor: "#ffa94d" },
|
||||
{ label: "Disponível", valor: saldoAtual.diasDisponiveis, cor: "#51cf66" },
|
||||
]);
|
||||
}
|
||||
|
||||
if (canvasStatus && totalSolicitacoes > 0) {
|
||||
desenharGraficoPizza(canvasStatus, [
|
||||
{ label: "Aprovadas", valor: aprovadas, cor: "#51cf66" },
|
||||
{ label: "Pendentes", valor: pendentes, cor: "#ffa94d" },
|
||||
{ label: "Reprovadas", valor: reprovadas, cor: "#ff6b6b" },
|
||||
]);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="dashboard-ferias">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-4xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
📊 Dashboard de Férias
|
||||
</h1>
|
||||
<p class="text-base-content/70 mt-2">Visualize seus saldos e histórico de solicitações</p>
|
||||
</div>
|
||||
|
||||
{#if saldosQuery.isLoading || solicitacoesQuery.isLoading}
|
||||
<!-- Loading Skeletons -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
{#each Array(4) as _}
|
||||
<div class="skeleton h-32 rounded-2xl"></div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Cards de Estatísticas -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<!-- Card 1: Saldo Disponível -->
|
||||
<div
|
||||
class="stat bg-gradient-to-br from-success/20 to-success/5 border-2 border-success/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
|
||||
>
|
||||
<div class="stat-figure text-success">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="inline-block w-10 h-10 stroke-current"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title text-success font-semibold">Disponível</div>
|
||||
<div class="stat-value text-success text-4xl">{saldoAtual?.diasDisponiveis || 0}</div>
|
||||
<div class="stat-desc text-success/70">dias para usar</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 2: Dias Usados -->
|
||||
<div
|
||||
class="stat bg-gradient-to-br from-error/20 to-error/5 border-2 border-error/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
|
||||
>
|
||||
<div class="stat-figure text-error">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="inline-block w-10 h-10 stroke-current"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title text-error font-semibold">Usado</div>
|
||||
<div class="stat-value text-error text-4xl">{saldoAtual?.diasUsados || 0}</div>
|
||||
<div class="stat-desc text-error/70">dias já gozados</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 3: Pendentes -->
|
||||
<div
|
||||
class="stat bg-gradient-to-br from-warning/20 to-warning/5 border-2 border-warning/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
|
||||
>
|
||||
<div class="stat-figure text-warning">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="inline-block w-10 h-10 stroke-current"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title text-warning font-semibold">Pendentes</div>
|
||||
<div class="stat-value text-warning text-4xl">{saldoAtual?.diasPendentes || 0}</div>
|
||||
<div class="stat-desc text-warning/70">aguardando aprovação</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 4: Total de Direito -->
|
||||
<div
|
||||
class="stat bg-gradient-to-br from-primary/20 to-primary/5 border-2 border-primary/30 shadow-2xl rounded-2xl hover:scale-105 transition-all duration-300"
|
||||
>
|
||||
<div class="stat-figure text-primary">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="inline-block w-10 h-10 stroke-current"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title text-primary font-semibold">Total Direito</div>
|
||||
<div class="stat-value text-primary text-4xl">{saldoAtual?.diasDireito || 0}</div>
|
||||
<div class="stat-desc text-primary/70">dias no ano</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gráficos -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||
<!-- Gráfico 1: Distribuição de Saldo -->
|
||||
<div class="card bg-base-100 shadow-2xl border-2 border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl mb-4">
|
||||
🥧 Distribuição de Saldo
|
||||
<div class="badge badge-primary badge-lg">
|
||||
Ano {saldoAtual?.anoReferencia || new Date().getFullYear()}
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
{#if saldoAtual}
|
||||
<div class="flex items-center justify-center">
|
||||
<canvas
|
||||
bind:this={canvasSaldo}
|
||||
width="300"
|
||||
height="300"
|
||||
class="max-w-full"
|
||||
></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Legenda -->
|
||||
<div class="flex justify-center gap-4 mt-4 flex-wrap">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded-full bg-[#51cf66]"></div>
|
||||
<span class="text-sm font-semibold">Disponível: {saldoAtual.diasDisponiveis} dias</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded-full bg-[#ffa94d]"></div>
|
||||
<span class="text-sm font-semibold">Pendente: {saldoAtual.diasPendentes} dias</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded-full bg-[#ff6b6b]"></div>
|
||||
<span class="text-sm font-semibold">Usado: {saldoAtual.diasUsados} dias</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="alert alert-info">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-current shrink-0 w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Nenhum saldo disponível para o ano atual</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gráfico 2: Status de Solicitações -->
|
||||
<div class="card bg-base-100 shadow-2xl border-2 border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl mb-4">
|
||||
📋 Status de Solicitações
|
||||
<div class="badge badge-secondary badge-lg">Total: {totalSolicitacoes}</div>
|
||||
</h2>
|
||||
|
||||
{#if totalSolicitacoes > 0}
|
||||
<div class="flex items-center justify-center">
|
||||
<canvas
|
||||
bind:this={canvasStatus}
|
||||
width="300"
|
||||
height="300"
|
||||
class="max-w-full"
|
||||
></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Legenda -->
|
||||
<div class="flex justify-center gap-4 mt-4 flex-wrap">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded-full bg-[#51cf66]"></div>
|
||||
<span class="text-sm font-semibold">Aprovadas: {aprovadas}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded-full bg-[#ffa94d]"></div>
|
||||
<span class="text-sm font-semibold">Pendentes: {pendentes}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded-full bg-[#ff6b6b]"></div>
|
||||
<span class="text-sm font-semibold">Reprovadas: {reprovadas}</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="alert alert-info">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-current shrink-0 w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Nenhuma solicitação de férias ainda</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Histórico de Saldos -->
|
||||
{#if saldos.length > 0}
|
||||
<div class="card bg-base-100 shadow-2xl border-2 border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl mb-4">📅 Histórico de Saldos</h2>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ano</th>
|
||||
<th>Direito</th>
|
||||
<th>Usado</th>
|
||||
<th>Pendente</th>
|
||||
<th>Disponível</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each saldos as saldo}
|
||||
<tr>
|
||||
<td class="font-bold">{saldo.anoReferencia}</td>
|
||||
<td>{saldo.diasDireito} dias</td>
|
||||
<td><span class="badge badge-error">{saldo.diasUsados}</span></td>
|
||||
<td><span class="badge badge-warning">{saldo.diasPendentes}</span></td>
|
||||
<td><span class="badge badge-success">{saldo.diasDisponiveis}</span></td>
|
||||
<td>
|
||||
{#if saldo.status === "ativo"}
|
||||
<span class="badge badge-success">Ativo</span>
|
||||
{:else if saldo.status === "vencido"}
|
||||
<span class="badge badge-error">Vencido</span>
|
||||
{:else}
|
||||
<span class="badge badge-neutral">Concluído</span>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.bg-clip-text {
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
canvas {
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,688 @@
|
||||
<script lang="ts">
|
||||
import { useQuery, useConvexClient } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import CalendarioFerias from "./CalendarioFerias.svelte";
|
||||
import { toast } from "svelte-sonner";
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
|
||||
interface Props {
|
||||
funcionarioId: Id<"funcionarios">;
|
||||
onSucesso?: () => void;
|
||||
onCancelar?: () => void;
|
||||
}
|
||||
|
||||
let { funcionarioId, onSucesso, onCancelar }: Props = $props();
|
||||
|
||||
// Cliente Convex
|
||||
const client = useConvexClient();
|
||||
|
||||
// Estado do wizard
|
||||
let passoAtual = $state(1);
|
||||
const totalPassos = 3;
|
||||
|
||||
// Dados da solicitação
|
||||
let anoSelecionado = $state(new Date().getFullYear());
|
||||
let periodosFerias: Array<{ dataInicio: string; dataFim: string; dias: number }> = $state([]);
|
||||
let observacao = $state("");
|
||||
let processando = $state(false);
|
||||
|
||||
// Queries
|
||||
const saldoQuery = $derived(
|
||||
useQuery(api.saldoFerias.obterSaldo, {
|
||||
funcionarioId,
|
||||
anoReferencia: anoSelecionado,
|
||||
})
|
||||
);
|
||||
|
||||
const validacaoQuery = $derived(
|
||||
periodosFerias.length > 0
|
||||
? useQuery(api.saldoFerias.validarSolicitacao, {
|
||||
funcionarioId,
|
||||
anoReferencia: anoSelecionado,
|
||||
periodos: periodosFerias.map((p) => ({
|
||||
dataInicio: p.dataInicio,
|
||||
dataFim: p.dataFim,
|
||||
})),
|
||||
})
|
||||
: { data: null }
|
||||
);
|
||||
|
||||
// Derivados
|
||||
const saldo = $derived(saldoQuery.data);
|
||||
const validacao = $derived(validacaoQuery.data);
|
||||
const totalDiasSelecionados = $derived(
|
||||
periodosFerias.reduce((acc, p) => acc + p.dias, 0)
|
||||
);
|
||||
|
||||
// Anos disponíveis (últimos 3 anos + próximo ano)
|
||||
const anosDisponiveis = $derived.by(() => {
|
||||
const anoAtual = new Date().getFullYear();
|
||||
return [anoAtual - 1, anoAtual, anoAtual + 1];
|
||||
});
|
||||
|
||||
// Configurações do calendário (baseado no saldo/regime)
|
||||
const maxPeriodos = $derived(saldo?.regimeTrabalho?.includes("Servidor") ? 2 : 3);
|
||||
const minDiasPorPeriodo = $derived(
|
||||
saldo?.regimeTrabalho?.includes("Servidor") ? 10 : 5
|
||||
);
|
||||
|
||||
// Funções
|
||||
function proximoPasso() {
|
||||
if (passoAtual === 1 && !saldo) {
|
||||
toast.error("Selecione um ano com saldo disponível");
|
||||
return;
|
||||
}
|
||||
|
||||
if (passoAtual === 2 && periodosFerias.length === 0) {
|
||||
toast.error("Selecione pelo menos 1 período de férias");
|
||||
return;
|
||||
}
|
||||
|
||||
if (passoAtual === 2 && validacao && !validacao.valido) {
|
||||
toast.error("Corrija os erros antes de continuar");
|
||||
return;
|
||||
}
|
||||
|
||||
if (passoAtual < totalPassos) {
|
||||
passoAtual++;
|
||||
}
|
||||
}
|
||||
|
||||
function passoAnterior() {
|
||||
if (passoAtual > 1) {
|
||||
passoAtual--;
|
||||
}
|
||||
}
|
||||
|
||||
async function enviarSolicitacao() {
|
||||
if (!validacao || !validacao.valido) {
|
||||
toast.error("Valide os períodos antes de enviar");
|
||||
return;
|
||||
}
|
||||
|
||||
processando = true;
|
||||
|
||||
try {
|
||||
await client.mutation(api.ferias.criarSolicitacao, {
|
||||
funcionarioId,
|
||||
anoReferencia: anoSelecionado,
|
||||
periodos: periodosFerias.map((p) => ({
|
||||
dataInicio: p.dataInicio,
|
||||
dataFim: p.dataFim,
|
||||
diasCorridos: p.dias,
|
||||
})),
|
||||
observacao: observacao || undefined,
|
||||
});
|
||||
|
||||
toast.success("Solicitação de férias enviada com sucesso! 🎉");
|
||||
if (onSucesso) onSucesso();
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || "Erro ao enviar solicitação");
|
||||
} finally {
|
||||
processando = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePeriodoAdicionado(periodo: {
|
||||
dataInicio: string;
|
||||
dataFim: string;
|
||||
dias: number;
|
||||
}) {
|
||||
periodosFerias = [...periodosFerias, periodo];
|
||||
toast.success(`Período de ${periodo.dias} dias adicionado! ✅`);
|
||||
}
|
||||
|
||||
function handlePeriodoRemovido(index: number) {
|
||||
const removido = periodosFerias[index];
|
||||
periodosFerias = periodosFerias.filter((_, i) => i !== index);
|
||||
toast.info(`Período de ${removido.dias} dias removido`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wizard-ferias-container">
|
||||
<!-- Progress Bar -->
|
||||
<div class="mb-8">
|
||||
<div class="flex justify-between items-center">
|
||||
{#each Array(totalPassos) as _, i}
|
||||
<div class="flex items-center flex-1">
|
||||
<!-- Círculo do passo -->
|
||||
<div
|
||||
class="relative flex items-center justify-center w-12 h-12 rounded-full font-bold transition-all duration-300"
|
||||
class:bg-primary={passoAtual > i + 1}
|
||||
class:text-white={passoAtual > i + 1}
|
||||
class:border-4={passoAtual === i + 1}
|
||||
class:border-primary={passoAtual === i + 1}
|
||||
class:bg-base-200={passoAtual < i + 1}
|
||||
class:text-base-content={passoAtual < i + 1}
|
||||
style:box-shadow={passoAtual === i + 1 ? "0 0 20px rgba(102, 126, 234, 0.5)" : "none"}
|
||||
>
|
||||
{#if passoAtual > i + 1}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="3"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
{i + 1}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Linha conectora -->
|
||||
{#if i < totalPassos - 1}
|
||||
<div
|
||||
class="flex-1 h-1 mx-2 transition-all duration-300"
|
||||
class:bg-primary={passoAtual > i + 1}
|
||||
class:bg-base-300={passoAtual <= i + 1}
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Labels dos passos -->
|
||||
<div class="flex justify-between mt-4 px-1">
|
||||
<div class="text-center flex-1">
|
||||
<p class="text-sm font-semibold" class:text-primary={passoAtual === 1}>Ano & Saldo</p>
|
||||
</div>
|
||||
<div class="text-center flex-1">
|
||||
<p class="text-sm font-semibold" class:text-primary={passoAtual === 2}>Períodos</p>
|
||||
</div>
|
||||
<div class="text-center flex-1">
|
||||
<p class="text-sm font-semibold" class:text-primary={passoAtual === 3}>Confirmação</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Conteúdo dos Passos -->
|
||||
<div class="wizard-content">
|
||||
<!-- PASSO 1: Ano & Saldo -->
|
||||
{#if passoAtual === 1}
|
||||
<div class="passo-content animate-fadeIn">
|
||||
<h2 class="text-3xl font-bold mb-6 text-center bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
Escolha o Ano de Referência
|
||||
</h2>
|
||||
|
||||
<!-- Seletor de Ano -->
|
||||
<div class="grid grid-cols-3 gap-4 mb-8">
|
||||
{#each anosDisponiveis as ano}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-lg transition-all duration-300 hover:scale-105"
|
||||
class:btn-primary={anoSelecionado === ano}
|
||||
class:btn-outline={anoSelecionado !== ano}
|
||||
onclick={() => (anoSelecionado = ano)}
|
||||
>
|
||||
{ano}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Card de Saldo -->
|
||||
{#if saldoQuery.isLoading}
|
||||
<div class="skeleton h-64 w-full rounded-2xl"></div>
|
||||
{:else if saldo}
|
||||
<div
|
||||
class="card bg-gradient-to-br from-primary/10 to-secondary/10 shadow-2xl border-2 border-primary/20"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-2xl mb-4">
|
||||
📊 Saldo de Férias {anoSelecionado}
|
||||
</h3>
|
||||
|
||||
<div class="stats stats-vertical lg:stats-horizontal shadow-lg w-full">
|
||||
<div class="stat">
|
||||
<div class="stat-figure text-primary">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="inline-block w-8 h-8 stroke-current"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title">Total Direito</div>
|
||||
<div class="stat-value text-primary">{saldo.diasDireito}</div>
|
||||
<div class="stat-desc">dias no ano</div>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="stat-figure text-success">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="inline-block w-8 h-8 stroke-current"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title">Disponível</div>
|
||||
<div class="stat-value text-success">{saldo.diasDisponiveis}</div>
|
||||
<div class="stat-desc">para usar</div>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="stat-figure text-warning">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="inline-block w-8 h-8 stroke-current"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-title">Usado</div>
|
||||
<div class="stat-value text-warning">{saldo.diasUsados}</div>
|
||||
<div class="stat-desc">até agora</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informações do Regime -->
|
||||
<div class="alert alert-info mt-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-current shrink-0 w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h4 class="font-bold">{saldo.regimeTrabalho}</h4>
|
||||
<p class="text-sm">
|
||||
Período aquisitivo: {new Date(saldo.dataInicio).toLocaleDateString("pt-BR")}
|
||||
a {new Date(saldo.dataFim).toLocaleDateString("pt-BR")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if saldo.diasDisponiveis === 0}
|
||||
<div class="alert alert-warning mt-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Você não tem saldo disponível para este ano.</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="alert alert-warning">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Nenhum saldo encontrado para este ano.</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- PASSO 2: Seleção de Períodos -->
|
||||
{#if passoAtual === 2}
|
||||
<div class="passo-content animate-fadeIn">
|
||||
<h2 class="text-3xl font-bold mb-6 text-center bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
Selecione os Períodos de Férias
|
||||
</h2>
|
||||
|
||||
<!-- Resumo rápido -->
|
||||
<div class="alert bg-base-200 mb-6">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-info shrink-0 w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<div>
|
||||
<p>
|
||||
<strong>Saldo disponível:</strong>
|
||||
{saldo?.diasDisponiveis || 0} dias | <strong>Selecionados:</strong>
|
||||
{totalDiasSelecionados} dias | <strong>Restante:</strong>
|
||||
{(saldo?.diasDisponiveis || 0) - totalDiasSelecionados} dias
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calendário -->
|
||||
<CalendarioFerias
|
||||
periodosExistentes={periodosFerias}
|
||||
onPeriodoAdicionado={handlePeriodoAdicionado}
|
||||
onPeriodoRemovido={handlePeriodoRemovido}
|
||||
maxPeriodos={maxPeriodos}
|
||||
minDiasPorPeriodo={minDiasPorPeriodo}
|
||||
modoVisualizacao="month">
|
||||
</CalendarioFerias>
|
||||
|
||||
<!-- Validações -->
|
||||
{#if validacao && periodosFerias.length > 0}
|
||||
<div class="mt-6">
|
||||
{#if validacao.valido}
|
||||
<div class="alert alert-success">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>✅ Períodos válidos! Total: {validacao.totalDias} dias</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="alert alert-error">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="font-bold">Erros encontrados:</p>
|
||||
<ul class="list-disc list-inside">
|
||||
{#each validacao.erros as erro}
|
||||
<li>{erro}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if validacao.avisos.length > 0}
|
||||
<div class="alert alert-warning mt-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="font-bold">Avisos:</p>
|
||||
<ul class="list-disc list-inside">
|
||||
{#each validacao.avisos as aviso}
|
||||
<li>{aviso}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- PASSO 3: Confirmação -->
|
||||
{#if passoAtual === 3}
|
||||
<div class="passo-content animate-fadeIn">
|
||||
<h2 class="text-3xl font-bold mb-6 text-center bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
||||
Confirme sua Solicitação
|
||||
</h2>
|
||||
|
||||
<!-- Resumo Final -->
|
||||
<div class="card bg-base-100 shadow-2xl">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-xl mb-4">📝 Resumo da Solicitação</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div class="stat bg-base-200 rounded-lg">
|
||||
<div class="stat-title">Ano de Referência</div>
|
||||
<div class="stat-value text-primary">{anoSelecionado}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-base-200 rounded-lg">
|
||||
<div class="stat-title">Total de Dias</div>
|
||||
<div class="stat-value text-success">{totalDiasSelecionados}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="font-bold text-lg mb-2">Períodos Selecionados:</h4>
|
||||
<div class="space-y-3">
|
||||
{#each periodosFerias as periodo, index}
|
||||
<div class="flex items-center gap-4 p-4 bg-base-200 rounded-lg">
|
||||
<div
|
||||
class="badge badge-lg badge-primary font-bold text-white w-12 h-12 flex items-center justify-center"
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="font-semibold">
|
||||
{new Date(periodo.dataInicio).toLocaleDateString("pt-BR", {
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
até
|
||||
{new Date(periodo.dataFim).toLocaleDateString("pt-BR", {
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</p>
|
||||
<p class="text-sm text-base-content/70">{periodo.dias} dias corridos</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Campo de Observação -->
|
||||
<div class="form-control mt-6">
|
||||
<label for="observacao" class="label">
|
||||
<span class="label-text font-semibold">Observações (opcional)</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="observacao"
|
||||
class="textarea textarea-bordered h-24"
|
||||
placeholder="Adicione alguma observação ou justificativa..."
|
||||
bind:value={observacao}
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Botões de Navegação -->
|
||||
<div class="flex justify-between mt-8">
|
||||
<div>
|
||||
{#if passoAtual > 1}
|
||||
<button type="button" class="btn btn-outline btn-lg gap-2" onclick={passoAnterior}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
Voltar
|
||||
</button>
|
||||
{:else if onCancelar}
|
||||
<button type="button" class="btn btn-ghost btn-lg" onclick={onCancelar}>
|
||||
Cancelar
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{#if passoAtual < totalPassos}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-lg gap-2"
|
||||
onclick={proximoPasso}
|
||||
disabled={passoAtual === 1 && (!saldo || saldo.diasDisponiveis === 0)}
|
||||
>
|
||||
Próximo
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-success btn-lg gap-2"
|
||||
onclick={enviarSolicitacao}
|
||||
disabled={processando}
|
||||
>
|
||||
{#if processando}
|
||||
<span class="loading loading-spinner"></span>
|
||||
Enviando...
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
Enviar Solicitação
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.wizard-ferias-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.passo-content {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
/* Gradiente no texto */
|
||||
.bg-clip-text {
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.wizard-ferias-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.passo-content {
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/state";
|
||||
import MenuProtection from "$lib/components/MenuProtection.svelte";
|
||||
import { Toaster } from "svelte-sonner";
|
||||
|
||||
const { children } = $props();
|
||||
|
||||
@@ -82,3 +83,6 @@
|
||||
{@render children()}
|
||||
</main>
|
||||
{/if}
|
||||
|
||||
<!-- Toast Notifications (Sonner) -->
|
||||
<Toaster position="top-right" richColors closeButton expand={true} />
|
||||
|
||||
@@ -257,11 +257,11 @@
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
Mínimo 8 caracteres, com letras maiúsculas, minúsculas, números e caracteres especiais
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirmar Senha -->
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -204,6 +204,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Notificações e Mensagens -->
|
||||
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<div class="p-3 bg-info/20 rounded-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8 text-info"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="card-title text-xl">Notificações e Mensagens</h2>
|
||||
</div>
|
||||
<p class="text-base-content/70 mb-4">
|
||||
Envie notificações para usuários do sistema via chat ou email. Configure templates de mensagens reutilizáveis.
|
||||
</p>
|
||||
<div class="card-actions justify-end">
|
||||
<a href="/ti/notificacoes" class="btn btn-info">
|
||||
Acessar Painel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Documentação -->
|
||||
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
|
||||
<div class="card-body">
|
||||
@@ -230,7 +263,7 @@
|
||||
Manuais, guias e documentação técnica do sistema para usuários e administradores.
|
||||
</p>
|
||||
<div class="card-actions justify-end">
|
||||
<button class="btn btn-primary" disabled>
|
||||
<button type="button" class="btn btn-primary" disabled>
|
||||
Em breve
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
let abaAtiva = $state<"atividades" | "logins">("atividades");
|
||||
let limite = $state(50);
|
||||
|
||||
// Queries
|
||||
const atividades = useQuery(api.logsAtividades.listarAtividades, { limite });
|
||||
const logins = useQuery(api.logsLogin.listarTodosLogins, { limite });
|
||||
// Queries com $derived para garantir reatividade
|
||||
const atividades = $derived(useQuery(api.logsAtividades.listarAtividades, { limite }));
|
||||
const logins = $derived(useQuery(api.logsLogin.listarTodosLogins, { limite }));
|
||||
|
||||
function formatarData(timestamp: number) {
|
||||
return new Date(timestamp).toLocaleString('pt-BR', {
|
||||
|
||||
@@ -187,42 +187,45 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Servidor -->
|
||||
<div class="form-control md:col-span-1">
|
||||
<label class="label">
|
||||
<label class="label" for="smtp-servidor">
|
||||
<span class="label-text font-medium">Servidor SMTP *</span>
|
||||
</label>
|
||||
<input
|
||||
id="smtp-servidor"
|
||||
type="text"
|
||||
bind:value={servidor}
|
||||
placeholder="smtp.exemplo.com"
|
||||
class="input input-bordered"
|
||||
/>
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Ex: smtp.gmail.com, smtp.office365.com</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Porta -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<label class="label" for="smtp-porta">
|
||||
<span class="label-text font-medium">Porta *</span>
|
||||
</label>
|
||||
<input
|
||||
id="smtp-porta"
|
||||
type="number"
|
||||
bind:value={porta}
|
||||
placeholder="587"
|
||||
class="input input-bordered"
|
||||
/>
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Comum: 587 (TLS), 465 (SSL), 25</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usuário -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<label class="label" for="smtp-usuario">
|
||||
<span class="label-text font-medium">Usuário/Email *</span>
|
||||
</label>
|
||||
<input
|
||||
id="smtp-usuario"
|
||||
type="text"
|
||||
bind:value={usuario}
|
||||
placeholder="usuario@exemplo.com"
|
||||
@@ -232,16 +235,17 @@
|
||||
|
||||
<!-- Senha -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<label class="label" for="smtp-senha">
|
||||
<span class="label-text font-medium">Senha *</span>
|
||||
</label>
|
||||
<input
|
||||
id="smtp-senha"
|
||||
type="password"
|
||||
bind:value={senha}
|
||||
placeholder="••••••••"
|
||||
class="input input-bordered"
|
||||
/>
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-warning">
|
||||
{#if configAtual?.data?.ativo}
|
||||
Deixe em branco para manter a senha atual
|
||||
@@ -249,15 +253,16 @@
|
||||
Digite a senha da conta de email
|
||||
{/if}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email Remetente -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<label class="label" for="smtp-email-remetente">
|
||||
<span class="label-text font-medium">Email Remetente *</span>
|
||||
</label>
|
||||
<input
|
||||
id="smtp-email-remetente"
|
||||
type="email"
|
||||
bind:value={emailRemetente}
|
||||
placeholder="noreply@sgse.pe.gov.br"
|
||||
@@ -267,10 +272,11 @@
|
||||
|
||||
<!-- Nome Remetente -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<label class="label" for="smtp-nome-remetente">
|
||||
<span class="label-text font-medium">Nome Remetente *</span>
|
||||
</label>
|
||||
<input
|
||||
id="smtp-nome-remetente"
|
||||
type="text"
|
||||
bind:value={nomeRemetente}
|
||||
placeholder="SGSE - Sistema de Gestão"
|
||||
|
||||
@@ -35,23 +35,97 @@
|
||||
|
||||
processando = true;
|
||||
try {
|
||||
// TODO: Implementar envio de notificação
|
||||
console.log("Enviar notificação", {
|
||||
destinatarioId,
|
||||
canal,
|
||||
templateId: usarTemplate ? templateId : undefined,
|
||||
mensagem: !usarTemplate ? mensagemPersonalizada : undefined
|
||||
});
|
||||
const destinatario = usuarios?.data?.find(u => u._id === destinatarioId);
|
||||
|
||||
alert("Notificação enviada com sucesso!");
|
||||
if (!destinatario) {
|
||||
alert("Destinatário não encontrado");
|
||||
return;
|
||||
}
|
||||
|
||||
let resultadoChat = null;
|
||||
let resultadoEmail = null;
|
||||
|
||||
// ENVIAR PARA CHAT
|
||||
if (canal === "chat" || canal === "ambos") {
|
||||
const conversaResult = await client.mutation(
|
||||
api.chat.criarOuBuscarConversaIndividual,
|
||||
{ outroUsuarioId: destinatarioId as any }
|
||||
);
|
||||
|
||||
if (conversaResult.conversaId) {
|
||||
const mensagem = usarTemplate
|
||||
? templateSelecionado?.corpo || ""
|
||||
: mensagemPersonalizada;
|
||||
|
||||
resultadoChat = await client.mutation(api.chat.enviarMensagem, {
|
||||
conversaId: conversaResult.conversaId,
|
||||
conteudo: mensagem,
|
||||
tipo: "texto", // Tipo de mensagem
|
||||
permitirNotificacaoParaSiMesmo: true, // ✅ Permite notificação para si mesmo via painel admin
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ENVIAR PARA EMAIL
|
||||
if (canal === "email" || canal === "ambos") {
|
||||
if (!destinatario.email) {
|
||||
alert("Destinatário não possui email cadastrado");
|
||||
processando = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (usarTemplate && templateId) {
|
||||
// Usar template
|
||||
const template = templateSelecionado;
|
||||
if (template) {
|
||||
resultadoEmail = await client.mutation(api.email.enviarEmailComTemplate, {
|
||||
destinatario: destinatario.email,
|
||||
destinatarioId: destinatario._id as any,
|
||||
templateCodigo: template.codigo,
|
||||
variaveis: {
|
||||
nome: destinatario.nome,
|
||||
matricula: destinatario.matricula,
|
||||
},
|
||||
enviadoPorId: destinatario._id as any, // TODO: Pegar usuário logado
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Mensagem personalizada
|
||||
resultadoEmail = await client.mutation(api.email.enfileirarEmail, {
|
||||
destinatario: destinatario.email,
|
||||
destinatarioId: destinatario._id as any,
|
||||
assunto: "Notificação do Sistema",
|
||||
corpo: mensagemPersonalizada,
|
||||
enviadoPorId: destinatario._id as any, // TODO: Pegar usuário logado
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Feedback de sucesso
|
||||
let mensagem = "Notificação enviada com sucesso!";
|
||||
if (canal === "ambos") {
|
||||
if (resultadoChat && resultadoEmail) {
|
||||
mensagem = "✅ Notificação enviada para Chat e Email!";
|
||||
} else if (resultadoChat) {
|
||||
mensagem = "✅ Notificação enviada para Chat. Email falhou.";
|
||||
} else if (resultadoEmail) {
|
||||
mensagem = "✅ Notificação enviada para Email. Chat falhou.";
|
||||
}
|
||||
} else if (canal === "chat" && resultadoChat) {
|
||||
mensagem = "✅ Mensagem enviada no Chat!";
|
||||
} else if (canal === "email" && resultadoEmail) {
|
||||
mensagem = "✅ Email enfileirado para envio!";
|
||||
}
|
||||
|
||||
alert(mensagem);
|
||||
|
||||
// Limpar form
|
||||
destinatarioId = "";
|
||||
templateId = "";
|
||||
mensagemPersonalizada = "";
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error("Erro ao enviar notificação:", error);
|
||||
alert("Erro ao enviar notificação");
|
||||
alert("Erro ao enviar notificação: " + (error.message || "Erro desconhecido"));
|
||||
} finally {
|
||||
processando = false;
|
||||
}
|
||||
@@ -82,10 +156,10 @@
|
||||
|
||||
<!-- Destinatário -->
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<label class="label" for="destinatario-select">
|
||||
<span class="label-text font-medium">Destinatário *</span>
|
||||
</label>
|
||||
<select bind:value={destinatarioId} class="select select-bordered">
|
||||
<select id="destinatario-select" bind:value={destinatarioId} class="select select-bordered">
|
||||
<option value="">Selecione um usuário</option>
|
||||
{#if usuarios?.data}
|
||||
{#each usuarios.data as usuario}
|
||||
@@ -99,9 +173,9 @@
|
||||
|
||||
<!-- Canal -->
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text font-medium">Canal de Envio *</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<label class="label cursor-pointer">
|
||||
<input
|
||||
@@ -135,9 +209,9 @@
|
||||
|
||||
<!-- Tipo de Mensagem -->
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<div class="label">
|
||||
<span class="label-text font-medium">Tipo de Mensagem</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<label class="label cursor-pointer">
|
||||
<input
|
||||
@@ -163,10 +237,10 @@
|
||||
{#if usarTemplate}
|
||||
<!-- Template -->
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<label class="label" for="template-select">
|
||||
<span class="label-text font-medium">Template *</span>
|
||||
</label>
|
||||
<select bind:value={templateId} class="select select-bordered">
|
||||
<select id="template-select" bind:value={templateId} class="select select-bordered">
|
||||
<option value="">Selecione um template</option>
|
||||
{#if templates?.data}
|
||||
{#each templates.data as template}
|
||||
@@ -192,10 +266,11 @@
|
||||
{:else}
|
||||
<!-- Mensagem Personalizada -->
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<label class="label" for="mensagem-textarea">
|
||||
<span class="label-text font-medium">Mensagem *</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="mensagem-textarea"
|
||||
bind:value={mensagemPersonalizada}
|
||||
class="textarea textarea-bordered h-32"
|
||||
placeholder="Digite sua mensagem personalizada..."
|
||||
@@ -267,14 +342,15 @@
|
||||
</div>
|
||||
{#if template.tipo !== "sistema"}
|
||||
<div class="dropdown dropdown-end">
|
||||
<label tabindex="0" class="btn btn-ghost btn-xs">
|
||||
<button type="button" tabindex="0" class="btn btn-ghost btn-xs" aria-label="Opções do template">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
|
||||
</svg>
|
||||
</label>
|
||||
</button>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-32">
|
||||
<li><button>Editar</button></li>
|
||||
<li><button class="text-error">Excluir</button></li>
|
||||
<li><button type="button">Editar</button></li>
|
||||
<li><button type="button" class="text-error">Excluir</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
<div class="card bg-base-100 shadow-xl mb-8">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl mb-4">Ações Rápidas</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<a href="/ti/usuarios" class="btn btn-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
@@ -104,13 +104,6 @@
|
||||
</svg>
|
||||
Ver Logs
|
||||
</a>
|
||||
|
||||
<a href="/ti/notificacoes" class="btn btn-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
||||
</svg>
|
||||
Enviar Notificação
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -412,8 +412,10 @@
|
||||
<td>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-info btn-square tooltip"
|
||||
data-tip="Ver Detalhes"
|
||||
aria-label="Ver Detalhes"
|
||||
onclick={() => abrirDetalhes(perfil)}
|
||||
disabled={processando}
|
||||
>
|
||||
@@ -439,8 +441,10 @@
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-warning btn-square tooltip"
|
||||
data-tip="Editar"
|
||||
aria-label="Editar"
|
||||
onclick={() => abrirEditar(perfil)}
|
||||
disabled={processando}
|
||||
>
|
||||
@@ -460,8 +464,10 @@
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-success btn-square tooltip"
|
||||
data-tip="Clonar"
|
||||
aria-label="Clonar"
|
||||
onclick={() => clonarPerfil(perfil)}
|
||||
disabled={processando}
|
||||
>
|
||||
@@ -481,8 +487,10 @@
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-error btn-square tooltip"
|
||||
data-tip={perfil.numeroUsuarios > 0 ? "Não pode excluir - Perfil em uso" : "Excluir"}
|
||||
aria-label="Excluir"
|
||||
onclick={() => abrirModalExcluir(perfil)}
|
||||
disabled={processando || perfil.numeroUsuarios > 0}
|
||||
>
|
||||
@@ -921,10 +929,10 @@
|
||||
<span>Esta ação não pode ser desfeita!</span>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-ghost" onclick={fecharModalExcluir} disabled={processando}>
|
||||
<button type="button" class="btn btn-ghost" onclick={fecharModalExcluir} disabled={processando}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button class="btn btn-error" onclick={confirmarExclusao} disabled={processando}>
|
||||
<button type="button" class="btn btn-error" onclick={confirmarExclusao} disabled={processando}>
|
||||
{#if processando}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{/if}
|
||||
|
||||
@@ -14,9 +14,18 @@
|
||||
let filtroStatus = $state<"todos" | "ativo" | "bloqueado" | "inativo">("todos");
|
||||
let usuarioSelecionado = $state<any>(null);
|
||||
let modalAberto = $state(false);
|
||||
let modalAcao = $state<"bloquear" | "desbloquear" | "reset">("bloquear");
|
||||
let modalAcao = $state<"bloquear" | "desbloquear" | "reset" | "associar">("bloquear");
|
||||
let motivo = $state("");
|
||||
let processando = $state(false);
|
||||
|
||||
// Modal de associar funcionário
|
||||
let modalAssociarAberto = $state(false);
|
||||
let usuarioParaAssociar = $state<any>(null);
|
||||
let funcionarioSelecionadoId = $state<string>("");
|
||||
let buscaFuncionario = $state("");
|
||||
|
||||
// Query de funcionários
|
||||
const funcionarios = useQuery(api.funcionarios.list, {});
|
||||
|
||||
// Usuários filtrados
|
||||
const usuariosFiltrados = $derived.by(() => {
|
||||
@@ -36,6 +45,21 @@
|
||||
return matchNome && matchStatus;
|
||||
});
|
||||
});
|
||||
|
||||
// Funcionários filtrados (sem associação ou disponíveis)
|
||||
const funcionariosFiltrados = $derived.by(() => {
|
||||
if (!funcionarios?.data || !Array.isArray(funcionarios.data)) return [];
|
||||
|
||||
return funcionarios.data.filter(f => {
|
||||
// Filtro por busca
|
||||
const matchBusca = !buscaFuncionario ||
|
||||
f.nome.toLowerCase().includes(buscaFuncionario.toLowerCase()) ||
|
||||
f.cpf?.includes(buscaFuncionario) ||
|
||||
f.matricula?.includes(buscaFuncionario);
|
||||
|
||||
return matchBusca;
|
||||
}).sort((a, b) => a.nome.localeCompare(b.nome));
|
||||
});
|
||||
|
||||
const stats = $derived.by(() => {
|
||||
if (!usuarios?.data || !Array.isArray(usuarios.data)) return null;
|
||||
@@ -59,6 +83,59 @@
|
||||
usuarioSelecionado = null;
|
||||
motivo = "";
|
||||
}
|
||||
|
||||
function abrirModalAssociar(usuario: any) {
|
||||
usuarioParaAssociar = usuario;
|
||||
funcionarioSelecionadoId = usuario.funcionarioId || "";
|
||||
buscaFuncionario = "";
|
||||
modalAssociarAberto = true;
|
||||
}
|
||||
|
||||
function fecharModalAssociar() {
|
||||
modalAssociarAberto = false;
|
||||
usuarioParaAssociar = null;
|
||||
funcionarioSelecionadoId = "";
|
||||
buscaFuncionario = "";
|
||||
}
|
||||
|
||||
async function associarFuncionario() {
|
||||
if (!usuarioParaAssociar || !funcionarioSelecionadoId) return;
|
||||
|
||||
processando = true;
|
||||
try {
|
||||
await client.mutation(api.usuarios.associarFuncionario, {
|
||||
usuarioId: usuarioParaAssociar._id as Id<"usuarios">,
|
||||
funcionarioId: funcionarioSelecionadoId as Id<"funcionarios">
|
||||
});
|
||||
|
||||
alert("Funcionário associado com sucesso!");
|
||||
fecharModalAssociar();
|
||||
} catch (error: any) {
|
||||
alert("Erro ao associar funcionário: " + error.message);
|
||||
} finally {
|
||||
processando = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function desassociarFuncionario() {
|
||||
if (!usuarioParaAssociar) return;
|
||||
|
||||
if (!confirm("Deseja realmente desassociar o funcionário deste usuário?")) return;
|
||||
|
||||
processando = true;
|
||||
try {
|
||||
await client.mutation(api.usuarios.desassociarFuncionario, {
|
||||
usuarioId: usuarioParaAssociar._id as Id<"usuarios">
|
||||
});
|
||||
|
||||
alert("Funcionário desassociado com sucesso!");
|
||||
fecharModalAssociar();
|
||||
} catch (error: any) {
|
||||
alert("Erro ao desassociar funcionário: " + error.message);
|
||||
} finally {
|
||||
processando = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function executarAcao() {
|
||||
if (!usuarioSelecionado) return;
|
||||
@@ -140,10 +217,11 @@
|
||||
<div class="card-body">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<label class="label" for="buscar-usuario-input">
|
||||
<span class="label-text">Buscar por nome, matrícula ou email</span>
|
||||
</label>
|
||||
<input
|
||||
id="buscar-usuario-input"
|
||||
type="text"
|
||||
bind:value={filtroNome}
|
||||
placeholder="Digite para buscar..."
|
||||
@@ -152,10 +230,10 @@
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<label class="label" for="filtro-status-select">
|
||||
<span class="label-text">Filtrar por status</span>
|
||||
</label>
|
||||
<select bind:value={filtroStatus} class="select select-bordered">
|
||||
<select id="filtro-status-select" bind:value={filtroStatus} class="select select-bordered">
|
||||
<option value="todos">Todos</option>
|
||||
<option value="ativo">Ativos</option>
|
||||
<option value="bloqueado">Bloqueados</option>
|
||||
@@ -180,6 +258,7 @@
|
||||
<th>Matrícula</th>
|
||||
<th>Nome</th>
|
||||
<th>Email</th>
|
||||
<th>Funcionário</th>
|
||||
<th>Status</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
@@ -190,11 +269,40 @@
|
||||
<td class="font-mono">{usuario.matricula}</td>
|
||||
<td>{usuario.nome}</td>
|
||||
<td>{usuario.email || "-"}</td>
|
||||
<td>
|
||||
{#if usuario.funcionarioId}
|
||||
<div class="badge badge-success gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Associado
|
||||
</div>
|
||||
{:else}
|
||||
<div class="badge badge-warning gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
Não associado
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
<UserStatusBadge ativo={usuario.ativo} bloqueado={usuario.bloqueado} />
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<!-- Botão Associar Funcionário -->
|
||||
<button
|
||||
class="btn btn-sm btn-info"
|
||||
onclick={() => abrirModalAssociar(usuario)}
|
||||
title={usuario.funcionarioId ? "Alterar funcionário associado" : "Associar funcionário"}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
{usuario.funcionarioId ? "Alterar" : "Associar"}
|
||||
</button>
|
||||
|
||||
{#if usuario.bloqueado}
|
||||
<button
|
||||
class="btn btn-sm btn-success"
|
||||
@@ -261,10 +369,11 @@
|
||||
|
||||
{#if modalAcao === "bloquear"}
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<label class="label" for="motivo-bloqueio-textarea">
|
||||
<span class="label-text">Motivo do bloqueio *</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="motivo-bloqueio-textarea"
|
||||
bind:value={motivo}
|
||||
class="textarea textarea-bordered"
|
||||
placeholder="Digite o motivo..."
|
||||
@@ -302,7 +411,126 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="modal-backdrop" onclick={fecharModal}></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Modal Associar Funcionário -->
|
||||
{#if modalAssociarAberto && usuarioParaAssociar}
|
||||
<div class="modal modal-open">
|
||||
<div class="modal-box max-w-2xl">
|
||||
<h3 class="font-bold text-lg mb-4">
|
||||
Associar Funcionário ao Usuário
|
||||
</h3>
|
||||
|
||||
<div class="mb-6">
|
||||
<p class="text-base-content/80 mb-2">
|
||||
<strong>Usuário:</strong> {usuarioParaAssociar.nome} ({usuarioParaAssociar.matricula})
|
||||
</p>
|
||||
|
||||
{#if usuarioParaAssociar.funcionarioId}
|
||||
<div class="alert alert-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>Este usuário já possui um funcionário associado. Você pode alterá-lo ou desassociá-lo.</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Busca de Funcionários -->
|
||||
<div class="form-control mb-4">
|
||||
<label for="busca-funcionario" class="label">
|
||||
<span class="label-text">Buscar Funcionário</span>
|
||||
</label>
|
||||
<input
|
||||
id="busca-funcionario"
|
||||
type="text"
|
||||
bind:value={buscaFuncionario}
|
||||
placeholder="Digite nome, CPF ou matrícula..."
|
||||
class="input input-bordered"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Lista de Funcionários -->
|
||||
<div class="form-control mb-6">
|
||||
<div class="label">
|
||||
<span class="label-text">Selecione o Funcionário *</span>
|
||||
</div>
|
||||
|
||||
<div class="border rounded-lg max-h-96 overflow-y-auto">
|
||||
{#if funcionariosFiltrados.length === 0}
|
||||
<div class="p-4 text-center text-base-content/60">
|
||||
{buscaFuncionario ? "Nenhum funcionário encontrado com esse critério" : "Carregando funcionários..."}
|
||||
</div>
|
||||
{:else}
|
||||
{#each funcionariosFiltrados as func}
|
||||
<label class="flex items-center gap-3 p-3 hover:bg-base-200 cursor-pointer border-b last:border-b-0">
|
||||
<input
|
||||
type="radio"
|
||||
name="funcionario"
|
||||
value={func._id}
|
||||
bind:group={funcionarioSelecionadoId}
|
||||
class="radio radio-primary"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold">{func.nome}</div>
|
||||
<div class="text-sm text-base-content/70">
|
||||
CPF: {func.cpf || "N/A"}
|
||||
{#if func.matricula}
|
||||
| Matrícula: {func.matricula}
|
||||
{/if}
|
||||
</div>
|
||||
{#if func.descricaoCargo}
|
||||
<div class="text-xs text-base-content/60">{func.descricaoCargo}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</label>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ações -->
|
||||
<div class="modal-action">
|
||||
<button
|
||||
class="btn btn-ghost"
|
||||
onclick={fecharModalAssociar}
|
||||
disabled={processando}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
|
||||
{#if usuarioParaAssociar.funcionarioId}
|
||||
<button
|
||||
class="btn btn-error"
|
||||
onclick={desassociarFuncionario}
|
||||
disabled={processando}
|
||||
>
|
||||
{#if processando}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{/if}
|
||||
Desassociar
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
onclick={associarFuncionario}
|
||||
disabled={processando || !funcionarioSelecionadoId}
|
||||
>
|
||||
{#if processando}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{/if}
|
||||
{usuarioParaAssociar.funcionarioId ? "Alterar" : "Associar"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="modal-backdrop" onclick={fecharModalAssociar}></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user