feat: implement vacation management system with request approval, notification handling, and employee training tracking; enhance UI components for improved user experience
This commit is contained in:
@@ -1,524 +1,447 @@
|
||||
<script lang="ts">
|
||||
import { useQuery, useConvexClient } from "convex-svelte";
|
||||
import { useConvexClient, useQuery } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import { requestNotificationPermission } from "$lib/utils/notifications";
|
||||
import { getAvatarUrl as generateAvatarUrl } from "$lib/utils/avatarGenerator";
|
||||
|
||||
import { authStore } from "$lib/stores/auth.svelte";
|
||||
import SolicitarFerias from "$lib/components/SolicitarFerias.svelte";
|
||||
import AprovarFerias from "$lib/components/AprovarFerias.svelte";
|
||||
|
||||
const client = useConvexClient();
|
||||
const perfil = useQuery(api.usuarios.obterPerfil, {});
|
||||
|
||||
// Estados
|
||||
let nome = $state("");
|
||||
let email = $state("");
|
||||
let matricula = $state("");
|
||||
let avatarSelecionado = $state("");
|
||||
let statusMensagemInput = $state("");
|
||||
let statusPresencaSelect = $state("online");
|
||||
let notificacoesAtivadas = $state(true);
|
||||
let somNotificacao = $state(true);
|
||||
|
||||
let uploadingFoto = $state(false);
|
||||
let salvando = $state(false);
|
||||
let mensagemSucesso = $state("");
|
||||
|
||||
// Sincronizar com perfil
|
||||
$effect(() => {
|
||||
if (perfil) {
|
||||
nome = perfil.nome || "";
|
||||
email = perfil.email || "";
|
||||
matricula = perfil.matricula || "";
|
||||
avatarSelecionado = perfil.avatar || "";
|
||||
statusMensagemInput = perfil.statusMensagem || "";
|
||||
statusPresencaSelect = perfil.statusPresenca || "online";
|
||||
notificacoesAtivadas = perfil.notificacoesAtivadas ?? true;
|
||||
somNotificacao = perfil.somNotificacao ?? true;
|
||||
}
|
||||
});
|
||||
|
||||
// Lista de avatares profissionais usando DiceBear - TODOS FELIZES E SORRIDENTES
|
||||
const avatares = [
|
||||
// Avatares masculinos (16)
|
||||
{ id: "avatar-m-1", seed: "John-Happy", label: "Homem 1" },
|
||||
{ id: "avatar-m-2", seed: "Peter-Smile", label: "Homem 2" },
|
||||
{ id: "avatar-m-3", seed: "Michael-Joy", label: "Homem 3" },
|
||||
{ id: "avatar-m-4", seed: "David-Glad", label: "Homem 4" },
|
||||
{ id: "avatar-m-5", seed: "James-Cheerful", label: "Homem 5" },
|
||||
{ id: "avatar-m-6", seed: "Robert-Bright", label: "Homem 6" },
|
||||
{ id: "avatar-m-7", seed: "William-Joyful", label: "Homem 7" },
|
||||
{ id: "avatar-m-8", seed: "Joseph-Merry", label: "Homem 8" },
|
||||
{ id: "avatar-m-9", seed: "Thomas-Happy", label: "Homem 9" },
|
||||
{ id: "avatar-m-10", seed: "Charles-Smile", label: "Homem 10" },
|
||||
{ id: "avatar-m-11", seed: "Daniel-Joy", label: "Homem 11" },
|
||||
{ id: "avatar-m-12", seed: "Matthew-Glad", label: "Homem 12" },
|
||||
{ id: "avatar-m-13", seed: "Anthony-Cheerful", label: "Homem 13" },
|
||||
{ id: "avatar-m-14", seed: "Mark-Bright", label: "Homem 14" },
|
||||
{ id: "avatar-m-15", seed: "Donald-Joyful", label: "Homem 15" },
|
||||
{ id: "avatar-m-16", seed: "Steven-Merry", label: "Homem 16" },
|
||||
|
||||
// Avatares femininos (16)
|
||||
{ id: "avatar-f-1", seed: "Maria-Happy", label: "Mulher 1" },
|
||||
{ id: "avatar-f-2", seed: "Ana-Smile", label: "Mulher 2" },
|
||||
{ id: "avatar-f-3", seed: "Patricia-Joy", label: "Mulher 3" },
|
||||
{ id: "avatar-f-4", seed: "Jennifer-Glad", label: "Mulher 4" },
|
||||
{ id: "avatar-f-5", seed: "Linda-Cheerful", label: "Mulher 5" },
|
||||
{ id: "avatar-f-6", seed: "Barbara-Bright", label: "Mulher 6" },
|
||||
{ id: "avatar-f-7", seed: "Elizabeth-Joyful", label: "Mulher 7" },
|
||||
{ id: "avatar-f-8", seed: "Jessica-Merry", label: "Mulher 8" },
|
||||
{ id: "avatar-f-9", seed: "Sarah-Happy", label: "Mulher 9" },
|
||||
{ id: "avatar-f-10", seed: "Karen-Smile", label: "Mulher 10" },
|
||||
{ id: "avatar-f-11", seed: "Nancy-Joy", label: "Mulher 11" },
|
||||
{ id: "avatar-f-12", seed: "Betty-Glad", label: "Mulher 12" },
|
||||
{ id: "avatar-f-13", seed: "Helen-Cheerful", label: "Mulher 13" },
|
||||
{ id: "avatar-f-14", seed: "Sandra-Bright", label: "Mulher 14" },
|
||||
{ id: "avatar-f-15", seed: "Ashley-Joyful", label: "Mulher 15" },
|
||||
{ id: "avatar-f-16", seed: "Kimberly-Merry", label: "Mulher 16" },
|
||||
];
|
||||
let abaAtiva = $state<"meu-perfil" | "minhas-ferias" | "aprovar-ferias">("meu-perfil");
|
||||
let mostrarFormSolicitar = $state(false);
|
||||
let solicitacaoSelecionada = $state<any>(null);
|
||||
|
||||
function getAvatarUrl(avatarId: string): string {
|
||||
// Usar gerador local ao invés da API externa
|
||||
return generateAvatarUrl(avatarId);
|
||||
// Queries
|
||||
const funcionarioQuery = $derived(
|
||||
authStore.usuario?.funcionarioId
|
||||
? useQuery(api.funcionarios.getById, { id: authStore.usuario.funcionarioId as any })
|
||||
: { data: null }
|
||||
);
|
||||
|
||||
const minhasSolicitacoesQuery = $derived(
|
||||
funcionarioQuery.data
|
||||
? useQuery(api.ferias.listarMinhasSolicitacoes, { funcionarioId: funcionarioQuery.data._id })
|
||||
: { data: [] }
|
||||
);
|
||||
|
||||
const solicitacoesSubordinadosQuery = $derived(
|
||||
authStore.usuario?._id
|
||||
? useQuery(api.ferias.listarSolicitacoesSubordinados, { gestorId: authStore.usuario._id as any })
|
||||
: { data: [] }
|
||||
);
|
||||
|
||||
const meuTimeQuery = $derived(
|
||||
funcionarioQuery.data
|
||||
? useQuery(api.times.obterTimeFuncionario, { funcionarioId: funcionarioQuery.data._id })
|
||||
: { data: null }
|
||||
);
|
||||
|
||||
const meusTimesGestorQuery = $derived(
|
||||
authStore.usuario?._id
|
||||
? useQuery(api.times.listarPorGestor, { gestorId: authStore.usuario._id as any })
|
||||
: { data: [] }
|
||||
);
|
||||
|
||||
const funcionario = $derived(funcionarioQuery.data);
|
||||
const minhasSolicitacoes = $derived(minhasSolicitacoesQuery?.data || []);
|
||||
const solicitacoesSubordinados = $derived(solicitacoesSubordinadosQuery?.data || []);
|
||||
const meuTime = $derived(meuTimeQuery?.data);
|
||||
const meusTimesGestor = $derived(meusTimesGestorQuery?.data || []);
|
||||
|
||||
// Verificar se é gestor
|
||||
const ehGestor = $derived((meusTimesGestor || []).length > 0);
|
||||
|
||||
async function recarregar() {
|
||||
mostrarFormSolicitar = false;
|
||||
solicitacaoSelecionada = null;
|
||||
}
|
||||
|
||||
async function handleUploadFoto(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
// Validar tipo
|
||||
if (!file.type.startsWith("image/")) {
|
||||
alert("Por favor, selecione uma imagem");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar tamanho (max 2MB)
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
alert("A imagem deve ter no máximo 2MB");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
uploadingFoto = true;
|
||||
|
||||
// 1. Obter upload URL
|
||||
const uploadUrl = await client.mutation(api.usuarios.uploadFotoPerfil, {});
|
||||
|
||||
// 2. Upload da foto
|
||||
const result = await fetch(uploadUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": file.type },
|
||||
body: file,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error("Falha no upload");
|
||||
}
|
||||
|
||||
const { storageId } = await result.json();
|
||||
|
||||
// 3. Atualizar perfil
|
||||
await client.mutation(api.usuarios.atualizarPerfil, {
|
||||
fotoPerfil: storageId,
|
||||
avatar: "", // Limpar avatar quando usa foto
|
||||
});
|
||||
|
||||
mensagemSucesso = "Foto de perfil atualizada com sucesso!";
|
||||
setTimeout(() => (mensagemSucesso = ""), 3000);
|
||||
} catch (error) {
|
||||
console.error("Erro ao fazer upload:", error);
|
||||
alert("Erro ao fazer upload da foto");
|
||||
} finally {
|
||||
uploadingFoto = false;
|
||||
input.value = "";
|
||||
}
|
||||
|
||||
async function selecionarSolicitacao(solicitacaoId: string) {
|
||||
const detalhes = await client.query(api.ferias.obterDetalhes, {
|
||||
solicitacaoId: solicitacaoId as any,
|
||||
});
|
||||
solicitacaoSelecionada = detalhes;
|
||||
}
|
||||
|
||||
async function handleSelecionarAvatar(avatarId: string) {
|
||||
try {
|
||||
avatarSelecionado = avatarId;
|
||||
await client.mutation(api.usuarios.atualizarPerfil, {
|
||||
avatar: avatarId,
|
||||
fotoPerfil: undefined, // Limpar foto quando usa avatar
|
||||
});
|
||||
mensagemSucesso = "Avatar atualizado com sucesso!";
|
||||
setTimeout(() => (mensagemSucesso = ""), 3000);
|
||||
} catch (error) {
|
||||
console.error("Erro ao atualizar avatar:", error);
|
||||
alert("Erro ao atualizar avatar");
|
||||
}
|
||||
|
||||
function getStatusBadge(status: string) {
|
||||
const badges: Record<string, string> = {
|
||||
aguardando_aprovacao: "badge-warning",
|
||||
aprovado: "badge-success",
|
||||
reprovado: "badge-error",
|
||||
data_ajustada_aprovada: "badge-info",
|
||||
};
|
||||
return badges[status] || "badge-neutral";
|
||||
}
|
||||
|
||||
async function handleSalvarConfiguracoes() {
|
||||
try {
|
||||
salvando = true;
|
||||
|
||||
// Validar statusMensagem
|
||||
if (statusMensagemInput.length > 100) {
|
||||
alert("A mensagem de status deve ter no máximo 100 caracteres");
|
||||
return;
|
||||
}
|
||||
|
||||
await client.mutation(api.usuarios.atualizarPerfil, {
|
||||
statusMensagem: statusMensagemInput.trim() || undefined,
|
||||
statusPresenca: statusPresencaSelect as any,
|
||||
notificacoesAtivadas,
|
||||
somNotificacao,
|
||||
});
|
||||
|
||||
mensagemSucesso = "Configurações salvas com sucesso!";
|
||||
setTimeout(() => (mensagemSucesso = ""), 3000);
|
||||
} catch (error) {
|
||||
console.error("Erro ao salvar configurações:", error);
|
||||
alert("Erro ao salvar configurações");
|
||||
} finally {
|
||||
salvando = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSolicitarNotificacoes() {
|
||||
const permission = await requestNotificationPermission();
|
||||
if (permission === "granted") {
|
||||
await client.mutation(api.usuarios.atualizarPerfil, { notificacoesAtivadas: true });
|
||||
notificacoesAtivadas = true;
|
||||
} else if (permission === "denied") {
|
||||
alert(
|
||||
"Você negou as notificações. Para ativá-las, permita notificações nas configurações do navegador."
|
||||
);
|
||||
}
|
||||
|
||||
function getStatusTexto(status: string) {
|
||||
const textos: Record<string, string> = {
|
||||
aguardando_aprovacao: "Aguardando",
|
||||
aprovado: "Aprovado",
|
||||
reprovado: "Reprovado",
|
||||
data_ajustada_aprovada: "Ajustado",
|
||||
};
|
||||
return textos[status] || status;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<main class="container mx-auto px-4 py-6 max-w-7xl">
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold text-base-content">Meu Perfil</h1>
|
||||
<p class="text-base-content/70">Gerencie suas informações e preferências</p>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-primary text-primary-content rounded-full w-16">
|
||||
<span class="text-2xl">{authStore.usuario?.nome.substring(0, 2).toUpperCase()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-primary">{authStore.usuario?.nome}</h1>
|
||||
<p class="text-base-content/70">{authStore.usuario?.email}</p>
|
||||
{#if meuTime}
|
||||
<div class="badge badge-outline mt-1" style="border-color: {meuTime.cor}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
{meuTime.nome}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if mensagemSucesso}
|
||||
<div class="alert alert-success mb-6">
|
||||
<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"
|
||||
/>
|
||||
<!-- Tabs -->
|
||||
<div role="tablist" class="tabs tabs-boxed mb-6">
|
||||
<button
|
||||
role="tab"
|
||||
class={`tab ${abaAtiva === "meu-perfil" ? "tab-active" : ""}`}
|
||||
onclick={() => abaAtiva = "meu-perfil"}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" 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>
|
||||
<span>{mensagemSucesso}</span>
|
||||
</div>
|
||||
{/if}
|
||||
Meu Perfil
|
||||
</button>
|
||||
|
||||
<button
|
||||
role="tab"
|
||||
class={`tab ${abaAtiva === "minhas-ferias" ? "tab-active" : ""}`}
|
||||
onclick={() => abaAtiva = "minhas-ferias"}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Minhas Férias
|
||||
</button>
|
||||
|
||||
{#if ehGestor}
|
||||
<button
|
||||
role="tab"
|
||||
class={`tab ${abaAtiva === "aprovar-ferias" ? "tab-active" : ""}`}
|
||||
onclick={() => abaAtiva = "aprovar-ferias"}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
Aprovar Férias
|
||||
{#if (solicitacoesSubordinados || []).filter((s: any) => s.status === "aguardando_aprovacao").length > 0}
|
||||
<span class="badge badge-warning badge-sm ml-2">
|
||||
{(solicitacoesSubordinados || []).filter((s: any) => s.status === "aguardando_aprovacao").length}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if perfil}
|
||||
<div class="grid gap-6">
|
||||
<!-- Card 1: Foto de Perfil -->
|
||||
<!-- Conteúdo das Abas -->
|
||||
{#if abaAtiva === "meu-perfil"}
|
||||
<!-- Meu Perfil -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Informações Pessoais -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Foto de Perfil</h2>
|
||||
|
||||
<div class="flex flex-col md:flex-row items-center gap-6">
|
||||
<!-- Preview -->
|
||||
<div class="flex-shrink-0">
|
||||
{#if perfil.fotoPerfilUrl}
|
||||
<div class="avatar">
|
||||
<div class="w-40 h-40 rounded-lg">
|
||||
<img src={perfil.fotoPerfilUrl} alt="Foto de perfil" class="object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
{:else if perfil.avatar || avatarSelecionado}
|
||||
<div class="avatar">
|
||||
<div class="w-40 h-40 rounded-lg bg-base-200 overflow-hidden">
|
||||
<img
|
||||
src={getAvatarUrl(perfil.avatar || avatarSelecionado)}
|
||||
alt="Avatar"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-neutral text-neutral-content rounded-lg w-40 h-40">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-20 h-20"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Upload -->
|
||||
<div class="flex-1">
|
||||
<label class="btn btn-primary btn-block gap-2">
|
||||
<input
|
||||
type="file"
|
||||
class="hidden"
|
||||
accept="image/*"
|
||||
onchange={handleUploadFoto}
|
||||
disabled={uploadingFoto}
|
||||
/>
|
||||
{#if uploadingFoto}
|
||||
<span class="loading loading-spinner"></span>
|
||||
Fazendo upload...
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5"
|
||||
/>
|
||||
</svg>
|
||||
Carregar Foto
|
||||
{/if}
|
||||
</label>
|
||||
<p class="text-xs text-base-content/60 mt-2">
|
||||
Máximo 2MB. Formatos: JPG, PNG, GIF, WEBP
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid de Avatares -->
|
||||
<div class="divider">OU escolha um avatar profissional</div>
|
||||
<div class="alert alert-info mb-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.182 15.182a4.5 4.5 0 0 1-6.364 0M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Z"
|
||||
/>
|
||||
</svg>
|
||||
<h2 class="card-title mb-4">Informações Pessoais</h2>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p class="font-semibold">32 avatares disponíveis - Todos felizes e sorridentes! 😊</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 md:grid-cols-8 lg:grid-cols-8 gap-3 max-h-96 overflow-y-auto p-2">
|
||||
{#each avatares as avatar}
|
||||
<button
|
||||
type="button"
|
||||
class={`relative w-full aspect-[3/4] rounded-lg overflow-hidden border-4 transition-all hover:scale-105 ${
|
||||
avatarSelecionado === avatar.id
|
||||
? "border-primary shadow-lg"
|
||||
: "border-base-300 hover:border-primary/50"
|
||||
}`}
|
||||
onclick={() => handleSelecionarAvatar(avatar.id)}
|
||||
title={avatar.label}
|
||||
>
|
||||
<img
|
||||
src={getAvatarUrl(avatar.id)}
|
||||
alt={avatar.label}
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
{#if avatarSelecionado === avatar.id}
|
||||
<div class="absolute inset-0 bg-primary/20 flex items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-10 h-10 text-primary"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 2: Informações Básicas -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Informações Básicas</h2>
|
||||
<p class="text-sm text-base-content/70 mb-4">
|
||||
Informações do seu cadastro (somente leitura)
|
||||
</p>
|
||||
|
||||
<div class="grid md:grid-cols-3 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label">
|
||||
<span class="label-text font-semibold">Nome</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered bg-base-200"
|
||||
value={nome}
|
||||
readonly
|
||||
/>
|
||||
</span>
|
||||
<p class="text-base-content/90">{authStore.usuario?.nome}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">E-mail</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
class="input input-bordered bg-base-200"
|
||||
value={email}
|
||||
readonly
|
||||
/>
|
||||
<div>
|
||||
<span class="label">
|
||||
<span class="label-text font-semibold">Email</span>
|
||||
</span>
|
||||
<p class="text-base-content/90">{authStore.usuario?.email}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Matrícula</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered bg-base-200"
|
||||
value={matricula}
|
||||
readonly
|
||||
/>
|
||||
<div>
|
||||
<span class="label">
|
||||
<span class="label-text font-semibold">Perfil</span>
|
||||
</span>
|
||||
<div class="badge badge-primary">{authStore.usuario?.role?.nome || "Usuário"}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Mensagem de Status do Chat</span>
|
||||
<span class="label-text-alt">{statusMensagemInput.length}/100</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="textarea textarea-bordered h-20"
|
||||
placeholder="Ex: Disponível para reuniões | Em atendimento | Ausente temporariamente"
|
||||
bind:value={statusMensagemInput}
|
||||
maxlength="100"
|
||||
></textarea>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Este texto aparecerá abaixo do seu nome no chat</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 3: Preferências de Chat -->
|
||||
<!-- Informações de Funcionário -->
|
||||
{#if funcionario}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-4">Dados Funcionais</h2>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<span class="label">
|
||||
<span class="label-text font-semibold">Matrícula</span>
|
||||
</span>
|
||||
<p class="text-base-content/90">{funcionario.matricula || "Não informada"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">
|
||||
<span class="label-text font-semibold">CPF</span>
|
||||
</span>
|
||||
<p class="text-base-content/90">{funcionario.cpf}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">
|
||||
<span class="label-text font-semibold">Time</span>
|
||||
</span>
|
||||
{#if meuTime}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="badge badge-lg badge-outline" style="border-color: {meuTime.cor}">
|
||||
{meuTime.nome}
|
||||
</div>
|
||||
<span class="text-xs text-base-content/50">Gestor: {meuTime.gestor?.nome}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-base-content/50">Não atribuído a um time</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">
|
||||
<span class="label-text font-semibold">Status</span>
|
||||
</span>
|
||||
{#if funcionario.statusFerias === "em_ferias"}
|
||||
<div class="badge badge-warning badge-lg">🏖️ Em Férias</div>
|
||||
{:else}
|
||||
<div class="badge badge-success badge-lg">✅ Ativo</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Times Gerenciados (se for gestor) -->
|
||||
{#if ehGestor}
|
||||
<div class="card bg-gradient-to-br from-primary/10 to-primary/5 shadow-xl md:col-span-2">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-4">
|
||||
<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="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
Times que Você Gerencia
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{#each meusTimesGestor as time}
|
||||
<div class="card bg-base-100 shadow border-l-4" style="border-color: {time.cor}">
|
||||
<div class="card-body p-4">
|
||||
<h3 class="font-bold text-lg">{time.nome}</h3>
|
||||
<p class="text-sm text-base-content/70">{time.descricao || "Sem descrição"}</p>
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<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 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<span class="text-sm font-semibold">{time.membros?.length || 0} membros</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{:else if abaAtiva === "minhas-ferias"}
|
||||
<!-- Minhas Férias -->
|
||||
<div class="space-y-6">
|
||||
<!-- Botão Nova Solicitação -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Preferências de Chat</h2>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Status de Presença</span>
|
||||
</label>
|
||||
<select class="select select-bordered" bind:value={statusPresencaSelect}>
|
||||
<option value="online">🟢 Online</option>
|
||||
<option value="ausente">🟡 Ausente</option>
|
||||
<option value="externo">🔵 Externo</option>
|
||||
<option value="em_reuniao">🔴 Em Reunião</option>
|
||||
<option value="offline">⚫ Offline</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
bind:checked={notificacoesAtivadas}
|
||||
/>
|
||||
<div>
|
||||
<span class="label-text font-medium">Notificações Ativadas</span>
|
||||
<p class="text-xs text-base-content/60">
|
||||
Receber notificações de novas mensagens
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{#if notificacoesAtivadas && typeof Notification !== "undefined" && Notification.permission !== "granted"}
|
||||
<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>Você precisa permitir notificações no navegador</span>
|
||||
<button type="button" class="btn btn-sm" onclick={handleSolicitarNotificacoes}>
|
||||
Permitir
|
||||
</button>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="card-title text-lg">Minhas Solicitações de Férias</h2>
|
||||
<p class="text-sm text-base-content/70">Solicite e acompanhe suas férias</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
bind:checked={somNotificacao}
|
||||
/>
|
||||
<div>
|
||||
<span class="label-text font-medium">Som de Notificação</span>
|
||||
<p class="text-xs text-base-content/60">
|
||||
Tocar um som ao receber mensagens
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
onclick={handleSalvarConfiguracoes}
|
||||
disabled={salvando}
|
||||
class="btn btn-primary gap-2"
|
||||
onclick={() => mostrarFormSolicitar = !mostrarFormSolicitar}
|
||||
>
|
||||
{#if salvando}
|
||||
<span class="loading loading-spinner"></span>
|
||||
Salvando...
|
||||
{:else}
|
||||
Salvar Configurações
|
||||
{/if}
|
||||
<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" />
|
||||
</svg>
|
||||
{mostrarFormSolicitar ? "Cancelar" : "Nova Solicitação"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if mostrarFormSolicitar}
|
||||
<div class="divider"></div>
|
||||
{#if funcionario}
|
||||
<SolicitarFerias funcionarioId={funcionario._id} onSucesso={recarregar} />
|
||||
{: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>
|
||||
<div>
|
||||
<h3 class="font-bold">Perfil de funcionário não encontrado</h3>
|
||||
<div class="text-xs">Seu usuário ainda não está associado a um cadastro de funcionário. Entre em contato com o RH.</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lista de Solicitações -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h3 class="font-bold text-lg mb-4">Histórico ({minhasSolicitacoes.length})</h3>
|
||||
|
||||
{#if minhasSolicitacoes.length === 0}
|
||||
<div class="alert">
|
||||
<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>
|
||||
<span>Você ainda não tem solicitações de férias.</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each minhasSolicitacoes as solicitacao}
|
||||
<div class="card bg-base-200 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<h4 class="font-bold">Férias {solicitacao.anoReferencia}</h4>
|
||||
<div class={`badge ${getStatusBadge(solicitacao.status)}`}>
|
||||
{getStatusTexto(solicitacao.status)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm space-y-1">
|
||||
<p><strong>Períodos:</strong> {solicitacao.periodos.length}</p>
|
||||
<p><strong>Total:</strong> {solicitacao.periodos.reduce((acc: number, p: any) => acc + p.diasCorridos, 0)} dias</p>
|
||||
{#if solicitacao.motivoReprovacao}
|
||||
<p class="text-error"><strong>Motivo:</strong> {solicitacao.motivoReprovacao}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right text-xs text-base-content/50">
|
||||
Solicitado em<br>
|
||||
{new Date(solicitacao._creationTime).toLocaleDateString("pt-BR")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Loading -->
|
||||
<div class="flex items-center justify-center h-96">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
|
||||
{:else if abaAtiva === "aprovar-ferias"}
|
||||
<!-- Aprovar Férias (Gestores) -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-4">
|
||||
Solicitações da Equipe ({solicitacoesSubordinados.length})
|
||||
</h2>
|
||||
|
||||
{#if solicitacoesSubordinados.length === 0}
|
||||
<div class="alert">
|
||||
<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>
|
||||
<span>Nenhuma solicitação pendente no momento.</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Funcionário</th>
|
||||
<th>Time</th>
|
||||
<th>Ano</th>
|
||||
<th>Períodos</th>
|
||||
<th>Dias</th>
|
||||
<th>Status</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each solicitacoesSubordinados as solicitacao}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="font-bold">{solicitacao.funcionario?.nome}</div>
|
||||
</td>
|
||||
<td>
|
||||
{#if solicitacao.time}
|
||||
<div class="badge badge-sm" style="border-color: {solicitacao.time.cor}">
|
||||
{solicitacao.time.nome}
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
<td>{solicitacao.anoReferencia}</td>
|
||||
<td>{solicitacao.periodos.length}</td>
|
||||
<td>{solicitacao.periodos.reduce((acc: number, p: any) => acc + p.diasCorridos, 0)}</td>
|
||||
<td>
|
||||
<div class={`badge badge-sm ${getStatusBadge(solicitacao.status)}`}>
|
||||
{getStatusTexto(solicitacao.status)}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{#if solicitacao.status === "aguardando_aprovacao"}
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
onclick={() => selecionarSolicitacao(solicitacao._id)}
|
||||
>
|
||||
Analisar
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="btn btn-sm btn-ghost"
|
||||
onclick={() => selecionarSolicitacao(solicitacao._id)}
|
||||
>
|
||||
Detalhes
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Modal de Aprovação -->
|
||||
{#if solicitacaoSelecionada}
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box max-w-4xl">
|
||||
{#if authStore.usuario}
|
||||
<AprovarFerias
|
||||
solicitacao={solicitacaoSelecionada}
|
||||
gestorId={authStore.usuario._id}
|
||||
onSucesso={recarregar}
|
||||
onCancelar={() => solicitacaoSelecionada = null}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button type="button" onclick={() => solicitacaoSelecionada = null} aria-label="Fechar modal">Fechar</button>
|
||||
</form>
|
||||
</dialog>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user