- Added the ProtectedRoute component to various dashboard pages to enforce authentication and role-based access control. - Updated allowedRoles and maxLevel parameters for specific routes to align with the new permission management structure. - Enhanced user experience by ensuring consistent access checks across the application.
813 lines
24 KiB
Svelte
813 lines
24 KiB
Svelte
<script lang="ts">
|
|
import { useConvexClient, useQuery } from 'convex-svelte';
|
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
|
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
|
|
import { goto } from '$app/navigation';
|
|
import { resolve } from '$app/paths';
|
|
import type { Id, Doc } from '@sgse-app/backend/convex/_generated/dataModel';
|
|
|
|
// Tipos baseados nos retornos das queries do backend
|
|
type Usuario = {
|
|
_id: Id<'usuarios'>;
|
|
matricula: string;
|
|
nome: string;
|
|
email: string;
|
|
ativo: boolean;
|
|
bloqueado?: boolean;
|
|
motivoBloqueio?: string;
|
|
primeiroAcesso: boolean;
|
|
ultimoAcesso?: number;
|
|
criadoEm: number;
|
|
role: {
|
|
_id: Id<'roles'>;
|
|
_creationTime?: number;
|
|
criadoPor?: Id<'usuarios'>;
|
|
customizado?: boolean;
|
|
descricao: string;
|
|
editavel?: boolean;
|
|
nome: string;
|
|
nivel: number;
|
|
setor?: string;
|
|
erro?: boolean;
|
|
};
|
|
funcionario?: {
|
|
_id: Id<'funcionarios'>;
|
|
nome: string;
|
|
matricula?: string;
|
|
descricaoCargo?: string;
|
|
simboloTipo: 'cargo_comissionado' | 'funcao_gratificada';
|
|
};
|
|
avisos?: Array<{
|
|
tipo: 'erro' | 'aviso' | 'info';
|
|
mensagem: string;
|
|
}>;
|
|
};
|
|
|
|
type Funcionario = {
|
|
_id: Id<'funcionarios'>;
|
|
nome: string;
|
|
matricula?: string;
|
|
cpf?: string;
|
|
rg?: string;
|
|
nascimento?: string;
|
|
email?: string;
|
|
telefone?: string;
|
|
endereco?: string;
|
|
cep?: string;
|
|
cidade?: string;
|
|
uf?: string;
|
|
simboloId: Id<'simbolos'>;
|
|
simboloTipo: 'cargo_comissionado' | 'funcao_gratificada';
|
|
admissaoData?: string;
|
|
desligamentoData?: string;
|
|
descricaoCargo?: string;
|
|
};
|
|
|
|
type Gestor = Doc<'usuarios'> | null;
|
|
|
|
type TimeComDetalhes = Doc<'times'> & {
|
|
gestor: Gestor;
|
|
totalMembros: number;
|
|
};
|
|
|
|
type MembroTime = Doc<'timesMembros'> & {
|
|
funcionario: Doc<'funcionarios'> | null;
|
|
};
|
|
|
|
type TimeComMembros = Doc<'times'> & {
|
|
gestor: Gestor;
|
|
membros: MembroTime[];
|
|
};
|
|
|
|
const client = useConvexClient();
|
|
|
|
// Queries
|
|
const timesQuery = useQuery(api.times.listar, {});
|
|
const usuariosQuery = useQuery(api.usuarios.listar, {});
|
|
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
|
|
|
|
const times = $derived((timesQuery?.data || []) as TimeComDetalhes[]);
|
|
const usuarios = $derived((usuariosQuery?.data || []) as Usuario[]);
|
|
const funcionarios = $derived((funcionariosQuery?.data || []) as Funcionario[]);
|
|
|
|
const carregando = $derived(
|
|
timesQuery === undefined || usuariosQuery === undefined || funcionariosQuery === undefined
|
|
);
|
|
|
|
// Estados
|
|
let modoEdicao = $state(false);
|
|
let timeEmEdicao = $state<TimeComDetalhes | null>(null);
|
|
let mostrarModalMembros = $state(false);
|
|
let timeParaMembros = $state<TimeComMembros | null>(null);
|
|
let mostrarConfirmacaoExclusao = $state(false);
|
|
let timeParaExcluir = $state<TimeComDetalhes | null>(null);
|
|
let processando = $state(false);
|
|
|
|
// Form
|
|
let formNome = $state('');
|
|
let formDescricao = $state('');
|
|
let formGestorId = $state('');
|
|
let formCor = $state('#3B82F6');
|
|
|
|
// Membros
|
|
let membrosDisponiveis = $derived(
|
|
funcionarios.filter((f: Funcionario) => {
|
|
// Verificar se o funcionário já está em algum time ativo
|
|
const jaNaEquipe = timeParaMembros?.membros?.some(
|
|
(m: MembroTime) => m.funcionario?._id === f._id
|
|
);
|
|
return !jaNaEquipe;
|
|
})
|
|
);
|
|
|
|
// Cores predefinidas
|
|
const coresDisponiveis = [
|
|
'#3B82F6', // Blue
|
|
'#10B981', // Green
|
|
'#F59E0B', // Yellow
|
|
'#EF4444', // Red
|
|
'#8B5CF6', // Purple
|
|
'#EC4899', // Pink
|
|
'#14B8A6', // Teal
|
|
'#F97316' // Orange
|
|
];
|
|
|
|
function novoTime() {
|
|
modoEdicao = true;
|
|
timeEmEdicao = null;
|
|
formNome = '';
|
|
formDescricao = '';
|
|
formGestorId = '';
|
|
formCor = coresDisponiveis[Math.floor(Math.random() * coresDisponiveis.length)];
|
|
}
|
|
|
|
function editarTime(time: TimeComDetalhes) {
|
|
modoEdicao = true;
|
|
timeEmEdicao = time;
|
|
formNome = time.nome;
|
|
formDescricao = time.descricao || '';
|
|
formGestorId = time.gestorId;
|
|
formCor = time.cor || '#3B82F6';
|
|
}
|
|
|
|
function cancelarEdicao() {
|
|
modoEdicao = false;
|
|
timeEmEdicao = null;
|
|
formNome = '';
|
|
formDescricao = '';
|
|
formGestorId = '';
|
|
formCor = '#3B82F6';
|
|
}
|
|
|
|
async function salvarTime() {
|
|
if (!formNome.trim() || !formGestorId) {
|
|
alert('Preencha todos os campos obrigatórios!');
|
|
return;
|
|
}
|
|
|
|
processando = true;
|
|
try {
|
|
if (timeEmEdicao) {
|
|
await client.mutation(api.times.atualizar, {
|
|
id: timeEmEdicao._id,
|
|
nome: formNome,
|
|
descricao: formDescricao || undefined,
|
|
gestorId: formGestorId as Id<'usuarios'>,
|
|
cor: formCor
|
|
});
|
|
} else {
|
|
await client.mutation(api.times.criar, {
|
|
nome: formNome,
|
|
descricao: formDescricao || undefined,
|
|
gestorId: formGestorId as Id<'usuarios'>,
|
|
cor: formCor
|
|
});
|
|
}
|
|
cancelarEdicao();
|
|
} catch (e: unknown) {
|
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
alert('Erro ao salvar: ' + errorMessage);
|
|
} finally {
|
|
processando = false;
|
|
}
|
|
}
|
|
|
|
function confirmarExclusao(time: TimeComDetalhes) {
|
|
timeParaExcluir = time;
|
|
mostrarConfirmacaoExclusao = true;
|
|
}
|
|
|
|
async function excluirTime() {
|
|
if (!timeParaExcluir) return;
|
|
|
|
processando = true;
|
|
try {
|
|
await client.mutation(api.times.desativar, { id: timeParaExcluir._id });
|
|
mostrarConfirmacaoExclusao = false;
|
|
timeParaExcluir = null;
|
|
} catch (e: unknown) {
|
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
alert('Erro ao excluir: ' + errorMessage);
|
|
} finally {
|
|
processando = false;
|
|
}
|
|
}
|
|
|
|
async function abrirGerenciarMembros(time: TimeComDetalhes) {
|
|
const detalhes = await client.query(api.times.obterPorId, { id: time._id });
|
|
if (detalhes) {
|
|
timeParaMembros = detalhes as TimeComMembros;
|
|
mostrarModalMembros = true;
|
|
}
|
|
}
|
|
|
|
async function adicionarMembro(funcionarioId: string) {
|
|
if (!timeParaMembros) return;
|
|
|
|
processando = true;
|
|
try {
|
|
await client.mutation(api.times.adicionarMembro, {
|
|
timeId: timeParaMembros._id,
|
|
funcionarioId: funcionarioId as Id<'funcionarios'>
|
|
});
|
|
|
|
// Recarregar detalhes do time
|
|
const detalhes = await client.query(api.times.obterPorId, {
|
|
id: timeParaMembros._id
|
|
});
|
|
if (detalhes) {
|
|
timeParaMembros = detalhes as TimeComMembros;
|
|
}
|
|
} catch (e: unknown) {
|
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
alert('Erro: ' + errorMessage);
|
|
} finally {
|
|
processando = false;
|
|
}
|
|
}
|
|
|
|
async function removerMembro(membroId: string) {
|
|
if (!confirm('Deseja realmente remover este membro do time?')) return;
|
|
|
|
processando = true;
|
|
try {
|
|
await client.mutation(api.times.removerMembro, {
|
|
membroId: membroId as Id<'timesMembros'>
|
|
});
|
|
|
|
// Recarregar detalhes do time
|
|
if (timeParaMembros) {
|
|
const detalhes = await client.query(api.times.obterPorId, {
|
|
id: timeParaMembros._id
|
|
});
|
|
if (detalhes) {
|
|
timeParaMembros = detalhes as TimeComMembros;
|
|
}
|
|
}
|
|
} catch (e: unknown) {
|
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
alert('Erro: ' + errorMessage);
|
|
} finally {
|
|
processando = false;
|
|
}
|
|
}
|
|
|
|
function fecharModalMembros() {
|
|
mostrarModalMembros = false;
|
|
timeParaMembros = null;
|
|
}
|
|
</script>
|
|
|
|
<ProtectedRoute allowedRoles={['ti_master', 'admin', 'ti_usuario']} maxLevel={1}>
|
|
<main class="container mx-auto max-w-7xl px-4 py-6">
|
|
<!-- Breadcrumb -->
|
|
<div class="breadcrumbs mb-4 text-sm">
|
|
<ul>
|
|
<li>
|
|
<a href={resolve('/ti')} class="text-primary hover:underline">Tecnologia da Informação</a>
|
|
</li>
|
|
<li>Gestão de Times</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Header -->
|
|
<div class="mb-6">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<div class="bg-secondary/10 rounded-xl p-3">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="text-secondary h-8 w-8"
|
|
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>
|
|
</div>
|
|
<div>
|
|
<h1 class="text-base-content text-3xl font-bold">Gestão de Times</h1>
|
|
<p class="text-base-content/60 mt-1">
|
|
Organize funcionários em equipes e defina gestores
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button class="btn gap-2" onclick={() => goto(resolve('/ti'))}>
|
|
<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="M10 19l-7-7m0 0l7-7m-7 7h18"
|
|
/>
|
|
</svg>
|
|
Voltar
|
|
</button>
|
|
<button class="btn btn-primary gap-2" onclick={novoTime}>
|
|
<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>
|
|
Novo Time
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Formulário de Edição -->
|
|
{#if modoEdicao}
|
|
<div
|
|
class="card from-primary/10 to-primary/5 border-primary/20 mb-6 border-2 bg-linear-to-br shadow-xl"
|
|
>
|
|
<div class="card-body">
|
|
<h2 class="card-title mb-4 text-xl">
|
|
{timeEmEdicao ? 'Editar Time' : 'Novo Time'}
|
|
</h2>
|
|
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<div class="form-control">
|
|
<label class="label" for="nome">
|
|
<span class="label-text font-semibold">Nome do Time *</span>
|
|
</label>
|
|
<input
|
|
id="nome"
|
|
type="text"
|
|
class="input input-bordered"
|
|
bind:value={formNome}
|
|
placeholder="Ex: Equipe Administrativa"
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label" for="gestor">
|
|
<span class="label-text font-semibold">Gestor *</span>
|
|
</label>
|
|
<select id="gestor" class="select select-bordered" bind:value={formGestorId}>
|
|
<option value="">Selecione um gestor</option>
|
|
{#each usuarios as usuario (usuario._id)}
|
|
<option value={usuario._id}>{usuario.nome}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-control md:col-span-2">
|
|
<label class="label" for="descricao">
|
|
<span class="label-text font-semibold">Descrição</span>
|
|
</label>
|
|
<textarea
|
|
id="descricao"
|
|
class="textarea textarea-bordered"
|
|
bind:value={formDescricao}
|
|
placeholder="Descrição opcional do time"
|
|
rows="2"
|
|
></textarea>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label" for="cor">
|
|
<span class="label-text font-semibold">Cor do Time</span>
|
|
</label>
|
|
<div class="flex flex-wrap gap-2">
|
|
{#each coresDisponiveis as cor (cor)}
|
|
<button
|
|
type="button"
|
|
class="h-10 w-10 rounded-lg border-2 transition-all hover:scale-110"
|
|
style="background-color: {cor}; border-color: {formCor === cor ? '#000' : cor}"
|
|
onclick={() => (formCor = cor)}
|
|
aria-label="Selecionar cor"
|
|
></button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-actions mt-6 justify-end">
|
|
<button class="btn" onclick={cancelarEdicao} disabled={processando}> Cancelar </button>
|
|
<button class="btn btn-primary" onclick={salvarTime} disabled={processando}>
|
|
{processando ? 'Salvando...' : 'Salvar'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Lista de Times -->
|
|
{#if carregando}
|
|
<div class="flex items-center justify-center py-20">
|
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
|
</div>
|
|
{:else}
|
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
{#each times.filter((t: TimeComDetalhes) => t.ativo) as time (time._id)}
|
|
<div
|
|
class="card bg-base-100 border-l-4 shadow-xl transition-all hover:shadow-2xl"
|
|
style="border-color: {time.cor || '#3B82F6'}"
|
|
>
|
|
<div class="card-body">
|
|
<div class="mb-2 flex items-start justify-between">
|
|
<div class="flex items-center gap-2">
|
|
<div
|
|
class="h-3 w-3 rounded-full"
|
|
style="background-color: {time.cor || '#3B82F6'}"
|
|
></div>
|
|
<h2 class="card-title text-lg">{time.nome}</h2>
|
|
</div>
|
|
<div class="dropdown dropdown-end">
|
|
<button type="button" class="btn btn-sm" aria-label="Menu do time">
|
|
<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 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>
|
|
</button>
|
|
<ul
|
|
role="menu"
|
|
class="dropdown-content menu bg-base-100 rounded-box border-base-300 z-1 w-52 border p-2 shadow-xl"
|
|
>
|
|
<li>
|
|
<button type="button" onclick={() => editarTime(time)}>
|
|
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
|
/>
|
|
</svg>
|
|
Editar
|
|
</button>
|
|
</li>
|
|
<li>
|
|
<button type="button" onclick={() => abrirGerenciarMembros(time)}>
|
|
<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>
|
|
Gerenciar Membros
|
|
</button>
|
|
</li>
|
|
<li>
|
|
<button
|
|
type="button"
|
|
onclick={() => confirmarExclusao(time)}
|
|
class="text-error"
|
|
>
|
|
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
/>
|
|
</svg>
|
|
Desativar
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="text-base-content/70 mb-3 min-h-8 text-sm">
|
|
{time.descricao || 'Sem descrição'}
|
|
</p>
|
|
|
|
<div class="divider my-2"></div>
|
|
|
|
<div class="space-y-2">
|
|
<div class="flex items-center gap-2 text-sm">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="text-primary h-4 w-4"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
<span class="text-base-content/70"
|
|
><strong>Gestor:</strong>
|
|
{time.gestor?.nome || 'Não definido'}</span
|
|
>
|
|
</div>
|
|
<div class="flex items-center gap-2 text-sm">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="text-primary 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-base-content/70"
|
|
><strong>Membros:</strong>
|
|
<span class="badge badge-primary badge-sm">{time.totalMembros || 0}</span></span
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-actions mt-4 justify-end">
|
|
<button
|
|
class="btn btn-sm btn-outline btn-primary"
|
|
onclick={() => abrirGerenciarMembros(time)}
|
|
>
|
|
Ver Detalhes
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
|
|
{#if times.filter((t: TimeComDetalhes) => t.ativo).length === 0}
|
|
<div class="col-span-full">
|
|
<div class="flex flex-col items-center justify-center py-16 text-center">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="text-base-content/30 h-16 w-16"
|
|
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>
|
|
<h3 class="mt-4 text-xl font-semibold">Nenhum time cadastrado</h3>
|
|
<p class="text-base-content/60 mt-2">
|
|
Clique em "Novo Time" para criar seu primeiro time
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Modal de Gerenciar Membros -->
|
|
{#if mostrarModalMembros && timeParaMembros}
|
|
<dialog class="modal modal-open">
|
|
<div class="modal-box max-w-4xl">
|
|
<h3 class="mb-4 flex items-center gap-2 text-2xl font-bold">
|
|
<div class="h-3 w-3 rounded-full" style="background-color: {timeParaMembros.cor}"></div>
|
|
{timeParaMembros.nome}
|
|
</h3>
|
|
|
|
<!-- Membros Atuais -->
|
|
<div class="mb-6">
|
|
<h4 class="mb-3 text-lg font-bold">
|
|
Membros Atuais ({timeParaMembros.membros?.length || 0})
|
|
</h4>
|
|
|
|
{#if timeParaMembros.membros && timeParaMembros.membros.length > 0}
|
|
<div class="max-h-60 space-y-2 overflow-y-auto">
|
|
{#each timeParaMembros.membros as membro (membro._id)}
|
|
<div class="bg-base-200 flex items-center justify-between rounded-lg p-3">
|
|
<div class="flex items-center gap-3">
|
|
<div class="avatar placeholder">
|
|
<div class="bg-primary text-primary-content w-10 rounded-full">
|
|
<span class="text-xs"
|
|
>{membro.funcionario?.nome.substring(0, 2).toUpperCase()}</span
|
|
>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="font-semibold">
|
|
{membro.funcionario?.nome}
|
|
</div>
|
|
<div class="text-base-content/50 text-xs">
|
|
Desde {new Date(membro.dataEntrada).toLocaleDateString('pt-BR')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button
|
|
class="btn btn-sm btn-error"
|
|
onclick={() => removerMembro(membro._id)}
|
|
disabled={processando}
|
|
aria-label="Remover membro"
|
|
>
|
|
<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="M6 18L18 6M6 6l12 12"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{:else}
|
|
<div class="alert alert-info">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
class="h-6 w-6 shrink-0 stroke-current"
|
|
>
|
|
<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 membro neste time ainda.</span>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="divider"></div>
|
|
|
|
<!-- Adicionar Membros -->
|
|
<div>
|
|
<h4 class="mb-3 text-lg font-bold">Adicionar Membros</h4>
|
|
|
|
{#if membrosDisponiveis.length > 0}
|
|
<div class="max-h-60 space-y-2 overflow-y-auto">
|
|
{#each membrosDisponiveis as funcionario (funcionario._id)}
|
|
<div
|
|
class="bg-base-200 hover:bg-base-300 flex items-center justify-between rounded-lg p-3 transition-colors"
|
|
>
|
|
<div class="flex items-center gap-3">
|
|
<div class="avatar placeholder">
|
|
<div class="bg-neutral text-neutral-content w-10 rounded-full">
|
|
<span class="text-xs"
|
|
>{funcionario.nome.substring(0, 2).toUpperCase()}</span
|
|
>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="font-semibold">{funcionario.nome}</div>
|
|
<div class="text-base-content/50 text-xs">
|
|
{funcionario.matricula || 'S/N'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button
|
|
class="btn btn-sm btn-primary"
|
|
onclick={() => adicionarMembro(funcionario._id)}
|
|
disabled={processando}
|
|
>
|
|
Adicionar
|
|
</button>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{:else}
|
|
<div class="alert">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
class="h-6 w-6 shrink-0 stroke-current"
|
|
>
|
|
<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>Todos os funcionários já estão em times.</span>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button class="btn" onclick={fecharModalMembros}>Fechar</button>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop">
|
|
<button type="button" onclick={fecharModalMembros} aria-label="Fechar modal"
|
|
>Fechar</button
|
|
>
|
|
</form>
|
|
</dialog>
|
|
{/if}
|
|
|
|
<!-- Modal de Confirmação de Exclusão -->
|
|
{#if mostrarConfirmacaoExclusao && timeParaExcluir}
|
|
<dialog class="modal modal-open">
|
|
<div class="modal-box">
|
|
<h3 class="text-lg font-bold">Confirmar Desativação</h3>
|
|
<p class="py-4">
|
|
Tem certeza que deseja desativar o time <strong>{timeParaExcluir.nome}</strong>? Todos
|
|
os membros serão removidos.
|
|
</p>
|
|
<div class="modal-action">
|
|
<button
|
|
class="btn"
|
|
onclick={() => (mostrarConfirmacaoExclusao = false)}
|
|
disabled={processando}
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button class="btn btn-error" onclick={excluirTime} disabled={processando}>
|
|
{processando ? 'Processando...' : 'Desativar'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop">
|
|
<button
|
|
type="button"
|
|
onclick={() => (mostrarConfirmacaoExclusao = false)}
|
|
aria-label="Fechar modal">Fechar</button
|
|
>
|
|
</form>
|
|
</dialog>
|
|
{/if}
|
|
</main>
|
|
</ProtectedRoute>
|