425 lines
13 KiB
Svelte
425 lines
13 KiB
Svelte
<script lang="ts">
|
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
|
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
|
import { useConvexClient } from 'convex-svelte';
|
|
import ErrorModal from './ErrorModal.svelte';
|
|
import UserAvatar from './chat/UserAvatar.svelte';
|
|
import { Calendar, FileText, XCircle, X, Check, Clock, User, Info } from 'lucide-svelte';
|
|
import { parseLocalDate } from '$lib/utils/datas';
|
|
|
|
type SolicitacaoAusencia = Doc<'solicitacoesAusencias'> & {
|
|
funcionario?: Doc<'funcionarios'> | null;
|
|
gestor?: Doc<'usuarios'> | null;
|
|
time?: Doc<'times'> | null;
|
|
};
|
|
|
|
interface Props {
|
|
solicitacao: SolicitacaoAusencia;
|
|
gestorId: Id<'usuarios'>;
|
|
onSucesso?: () => void;
|
|
onCancelar?: () => void;
|
|
}
|
|
|
|
const { solicitacao, gestorId, onSucesso, onCancelar }: Props = $props();
|
|
|
|
const client = useConvexClient();
|
|
|
|
let motivoReprovacao = $state('');
|
|
let processando = $state(false);
|
|
let erro = $state('');
|
|
let mostrarModalErro = $state(false);
|
|
let mensagemErroModal = $state('');
|
|
|
|
function calcularDias(dataInicio: string, dataFim: string): number {
|
|
const inicio = parseLocalDate(dataInicio);
|
|
const fim = parseLocalDate(dataFim);
|
|
const diff = fim.getTime() - inicio.getTime();
|
|
return Math.ceil(diff / (1000 * 60 * 60 * 24)) + 1;
|
|
}
|
|
|
|
let totalDias = $derived(calcularDias(solicitacao.dataInicio, solicitacao.dataFim));
|
|
|
|
async function aprovar() {
|
|
try {
|
|
processando = true;
|
|
erro = '';
|
|
mostrarModalErro = false;
|
|
|
|
await client.mutation(api.ausencias.aprovar, {
|
|
solicitacaoId: solicitacao._id,
|
|
gestorId: gestorId
|
|
});
|
|
|
|
if (onSucesso) onSucesso();
|
|
} catch (e) {
|
|
const mensagemErro = e instanceof Error ? e.message : String(e);
|
|
|
|
// Verificar se é erro de permissão
|
|
if (
|
|
mensagemErro.includes('permissão') ||
|
|
mensagemErro.includes('permission') ||
|
|
mensagemErro.includes('Você não tem permissão')
|
|
) {
|
|
mensagemErroModal =
|
|
'Você não tem permissão para aprovar esta solicitação de ausência. Apenas o gestor responsável pelo time do funcionário pode realizar esta ação.';
|
|
mostrarModalErro = true;
|
|
} else {
|
|
erro = mensagemErro;
|
|
}
|
|
} finally {
|
|
processando = false;
|
|
}
|
|
}
|
|
|
|
async function reprovar() {
|
|
if (!motivoReprovacao.trim()) {
|
|
erro = 'Informe o motivo da reprovação';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
processando = true;
|
|
erro = '';
|
|
mostrarModalErro = false;
|
|
|
|
await client.mutation(api.ausencias.reprovar, {
|
|
solicitacaoId: solicitacao._id,
|
|
gestorId: gestorId,
|
|
motivoReprovacao: motivoReprovacao.trim()
|
|
});
|
|
|
|
if (onSucesso) onSucesso();
|
|
} catch (e) {
|
|
const mensagemErro = e instanceof Error ? e.message : String(e);
|
|
|
|
// Verificar se é erro de permissão
|
|
if (
|
|
mensagemErro.includes('permissão') ||
|
|
mensagemErro.includes('permission') ||
|
|
mensagemErro.includes('Você não tem permissão')
|
|
) {
|
|
mensagemErroModal =
|
|
'Você não tem permissão para reprovar esta solicitação de ausência. Apenas o gestor responsável pelo time do funcionário pode realizar esta ação.';
|
|
mostrarModalErro = true;
|
|
} else {
|
|
erro = mensagemErro;
|
|
}
|
|
} finally {
|
|
processando = false;
|
|
}
|
|
}
|
|
|
|
function fecharModalErro() {
|
|
mostrarModalErro = false;
|
|
mensagemErroModal = '';
|
|
}
|
|
|
|
function getStatusBadge(status: string) {
|
|
const badges: Record<string, string> = {
|
|
aguardando_aprovacao: 'badge-warning',
|
|
aprovado: 'badge-success',
|
|
reprovado: 'badge-error'
|
|
};
|
|
return badges[status] || 'badge-neutral';
|
|
}
|
|
|
|
function getStatusTexto(status: string) {
|
|
const textos: Record<string, string> = {
|
|
aguardando_aprovacao: 'Aguardando Aprovação',
|
|
aprovado: 'Aprovado',
|
|
reprovado: 'Reprovado'
|
|
};
|
|
return textos[status] || status;
|
|
}
|
|
</script>
|
|
|
|
<div class="aprovar-ausencia">
|
|
<!-- Header -->
|
|
<div class="mb-4">
|
|
<h2 class="text-primary mb-1 text-2xl font-bold">Aprovar/Reprovar Ausência</h2>
|
|
<p class="text-base-content/70 text-sm">Analise a solicitação e tome uma decisão</p>
|
|
</div>
|
|
|
|
<!-- Card Principal -->
|
|
<div class="card bg-base-100 border-primary border-t-4 shadow-2xl">
|
|
<div class="card-body p-4 md:p-6">
|
|
<!-- Informações do Funcionário -->
|
|
<div class="mb-4">
|
|
<h3 class="text-primary mb-3 flex items-center gap-2 text-lg font-bold">
|
|
<div class="bg-primary/10 rounded-lg p-1.5">
|
|
<User class="text-primary h-5 w-5" strokeWidth={2} />
|
|
</div>
|
|
Funcionário
|
|
</h3>
|
|
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
<div class="bg-base-200/50 hover:bg-base-200 rounded-lg p-3 transition-all">
|
|
<p class="text-base-content/60 mb-1.5 text-xs font-semibold tracking-wide uppercase">
|
|
Nome
|
|
</p>
|
|
<div class="flex items-center gap-2">
|
|
<UserAvatar
|
|
fotoPerfilUrl={solicitacao.funcionario?.fotoPerfilUrl}
|
|
nome={solicitacao.funcionario?.nome || 'N/A'}
|
|
size="sm"
|
|
/>
|
|
<p class="text-base-content text-base font-bold truncate">
|
|
{solicitacao.funcionario?.nome || 'N/A'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{#if solicitacao.time}
|
|
<div class="bg-base-200/50 hover:bg-base-200 rounded-lg p-3 transition-all">
|
|
<p class="text-base-content/60 mb-1.5 text-xs font-semibold tracking-wide uppercase">
|
|
Time
|
|
</p>
|
|
<div
|
|
class="badge badge-sm font-semibold max-w-full overflow-hidden text-ellipsis whitespace-nowrap"
|
|
style="background-color: {solicitacao.time.cor}20; border-color: {solicitacao.time
|
|
.cor}; color: {solicitacao.time.cor}"
|
|
title={solicitacao.time.nome}
|
|
>
|
|
{solicitacao.time.nome}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="divider my-4"></div>
|
|
|
|
<!-- Período da Ausência -->
|
|
<div class="mb-4">
|
|
<h3 class="text-primary mb-3 flex items-center gap-2 text-lg font-bold">
|
|
<div class="bg-primary/10 rounded-lg p-1.5">
|
|
<Calendar class="text-primary h-5 w-5" strokeWidth={2} />
|
|
</div>
|
|
Período da Ausência
|
|
</h3>
|
|
<div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
|
<div
|
|
class="stat border-primary/20 from-primary/5 to-primary/10 hover:border-primary/30 rounded-lg border-2 bg-gradient-to-br shadow-md transition-all hover:shadow-lg p-3"
|
|
>
|
|
<div class="stat-title text-base-content/70 text-xs">Data Início</div>
|
|
<div class="stat-value text-primary text-lg font-bold">
|
|
{parseLocalDate(solicitacao.dataInicio).toLocaleDateString('pt-BR')}
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="stat border-primary/20 from-primary/5 to-primary/10 hover:border-primary/30 rounded-lg border-2 bg-gradient-to-br shadow-md transition-all hover:shadow-lg p-3"
|
|
>
|
|
<div class="stat-title text-base-content/70 text-xs">Data Fim</div>
|
|
<div class="stat-value text-primary text-lg font-bold">
|
|
{parseLocalDate(solicitacao.dataFim).toLocaleDateString('pt-BR')}
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="stat border-primary/30 from-primary/10 to-primary/15 hover:border-primary/40 rounded-lg border-2 bg-gradient-to-br shadow-md transition-all hover:shadow-lg p-3"
|
|
>
|
|
<div class="stat-title text-base-content/70 text-xs">Total de Dias</div>
|
|
<div class="stat-value text-primary text-2xl font-bold">
|
|
{totalDias}
|
|
</div>
|
|
<div class="stat-desc text-base-content/60 text-xs">dias corridos</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="divider my-4"></div>
|
|
|
|
<!-- Motivo -->
|
|
<div class="mb-4">
|
|
<h3 class="text-primary mb-3 flex items-center gap-2 text-lg font-bold">
|
|
<div class="bg-primary/10 rounded-lg p-1.5">
|
|
<FileText class="text-primary h-5 w-5" strokeWidth={2} />
|
|
</div>
|
|
Motivo da Ausência
|
|
</h3>
|
|
<div class="card border-primary/10 bg-base-200/50 rounded-lg border-2 shadow-sm">
|
|
<div class="card-body p-3">
|
|
<p class="text-base-content text-sm leading-relaxed whitespace-pre-wrap">
|
|
{solicitacao.motivo}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status Atual -->
|
|
<div class="bg-base-200/30 mb-4 rounded-lg p-3">
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-base-content/70 text-xs font-semibold tracking-wide uppercase"
|
|
>Status:</span
|
|
>
|
|
<div class={`badge badge-sm ${getStatusBadge(solicitacao.status)}`}>
|
|
{getStatusTexto(solicitacao.status)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Informações de Aprovação/Reprovação -->
|
|
{#if solicitacao.status === 'aprovado'}
|
|
<div class="alert alert-success mb-4 shadow-lg py-3">
|
|
<Check class="h-5 w-5 shrink-0 stroke-current" strokeWidth={2} />
|
|
<div class="flex-1">
|
|
<div class="font-bold text-sm">Aprovado</div>
|
|
{#if solicitacao.gestor}
|
|
<div class="text-xs mt-1">
|
|
Por: <strong>{solicitacao.gestor.nome}</strong>
|
|
</div>
|
|
{/if}
|
|
{#if solicitacao.dataAprovacao}
|
|
<div class="text-xs mt-1 opacity-80">
|
|
Em: {new Date(solicitacao.dataAprovacao).toLocaleString('pt-BR')}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if solicitacao.status === 'reprovado'}
|
|
<div class="alert alert-error mb-4 shadow-lg py-3">
|
|
<XCircle class="h-5 w-5 shrink-0 stroke-current" strokeWidth={2} />
|
|
<div class="flex-1">
|
|
<div class="font-bold text-sm">Reprovado</div>
|
|
{#if solicitacao.gestor}
|
|
<div class="text-xs mt-1">
|
|
Por: <strong>{solicitacao.gestor.nome}</strong>
|
|
</div>
|
|
{/if}
|
|
{#if solicitacao.dataReprovacao}
|
|
<div class="text-xs mt-1 opacity-80">
|
|
Em: {new Date(solicitacao.dataReprovacao).toLocaleString('pt-BR')}
|
|
</div>
|
|
{/if}
|
|
{#if solicitacao.motivoReprovacao}
|
|
<div class="mt-2">
|
|
<div class="text-xs font-semibold">Motivo:</div>
|
|
<div class="text-xs">{solicitacao.motivoReprovacao}</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Histórico de Alterações -->
|
|
{#if solicitacao.historicoAlteracoes && solicitacao.historicoAlteracoes.length > 0}
|
|
<div class="mb-4">
|
|
<h3 class="text-primary mb-3 flex items-center gap-2 text-lg font-bold">
|
|
<div class="bg-primary/10 rounded-lg p-1.5">
|
|
<Clock class="text-primary h-5 w-5" strokeWidth={2} />
|
|
</div>
|
|
Histórico de Alterações
|
|
</h3>
|
|
<div class="card border-primary/10 bg-base-200/50 rounded-lg border-2 shadow-sm">
|
|
<div class="card-body p-3">
|
|
<div class="space-y-2">
|
|
{#each solicitacao.historicoAlteracoes as hist}
|
|
<div class="border-base-300 flex items-start gap-2 border-b pb-2 last:border-0 last:pb-0">
|
|
<Clock class="text-primary mt-0.5 h-3.5 w-3.5 shrink-0" strokeWidth={2} />
|
|
<div class="flex-1">
|
|
<div class="text-base-content text-xs font-semibold">{hist.acao}</div>
|
|
<div class="text-base-content/60 text-xs">
|
|
{new Date(hist.data).toLocaleString('pt-BR')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Erro -->
|
|
{#if erro}
|
|
<div class="alert alert-error mb-4 shadow-lg py-3">
|
|
<XCircle class="h-5 w-5 shrink-0 stroke-current" />
|
|
<span class="text-sm">{erro}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Ações -->
|
|
{#if solicitacao.status === 'aguardando_aprovacao'}
|
|
<div class="card-actions mt-4 justify-end gap-2 flex-wrap">
|
|
<button
|
|
type="button"
|
|
class="btn btn-error btn-sm md:btn-md gap-2"
|
|
onclick={reprovar}
|
|
disabled={processando}
|
|
>
|
|
{#if processando}
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
{:else}
|
|
<X class="h-4 w-4" strokeWidth={2} />
|
|
{/if}
|
|
Reprovar
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-success btn-sm md:btn-md gap-2"
|
|
onclick={aprovar}
|
|
disabled={processando}
|
|
>
|
|
{#if processando}
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
{:else}
|
|
<Check class="h-4 w-4" strokeWidth={2} />
|
|
{/if}
|
|
Aprovar
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Modal de Reprovação -->
|
|
{#if motivoReprovacao !== undefined}
|
|
<div class="border-error/20 bg-error/5 mt-4 rounded-lg border-2 p-3">
|
|
<div class="form-control">
|
|
<label class="label py-1" for="motivo-reprovacao">
|
|
<span class="label-text text-error text-sm font-bold">Motivo da Reprovação</span>
|
|
</label>
|
|
<textarea
|
|
id="motivo-reprovacao"
|
|
class="textarea textarea-bordered textarea-sm focus:border-error focus:outline-error h-20"
|
|
placeholder="Informe o motivo da reprovação..."
|
|
bind:value={motivoReprovacao}
|
|
></textarea>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{:else}
|
|
<div class="alert alert-info shadow-lg py-3">
|
|
<Info class="h-5 w-5 shrink-0 stroke-current" strokeWidth={2} />
|
|
<span class="text-sm">Esta solicitação já foi processada.</span>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Botão Cancelar -->
|
|
<div class="mt-4 text-center">
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost btn-sm"
|
|
onclick={() => {
|
|
if (onCancelar) onCancelar();
|
|
}}
|
|
disabled={processando}
|
|
>
|
|
Fechar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal de Erro -->
|
|
<ErrorModal
|
|
open={mostrarModalErro}
|
|
title="Erro de Permissão"
|
|
message={mensagemErroModal || 'Você não tem permissão para realizar esta ação.'}
|
|
onClose={fecharModalErro}
|
|
/>
|
|
|
|
<style>
|
|
.aprovar-ausencia {
|
|
max-width: 100%;
|
|
margin: 0 auto;
|
|
}
|
|
</style>
|