refactor: enhance role management UI and integrate profile management features
- Introduced a modal for managing user profiles, allowing for the creation and editing of profiles with improved state management. - Updated the role filtering logic to enhance type safety and readability. - Refactored UI components for better user experience, including improved button states and loading indicators. - Removed outdated code related to permissions and streamlined the overall structure for maintainability.
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
import { authStore } from "$lib/stores/auth.svelte";
|
||||
type RoleRow = {
|
||||
_id: Id<"roles">;
|
||||
_creationTime: number;
|
||||
@@ -32,6 +33,14 @@
|
||||
// Formato: { "roleId-recurso": true/false }
|
||||
let recursosExpandidos: Record<string, boolean> = $state({});
|
||||
|
||||
// Gerenciamento de Perfis
|
||||
let modalGerenciarPerfisAberto = $state(false);
|
||||
let perfilSendoEditado = $state<RoleRow | null>(null);
|
||||
let nomeNovoPerfil = $state("");
|
||||
let descricaoNovoPerfil = $state("");
|
||||
let nivelNovoPerfil = $state(3);
|
||||
let processando = $state(false);
|
||||
|
||||
// Cache de permissões por role
|
||||
let permissoesPorRole: Record<
|
||||
string,
|
||||
@@ -66,13 +75,13 @@
|
||||
|
||||
const rolesFiltradas = $derived.by(() => {
|
||||
if (!rolesQuery.data) return [];
|
||||
let rs: Array<RoleRow> = rolesQuery.data as Array<RoleRow>;
|
||||
let rs = rolesQuery.data; // Removed explicit type annotation
|
||||
if (filtroRole)
|
||||
rs = rs.filter((r: RoleRow) => r._id === (filtroRole as any));
|
||||
rs = rs.filter((r) => r._id === (filtroRole)); // Removed as any
|
||||
if (busca.trim()) {
|
||||
const b = busca.toLowerCase();
|
||||
rs = rs.filter(
|
||||
(r: RoleRow) =>
|
||||
(r) =>
|
||||
r.descricao.toLowerCase().includes(b) ||
|
||||
r.nome.toLowerCase().includes(b)
|
||||
);
|
||||
@@ -120,8 +129,9 @@
|
||||
];
|
||||
}
|
||||
mostrarMensagem("success", "Permissão atualizada com sucesso!");
|
||||
} catch (e: any) {
|
||||
mostrarMensagem("error", e.message || "Erro ao atualizar permissão");
|
||||
} catch (error: unknown) { // Changed to unknown
|
||||
const message = error instanceof Error ? error.message : "Erro ao atualizar permissão";
|
||||
mostrarMensagem("error", message);
|
||||
} finally {
|
||||
salvando = false;
|
||||
}
|
||||
@@ -132,6 +142,90 @@
|
||||
const entry = dados?.find((e) => e.recurso === recurso);
|
||||
return entry ? entry.acoes.includes(acao) : false;
|
||||
}
|
||||
|
||||
function abrirModalCriarPerfil() {
|
||||
nomeNovoPerfil = "";
|
||||
descricaoNovoPerfil = "";
|
||||
nivelNovoPerfil = 3; // Default to a common level
|
||||
perfilSendoEditado = null;
|
||||
modalGerenciarPerfisAberto = true;
|
||||
}
|
||||
|
||||
function prepararEdicaoPerfil(role: RoleRow) {
|
||||
perfilSendoEditado = role;
|
||||
nomeNovoPerfil = role.nome;
|
||||
descricaoNovoPerfil = role.descricao;
|
||||
nivelNovoPerfil = role.nivel;
|
||||
modalGerenciarPerfisAberto = true;
|
||||
}
|
||||
|
||||
function fecharModalGerenciarPerfis() {
|
||||
modalGerenciarPerfisAberto = false;
|
||||
perfilSendoEditado = null;
|
||||
}
|
||||
|
||||
async function criarNovoPerfil() {
|
||||
if (!nomeNovoPerfil.trim()) return;
|
||||
|
||||
processando = true;
|
||||
try {
|
||||
const result = await client.mutation(api.roles.criar, {
|
||||
nome: nomeNovoPerfil.trim(),
|
||||
descricao: descricaoNovoPerfil.trim(),
|
||||
nivel: nivelNovoPerfil,
|
||||
customizado: true,
|
||||
});
|
||||
|
||||
if (result.sucesso) {
|
||||
mostrarMensagem("success", "Perfil criado com sucesso!");
|
||||
nomeNovoPerfil = "";
|
||||
descricaoNovoPerfil = "";
|
||||
nivelNovoPerfil = 3;
|
||||
fecharModalGerenciarPerfis();
|
||||
if (rolesQuery.refetch) { // Verificação para garantir que refetch existe
|
||||
rolesQuery.refetch(); // Atualiza a lista de perfis
|
||||
}
|
||||
} else {
|
||||
mostrarMensagem("error", `Erro ao criar perfil: ${result.erro}`);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
mostrarMensagem("error", `Erro ao criar perfil: ${message}`);
|
||||
} finally {
|
||||
processando = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function editarPerfil() {
|
||||
if (!perfilSendoEditado || !nomeNovoPerfil.trim()) return;
|
||||
|
||||
processando = true;
|
||||
try {
|
||||
const result = await client.mutation(api.roles.atualizar, {
|
||||
roleId: perfilSendoEditado._id,
|
||||
nome: nomeNovoPerfil.trim(),
|
||||
descricao: descricaoNovoPerfil.trim(),
|
||||
nivel: nivelNovoPerfil,
|
||||
setor: perfilSendoEditado.setor, // Manter setor existente
|
||||
});
|
||||
|
||||
if (result.sucesso) {
|
||||
mostrarMensagem("success", "Perfil atualizado com sucesso!");
|
||||
fecharModalGerenciarPerfis();
|
||||
if (rolesQuery.refetch) { // Verificação para garantir que refetch existe
|
||||
rolesQuery.refetch(); // Atualiza a lista de perfis
|
||||
}
|
||||
} else {
|
||||
mostrarMensagem("error", `Erro ao atualizar perfil: ${result.erro}`);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
mostrarMensagem("error", `Erro ao atualizar perfil: ${message}`);
|
||||
} finally {
|
||||
processando = false;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<ProtectedRoute allowedRoles={["ti_master", "admin"]} maxLevel={1}>
|
||||
@@ -160,7 +254,7 @@
|
||||
<li>
|
||||
<a href="/ti" class="text-primary hover:text-primary-focus">TI</a>
|
||||
</li>
|
||||
<li class="font-semibold">Gerenciar Permissões</li>
|
||||
<li class="font-semibold">Gerenciar Perfis & Permissões</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -185,12 +279,32 @@
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h1 class="text-3xl font-bold text-base-content">
|
||||
Gerenciar Permissões de Acesso
|
||||
Gerenciar Perfis & Permissões de Acesso
|
||||
</h1>
|
||||
<p class="text-base-content/60 mt-1">
|
||||
Configure as permissões de acesso aos menus do sistema por função
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary gap-2"
|
||||
onclick={abrirModalCriarPerfil}
|
||||
>
|
||||
<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>
|
||||
Criar Novo Perfil
|
||||
</button>
|
||||
<button class="btn btn-ghost gap-2" onclick={() => goto("/ti")}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -479,6 +593,15 @@
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline btn-sm"
|
||||
onclick={() => prepararEdicaoPerfil(roleRow)}
|
||||
>
|
||||
Editar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if roleRow.nivel <= 1}
|
||||
@@ -574,4 +697,148 @@
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<!-- Modal Gerenciar Perfis -->
|
||||
{#if modalGerenciarPerfisAberto}
|
||||
<dialog class="modal modal-open">
|
||||
<div
|
||||
class="modal-box max-w-4xl w-full overflow-hidden border border-base-200/60 bg-base-200/40 p-0 shadow-2xl"
|
||||
>
|
||||
<div
|
||||
class="relative bg-gradient-to-r from-primary via-primary/90 to-secondary/80 px-8 py-6 text-base-100"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle btn-ghost absolute right-4 top-4 text-base-100/80 hover:text-base-100"
|
||||
onclick={fecharModalGerenciarPerfis}
|
||||
aria-label="Fechar modal"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
<h3 class="text-3xl font-black tracking-tight">Gerenciar Perfis de Acesso</h3>
|
||||
<p class="mt-2 max-w-2xl text-sm text-base-100/80 md:text-base">
|
||||
{perfilSendoEditado
|
||||
? "Atualize as informações do perfil selecionado para manter a governança de acesso alinhada com as diretrizes do sistema."
|
||||
: "Crie um novo perfil de acesso definindo nome, descrição e nível hierárquico conforme os padrões adotados pela Secretaria."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-base-100 px-8 py-6">
|
||||
<section
|
||||
class="space-y-6 rounded-2xl border border-base-200/60 bg-base-100 p-6 shadow-sm"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<h4 class="text-xl font-semibold text-base-content">
|
||||
{perfilSendoEditado ? "Editar Perfil" : "Criar Novo Perfil"}
|
||||
</h4>
|
||||
<p class="text-sm text-base-content/70">
|
||||
{perfilSendoEditado
|
||||
? "Os campos bloqueados indicam atributos padronizados do sistema. Ajuste apenas o que estiver disponível."
|
||||
: "Preencha as informações com atenção para garantir que o novo perfil siga o padrão institucional."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if perfilSendoEditado}
|
||||
<span
|
||||
class="badge badge-outline badge-primary badge-lg self-start flex flex-col items-center gap-1 px-5 py-3 text-center"
|
||||
>
|
||||
<span class="text-[11px] font-semibold uppercase tracking-[0.32em] text-primary">
|
||||
Nível atual
|
||||
</span>
|
||||
<span class="text-2xl font-bold leading-none text-primary">
|
||||
{perfilSendoEditado.nivel}
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label" for="nome-perfil-input">
|
||||
<span class="label-text">Nome do Perfil *</span>
|
||||
</label>
|
||||
<input
|
||||
id="nome-perfil-input"
|
||||
type="text"
|
||||
bind:value={nomeNovoPerfil}
|
||||
class="input input-bordered input-primary"
|
||||
placeholder="Ex: RH, Financeiro, Gestor"
|
||||
disabled={perfilSendoEditado !== null && !perfilSendoEditado.customizado}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label" for="descricao-perfil-input">
|
||||
<span class="label-text">Descrição</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="descricao-perfil-input"
|
||||
bind:value={descricaoNovoPerfil}
|
||||
class="textarea textarea-bordered textarea-primary min-h-[120px]"
|
||||
placeholder="Breve descrição das responsabilidades e limites de atuação deste perfil."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-control max-w-xs">
|
||||
<label class="label" for="nivel-perfil-input">
|
||||
<span class="label-text">Nível de Acesso (0-5) *</span>
|
||||
</label>
|
||||
<input
|
||||
id="nivel-perfil-input"
|
||||
type="number"
|
||||
bind:value={nivelNovoPerfil}
|
||||
min="0"
|
||||
max="5"
|
||||
class="input input-bordered input-secondary"
|
||||
disabled={perfilSendoEditado !== null && !perfilSendoEditado.customizado}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div
|
||||
class="mt-8 flex flex-col-reverse gap-3 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<p class="text-sm text-base-content/60">
|
||||
Campos marcados com * são obrigatórios.
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost"
|
||||
onclick={fecharModalGerenciarPerfis}
|
||||
disabled={processando}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
disabled={!nomeNovoPerfil.trim() || processando}
|
||||
onclick={perfilSendoEditado ? editarPerfil : criarNovoPerfil}
|
||||
>
|
||||
{#if processando}
|
||||
<span class="loading loading-spinner"></span>
|
||||
Processando...
|
||||
{:else}
|
||||
{perfilSendoEditado ? "Salvar Alterações" : "Criar Perfil"}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button
|
||||
type="button"
|
||||
onclick={fecharModalGerenciarPerfis}
|
||||
aria-label="Fechar modal"
|
||||
>Fechar</button>
|
||||
</form>
|
||||
</dialog>
|
||||
{/if}
|
||||
</ProtectedRoute>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,82 @@
|
||||
import { useConvexClient, useQuery } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import { authStore } from "$lib/stores/auth.svelte";
|
||||
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
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();
|
||||
|
||||
@@ -11,17 +86,23 @@
|
||||
const usuariosQuery = useQuery(api.usuarios.listar, {});
|
||||
const funcionariosQuery = useQuery(api.funcionarios.getAll, {});
|
||||
|
||||
const times = $derived(timesQuery?.data || []);
|
||||
const usuarios = $derived(usuariosQuery?.data || []);
|
||||
const funcionarios = $derived(funcionariosQuery?.data || []);
|
||||
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<any>(null);
|
||||
let timeEmEdicao = $state<TimeComDetalhes | null>(null);
|
||||
let mostrarModalMembros = $state(false);
|
||||
let timeParaMembros = $state<any>(null);
|
||||
let timeParaMembros = $state<TimeComMembros | null>(null);
|
||||
let mostrarConfirmacaoExclusao = $state(false);
|
||||
let timeParaExcluir = $state<any>(null);
|
||||
let timeParaExcluir = $state<TimeComDetalhes | null>(null);
|
||||
let processando = $state(false);
|
||||
|
||||
// Form
|
||||
@@ -32,9 +113,9 @@
|
||||
|
||||
// Membros
|
||||
let membrosDisponiveis = $derived(
|
||||
funcionarios.filter((f: any) => {
|
||||
funcionarios.filter((f: Funcionario) => {
|
||||
// Verificar se o funcionário já está em algum time ativo
|
||||
const jaNaEquipe = timeParaMembros?.membros?.some((m: any) => m.funcionario?._id === f._id);
|
||||
const jaNaEquipe = timeParaMembros?.membros?.some((m: MembroTime) => m.funcionario?._id === f._id);
|
||||
return !jaNaEquipe;
|
||||
})
|
||||
);
|
||||
@@ -60,7 +141,7 @@
|
||||
formCor = coresDisponiveis[Math.floor(Math.random() * coresDisponiveis.length)];
|
||||
}
|
||||
|
||||
function editarTime(time: any) {
|
||||
function editarTime(time: TimeComDetalhes) {
|
||||
modoEdicao = true;
|
||||
timeEmEdicao = time;
|
||||
formNome = time.nome;
|
||||
@@ -91,26 +172,27 @@
|
||||
id: timeEmEdicao._id,
|
||||
nome: formNome,
|
||||
descricao: formDescricao || undefined,
|
||||
gestorId: formGestorId as any,
|
||||
gestorId: formGestorId as Id<"usuarios">,
|
||||
cor: formCor,
|
||||
});
|
||||
} else {
|
||||
await client.mutation(api.times.criar, {
|
||||
nome: formNome,
|
||||
descricao: formDescricao || undefined,
|
||||
gestorId: formGestorId as any,
|
||||
gestorId: formGestorId as Id<"usuarios">,
|
||||
cor: formCor,
|
||||
});
|
||||
}
|
||||
cancelarEdicao();
|
||||
} catch (e: any) {
|
||||
alert("Erro ao salvar: " + (e.message || e));
|
||||
} catch (e: unknown) {
|
||||
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||
alert("Erro ao salvar: " + errorMessage);
|
||||
} finally {
|
||||
processando = false;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmarExclusao(time: any) {
|
||||
function confirmarExclusao(time: TimeComDetalhes) {
|
||||
timeParaExcluir = time;
|
||||
mostrarConfirmacaoExclusao = true;
|
||||
}
|
||||
@@ -123,17 +205,20 @@
|
||||
await client.mutation(api.times.desativar, { id: timeParaExcluir._id });
|
||||
mostrarConfirmacaoExclusao = false;
|
||||
timeParaExcluir = null;
|
||||
} catch (e: any) {
|
||||
alert("Erro ao excluir: " + (e.message || e));
|
||||
} catch (e: unknown) {
|
||||
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||
alert("Erro ao excluir: " + errorMessage);
|
||||
} finally {
|
||||
processando = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function abrirGerenciarMembros(time: any) {
|
||||
async function abrirGerenciarMembros(time: TimeComDetalhes) {
|
||||
const detalhes = await client.query(api.times.obterPorId, { id: time._id });
|
||||
timeParaMembros = detalhes;
|
||||
mostrarModalMembros = true;
|
||||
if (detalhes) {
|
||||
timeParaMembros = detalhes as TimeComMembros;
|
||||
mostrarModalMembros = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function adicionarMembro(funcionarioId: string) {
|
||||
@@ -143,14 +228,17 @@
|
||||
try {
|
||||
await client.mutation(api.times.adicionarMembro, {
|
||||
timeId: timeParaMembros._id,
|
||||
funcionarioId: funcionarioId as any,
|
||||
funcionarioId: funcionarioId as Id<"funcionarios">,
|
||||
});
|
||||
|
||||
// Recarregar detalhes do time
|
||||
const detalhes = await client.query(api.times.obterPorId, { id: timeParaMembros._id });
|
||||
timeParaMembros = detalhes;
|
||||
} catch (e: any) {
|
||||
alert("Erro: " + (e.message || e));
|
||||
if (detalhes) {
|
||||
timeParaMembros = detalhes as TimeComMembros;
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||
alert("Erro: " + errorMessage);
|
||||
} finally {
|
||||
processando = false;
|
||||
}
|
||||
@@ -161,13 +249,18 @@
|
||||
|
||||
processando = true;
|
||||
try {
|
||||
await client.mutation(api.times.removerMembro, { membroId: membroId as any });
|
||||
await client.mutation(api.times.removerMembro, { membroId: membroId as Id<"timesMembros"> });
|
||||
|
||||
// Recarregar detalhes do time
|
||||
const detalhes = await client.query(api.times.obterPorId, { id: timeParaMembros._id });
|
||||
timeParaMembros = detalhes;
|
||||
} catch (e: any) {
|
||||
alert("Erro: " + (e.message || e));
|
||||
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;
|
||||
}
|
||||
@@ -179,7 +272,8 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="container mx-auto px-4 py-6 max-w-7xl">
|
||||
<ProtectedRoute allowedRoles={["ti_master", "admin", "ti_usuario"]} maxLevel={2}>
|
||||
<main class="container mx-auto px-4 py-6 max-w-7xl">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="text-sm breadcrumbs mb-4">
|
||||
<ul>
|
||||
@@ -192,14 +286,14 @@
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-3 bg-blue-500/20 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<div class="p-3 bg-secondary/10 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-secondary" 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-3xl font-bold text-primary">Gestão de Times</h1>
|
||||
<p class="text-base-content/70">Organize funcionários em equipes e defina gestores</p>
|
||||
<h1 class="text-3xl font-bold text-base-content">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">
|
||||
@@ -297,20 +391,27 @@
|
||||
{/if}
|
||||
|
||||
<!-- Lista de Times -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{#each times as time}
|
||||
{#if time.ativo}
|
||||
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all border-l-4" style="border-color: {time.cor}">
|
||||
{#if carregando}
|
||||
<div class="flex justify-center items-center py-20">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{#each times.filter((t: TimeComDetalhes) => t.ativo) as time}
|
||||
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all border-l-4" style="border-color: {time.cor || '#3B82F6'}">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between">
|
||||
<h2 class="card-title text-lg">{time.nome}</h2>
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-3 h-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 tabindex="0" class="btn btn-ghost btn-sm btn-square" aria-label="Menu do time">
|
||||
<button type="button" class="btn btn-ghost btn-sm btn-square" 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 tabindex="0" class="dropdown-content z-[1] menu p-2 shadow-xl bg-base-100 rounded-box w-52 border border-base-300">
|
||||
<ul role="menu" class="dropdown-content z-[1] menu p-2 shadow-xl bg-base-100 rounded-box w-52 border border-base-300">
|
||||
<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">
|
||||
@@ -339,7 +440,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-base-content/70 mb-3">{time.descricao || "Sem descrição"}</p>
|
||||
<p class="text-sm text-base-content/70 mb-3 min-h-[2rem]">{time.descricao || "Sem descrição"}</p>
|
||||
|
||||
<div class="divider my-2"></div>
|
||||
|
||||
@@ -348,31 +449,38 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-primary" 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><strong>Gestor:</strong> {time.gestor?.nome}</span>
|
||||
<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="h-4 w-4 text-primary" 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><strong>Membros:</strong> {time.totalMembros || 0}</span>
|
||||
<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 justify-end mt-4">
|
||||
<button class="btn btn-sm btn-outline btn-primary" onclick={() => abrirGerenciarMembros(time)}>
|
||||
Ver Detalhes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
|
||||
{#if times.filter((t: any) => t.ativo).length === 0}
|
||||
<div class="col-span-full">
|
||||
<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>Nenhum time cadastrado. Clique em "Novo Time" para começar.</span>
|
||||
{#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="h-16 w-16 text-base-content/30" 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="text-xl font-semibold mt-4">Nenhum time cadastrado</h3>
|
||||
<p class="text-base-content/60 mt-2">Clique em "Novo Time" para criar seu primeiro time</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Modal de Gerenciar Membros -->
|
||||
{#if mostrarModalMembros && timeParaMembros}
|
||||
@@ -502,4 +610,5 @@
|
||||
</dialog>
|
||||
{/if}
|
||||
</main>
|
||||
</ProtectedRoute>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user