Merge pull request #15 from killer-cf/fix-page-with-lint-errors
Fix page with lint errors
This commit is contained in:
19
.cursor/mcp.json
Normal file
19
.cursor/mcp.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"svelte": {
|
||||||
|
"url": "https://mcp.svelte.dev/mcp"
|
||||||
|
},
|
||||||
|
"context7": {
|
||||||
|
"url": "https://mcp.context7.com/mcp"
|
||||||
|
},
|
||||||
|
"convex": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"convex@latest",
|
||||||
|
"mcp",
|
||||||
|
"start"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
.cursor/rules/svelte_rules.mdc
Normal file
27
.cursor/rules/svelte_rules.mdc
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
|
||||||
|
|
||||||
|
## Available MCP Tools:
|
||||||
|
|
||||||
|
### 1. list-sections
|
||||||
|
|
||||||
|
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
|
||||||
|
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
|
||||||
|
|
||||||
|
### 2. get-documentation
|
||||||
|
|
||||||
|
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
|
||||||
|
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
|
||||||
|
|
||||||
|
### 3. svelte-autofixer
|
||||||
|
|
||||||
|
Analyzes Svelte code and returns issues and suggestions.
|
||||||
|
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
|
||||||
|
|
||||||
|
### 4. playground-link
|
||||||
|
|
||||||
|
Generates a Svelte Playground link with the provided code.
|
||||||
|
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
description: Guidelines for TypeScript usage, including type safety rules and Convex query typing
|
description: Guidelines for TypeScript usage, including type safety rules and Convex query typing
|
||||||
globs: **/*.ts,**/*.tsx,**/*.svelte
|
globs: **/*.ts,**/*.svelte
|
||||||
|
alwaysApply: false
|
||||||
---
|
---
|
||||||
|
|
||||||
# TypeScript Guidelines
|
# TypeScript Guidelines
|
||||||
@@ -8,6 +9,7 @@ globs: **/*.ts,**/*.tsx,**/*.svelte
|
|||||||
## Type Safety Rules
|
## Type Safety Rules
|
||||||
|
|
||||||
### Avoid `any` Type
|
### Avoid `any` Type
|
||||||
|
|
||||||
- **NEVER** use the `any` type in production code
|
- **NEVER** use the `any` type in production code
|
||||||
- The only exception is in test files (files matching `*.test.ts`, `*.test.tsx`, `*.spec.ts`, `*.spec.tsx`)
|
- The only exception is in test files (files matching `*.test.ts`, `*.test.tsx`, `*.spec.ts`, `*.spec.tsx`)
|
||||||
- Instead of `any`, use:
|
- Instead of `any`, use:
|
||||||
@@ -20,6 +22,7 @@ globs: **/*.ts,**/*.tsx,**/*.svelte
|
|||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
**❌ Bad:**
|
**❌ Bad:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function processData(data: any) {
|
function processData(data: any) {
|
||||||
return data.value;
|
return data.value;
|
||||||
@@ -27,6 +30,7 @@ function processData(data: any) {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**✅ Good:**
|
**✅ Good:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function processData(data: { value: string }) {
|
function processData(data: { value: string }) {
|
||||||
return data.value;
|
return data.value;
|
||||||
@@ -47,6 +51,7 @@ function processData(data: unknown) {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**✅ Exception (tests only):**
|
**✅ Exception (tests only):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// test.ts or *.spec.ts
|
// test.ts or *.spec.ts
|
||||||
it('should handle any input', () => {
|
it('should handle any input', () => {
|
||||||
@@ -58,6 +63,7 @@ it('should handle any input', () => {
|
|||||||
## Convex Query Typing
|
## Convex Query Typing
|
||||||
|
|
||||||
### Frontend Query Usage
|
### Frontend Query Usage
|
||||||
|
|
||||||
- **DO NOT** create manual type definitions for Convex query results in the frontend
|
- **DO NOT** create manual type definitions for Convex query results in the frontend
|
||||||
- Convex queries already return properly typed results based on their `returns` validator
|
- Convex queries already return properly typed results based on their `returns` validator
|
||||||
- The TypeScript types are automatically inferred from the query's return validator
|
- The TypeScript types are automatically inferred from the query's return validator
|
||||||
@@ -66,10 +72,11 @@ it('should handle any input', () => {
|
|||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
**❌ Bad:**
|
**❌ Bad:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Don't manually type the result
|
// Don't manually type the result
|
||||||
type UserListResult = Array<{
|
type UserListResult = Array<{
|
||||||
_id: Id<"users">;
|
_id: Id<'users'>;
|
||||||
name: string;
|
name: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
@@ -77,6 +84,7 @@ const users: UserListResult = useQuery(api.users.list);
|
|||||||
```
|
```
|
||||||
|
|
||||||
**✅ Good:**
|
**✅ Good:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Let TypeScript infer the type from the query
|
// Let TypeScript infer the type from the query
|
||||||
const users = useQuery(api.users.list);
|
const users = useQuery(api.users.list);
|
||||||
@@ -84,7 +92,7 @@ const users = useQuery(api.users.list);
|
|||||||
|
|
||||||
// You can still use it with type inference
|
// You can still use it with type inference
|
||||||
if (users !== undefined) {
|
if (users !== undefined) {
|
||||||
users.forEach(user => {
|
users.forEach((user) => {
|
||||||
// TypeScript knows user._id is Id<"users"> and user.name is string
|
// TypeScript knows user._id is Id<"users"> and user.name is string
|
||||||
console.log(user.name);
|
console.log(user.name);
|
||||||
});
|
});
|
||||||
@@ -92,16 +100,18 @@ if (users !== undefined) {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**✅ Good (with explicit type if needed for clarity):**
|
**✅ Good (with explicit type if needed for clarity):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Only if you need to export or explicitly annotate for documentation
|
// Only if you need to export or explicitly annotate for documentation
|
||||||
import type { FunctionReturnType } from "convex/server";
|
import type { FunctionReturnType } from 'convex/server';
|
||||||
import type { api } from "./convex/_generated/api";
|
import type { api } from './convex/_generated/api';
|
||||||
|
|
||||||
type UserListResult = FunctionReturnType<typeof api.users.list>;
|
type UserListResult = FunctionReturnType<typeof api.users.list>;
|
||||||
const users = useQuery(api.users.list);
|
const users = useQuery(api.users.list);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Best Practices
|
### Best Practices
|
||||||
|
|
||||||
- Trust Convex's type inference - it's based on your schema and validators
|
- Trust Convex's type inference - it's based on your schema and validators
|
||||||
- If you need type annotations, use `FunctionReturnType` from Convex's type utilities
|
- If you need type annotations, use `FunctionReturnType` from Convex's type utilities
|
||||||
- Only create manual types if you're doing complex transformations that need intermediate types
|
- Only create manual types if you're doing complex transformations that need intermediate types
|
||||||
|
|||||||
@@ -1,20 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useQuery, useConvexClient } from "convex-svelte";
|
import { useQuery, useConvexClient } from 'convex-svelte';
|
||||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from '$app/navigation';
|
||||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
import { resolve } from '$app/paths';
|
||||||
type RoleRow = {
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
_id: Id<"roles">;
|
|
||||||
_creationTime: number;
|
|
||||||
nome: string;
|
|
||||||
descricao: string;
|
|
||||||
nivel: number;
|
|
||||||
setor?: string;
|
|
||||||
customizado: boolean;
|
|
||||||
editavel?: boolean;
|
|
||||||
criadoPor?: Id<"usuarios">;
|
|
||||||
};
|
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
@@ -23,49 +13,43 @@
|
|||||||
const catalogoQuery = useQuery(api.permissoesAcoes.listarRecursosEAcoes, {});
|
const catalogoQuery = useQuery(api.permissoesAcoes.listarRecursosEAcoes, {});
|
||||||
|
|
||||||
let salvando = $state(false);
|
let salvando = $state(false);
|
||||||
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(
|
let mensagem = $state<{ tipo: 'success' | 'error'; texto: string } | null>(null);
|
||||||
null,
|
let busca = $state('');
|
||||||
);
|
let filtroRole = $state<Id<'roles'> | ''>('');
|
||||||
let busca = $state("");
|
let modalNovoPerfilAberto = $state(false);
|
||||||
let filtroRole = $state("");
|
let nomeNovoPerfil = $state('');
|
||||||
|
let descricaoNovoPerfil = $state('');
|
||||||
|
let setorNovoPerfil = $state('');
|
||||||
|
let nivelNovoPerfil = $state(3);
|
||||||
|
let roleParaDuplicar = $state<Id<'roles'> | ''>('');
|
||||||
|
let criandoNovoPerfil = $state(false);
|
||||||
|
|
||||||
// Controla quais recursos estão expandidos (mostrando as ações) por perfil
|
// Controla quais recursos estão expandidos (mostrando as ações) por perfil
|
||||||
// Formato: { "roleId-recurso": true/false }
|
// Formato: { "roleId-recurso": true/false }
|
||||||
let recursosExpandidos: Record<string, boolean> = $state({});
|
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
|
// Cache de permissões por role
|
||||||
let permissoesPorRole: Record<
|
let permissoesPorRole: Record<string, Array<{ recurso: string; acoes: Array<string> }>> = $state(
|
||||||
string,
|
{}
|
||||||
Array<{ recurso: string; acoes: Array<string> }>
|
|
||||||
> = $state({});
|
|
||||||
|
|
||||||
async function carregarPermissoesRole(roleId: Id<"roles">) {
|
|
||||||
if (permissoesPorRole[roleId]) return;
|
|
||||||
const dados = await client.query(
|
|
||||||
api.permissoesAcoes.listarPermissoesAcoesPorRole,
|
|
||||||
{ roleId },
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
async function carregarPermissoesRole(roleId: Id<'roles'>) {
|
||||||
|
if (permissoesPorRole[roleId]) return;
|
||||||
|
const dados = await client.query(api.permissoesAcoes.listarPermissoesAcoesPorRole, { roleId });
|
||||||
permissoesPorRole[roleId] = dados;
|
permissoesPorRole[roleId] = dados;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleRecurso(roleId: Id<"roles">, recurso: string) {
|
function toggleRecurso(roleId: Id<'roles'>, recurso: string) {
|
||||||
const key = `${roleId}-${recurso}`;
|
const key = `${roleId}-${recurso}`;
|
||||||
recursosExpandidos[key] = !recursosExpandidos[key];
|
recursosExpandidos[key] = !recursosExpandidos[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRecursoExpandido(roleId: Id<"roles">, recurso: string) {
|
function isRecursoExpandido(roleId: Id<'roles'>, recurso: string) {
|
||||||
const key = `${roleId}-${recurso}`;
|
const key = `${roleId}-${recurso}`;
|
||||||
return recursosExpandidos[key] ?? false;
|
return recursosExpandidos[key] ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mostrarMensagem(tipo: "success" | "error", texto: string) {
|
function mostrarMensagem(tipo: 'success' | 'error', texto: string) {
|
||||||
mensagem = { tipo, texto };
|
mensagem = { tipo, texto };
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
mensagem = null;
|
mensagem = null;
|
||||||
@@ -79,9 +63,7 @@
|
|||||||
if (busca.trim()) {
|
if (busca.trim()) {
|
||||||
const b = busca.toLowerCase();
|
const b = busca.toLowerCase();
|
||||||
rs = rs.filter(
|
rs = rs.filter(
|
||||||
(r) =>
|
(r) => r.descricao.toLowerCase().includes(b) || r.nome.toLowerCase().includes(b)
|
||||||
r.descricao.toLowerCase().includes(b) ||
|
|
||||||
r.nome.toLowerCase().includes(b),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return rs;
|
return rs;
|
||||||
@@ -98,143 +80,142 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function toggleAcao(
|
async function toggleAcao(roleId: Id<'roles'>, recurso: string, acao: string, conceder: boolean) {
|
||||||
roleId: Id<"roles">,
|
|
||||||
recurso: string,
|
|
||||||
acao: string,
|
|
||||||
conceder: boolean,
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
salvando = true;
|
salvando = true;
|
||||||
await client.mutation(api.permissoesAcoes.atualizarPermissaoAcao, {
|
await client.mutation(api.permissoesAcoes.atualizarPermissaoAcao, {
|
||||||
roleId,
|
roleId,
|
||||||
recurso,
|
recurso,
|
||||||
acao,
|
acao,
|
||||||
conceder,
|
conceder
|
||||||
});
|
});
|
||||||
// Atualizar cache local
|
// Atualizar cache local
|
||||||
const atual = permissoesPorRole[roleId] || [];
|
const atual = permissoesPorRole[roleId] || [];
|
||||||
const entry = atual.find((e) => e.recurso === recurso);
|
const entry = atual.find((e) => e.recurso === recurso);
|
||||||
if (entry) {
|
if (entry) {
|
||||||
const set = new Set(entry.acoes);
|
entry.acoes = conceder
|
||||||
if (conceder) set.add(acao);
|
? [...entry.acoes.filter((valor) => valor !== acao), acao]
|
||||||
else set.delete(acao);
|
: entry.acoes.filter((valor) => valor !== acao);
|
||||||
entry.acoes = Array.from(set);
|
} else if (conceder) {
|
||||||
} else {
|
permissoesPorRole[roleId] = [...atual, { recurso, acoes: [acao] }];
|
||||||
permissoesPorRole[roleId] = [
|
|
||||||
...atual,
|
|
||||||
{ recurso, acoes: conceder ? [acao] : [] },
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
mostrarMensagem("success", "Permissão atualizada com sucesso!");
|
mostrarMensagem('success', 'Permissão atualizada com sucesso!');
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// Changed to unknown
|
// Changed to unknown
|
||||||
const message =
|
const message = error instanceof Error ? error.message : 'Erro ao atualizar permissão';
|
||||||
error instanceof Error ? error.message : "Erro ao atualizar permissão";
|
mostrarMensagem('error', message);
|
||||||
mostrarMensagem("error", message);
|
|
||||||
} finally {
|
} finally {
|
||||||
salvando = false;
|
salvando = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isConcedida(roleId: Id<"roles">, recurso: string, acao: string) {
|
function isConcedida(roleId: Id<'roles'>, recurso: string, acao: string) {
|
||||||
const dados = permissoesPorRole[roleId];
|
const dados = permissoesPorRole[roleId];
|
||||||
const entry = dados?.find((e) => e.recurso === recurso);
|
const entry = dados?.find((e) => e.recurso === recurso);
|
||||||
return entry ? entry.acoes.includes(acao) : false;
|
return entry ? entry.acoes.includes(acao) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function abrirModalCriarPerfil() {
|
const gerarIdentificador = (valor: string) =>
|
||||||
nomeNovoPerfil = "";
|
valor
|
||||||
descricaoNovoPerfil = "";
|
.normalize('NFD')
|
||||||
nivelNovoPerfil = 3; // Default to a common level
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
perfilSendoEditado = null;
|
.trim()
|
||||||
modalGerenciarPerfisAberto = true;
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '_')
|
||||||
|
.replace(/^_+|_+$/g, '')
|
||||||
|
.replace(/_{2,}/g, '_');
|
||||||
|
|
||||||
|
const identificadorSugerido = $derived.by(() => gerarIdentificador(nomeNovoPerfil));
|
||||||
|
|
||||||
|
const podeSalvarNovoPerfil = $derived.by(() => {
|
||||||
|
const nome = nomeNovoPerfil.trim();
|
||||||
|
const nivel = Number(nivelNovoPerfil);
|
||||||
|
const nivelValido = Number.isFinite(nivel) && nivel >= 0 && nivel <= 10;
|
||||||
|
return nome.length >= 3 && nivelValido && !criandoNovoPerfil;
|
||||||
|
});
|
||||||
|
|
||||||
|
const roleDuplicacaoSelecionada = $derived.by(() => {
|
||||||
|
if (!roleParaDuplicar || !rolesQuery.data) return null;
|
||||||
|
return rolesQuery.data.find((role) => role._id === roleParaDuplicar) ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (roleParaDuplicar) {
|
||||||
|
carregarPermissoesRole(roleParaDuplicar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const resumoPermissoesDuplicacao = $derived.by(() => {
|
||||||
|
if (!roleParaDuplicar) return null;
|
||||||
|
const permissoes = permissoesPorRole[roleParaDuplicar];
|
||||||
|
if (!permissoes) return null;
|
||||||
|
const totalRecursos = permissoes.length;
|
||||||
|
const totalAcoes = permissoes.reduce((acc, item) => acc + item.acoes.length, 0);
|
||||||
|
return { totalRecursos, totalAcoes };
|
||||||
|
});
|
||||||
|
|
||||||
|
function abrirModalNovoPerfil() {
|
||||||
|
nomeNovoPerfil = '';
|
||||||
|
descricaoNovoPerfil = '';
|
||||||
|
setorNovoPerfil = '';
|
||||||
|
nivelNovoPerfil = 3;
|
||||||
|
roleParaDuplicar = '';
|
||||||
|
modalNovoPerfilAberto = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepararEdicaoPerfil(role: RoleRow) {
|
function fecharModalNovoPerfil() {
|
||||||
perfilSendoEditado = role;
|
modalNovoPerfilAberto = false;
|
||||||
nomeNovoPerfil = role.nome;
|
|
||||||
descricaoNovoPerfil = role.descricao;
|
|
||||||
nivelNovoPerfil = role.nivel;
|
|
||||||
modalGerenciarPerfisAberto = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fecharModalGerenciarPerfis() {
|
|
||||||
modalGerenciarPerfisAberto = false;
|
|
||||||
perfilSendoEditado = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function criarNovoPerfil() {
|
async function criarNovoPerfil() {
|
||||||
if (!nomeNovoPerfil.trim()) return;
|
if (!podeSalvarNovoPerfil) return;
|
||||||
|
|
||||||
processando = true;
|
const nome = nomeNovoPerfil.trim();
|
||||||
|
const descricao = descricaoNovoPerfil.trim();
|
||||||
|
const setor = setorNovoPerfil.trim();
|
||||||
|
const nivel = Math.min(Math.max(Math.round(Number(nivelNovoPerfil)), 0), 10);
|
||||||
|
|
||||||
|
criandoNovoPerfil = true;
|
||||||
try {
|
try {
|
||||||
const result = await client.mutation(api.roles.criar, {
|
const resultado = await client.mutation(api.roles.criar, {
|
||||||
nome: nomeNovoPerfil.trim(),
|
nome,
|
||||||
descricao: descricaoNovoPerfil.trim(),
|
descricao,
|
||||||
nivel: nivelNovoPerfil,
|
nivel,
|
||||||
customizado: true,
|
setor: setor.length > 0 ? setor : undefined,
|
||||||
|
copiarDeRoleId: roleParaDuplicar || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.sucesso) {
|
if (resultado.sucesso) {
|
||||||
mostrarMensagem("success", "Perfil criado com sucesso!");
|
mostrarMensagem('success', 'Perfil criado com sucesso!');
|
||||||
nomeNovoPerfil = "";
|
modalNovoPerfilAberto = false;
|
||||||
descricaoNovoPerfil = "";
|
|
||||||
nivelNovoPerfil = 3;
|
|
||||||
fecharModalGerenciarPerfis();
|
|
||||||
if (rolesQuery.refetch) {
|
|
||||||
// Verificação para garantir que refetch existe
|
|
||||||
rolesQuery.refetch(); // Atualiza a lista de perfis
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
mostrarMensagem("error", `Erro ao criar perfil: ${result.erro}`);
|
const mapaErros: Record<string, string> = {
|
||||||
|
nome_ja_utilizado:
|
||||||
|
'Já existe um perfil com este identificador. Ajuste o nome e tente novamente.',
|
||||||
|
nome_invalido: 'Informe um nome válido com pelo menos 3 caracteres.',
|
||||||
|
sem_permissao: 'Você não possui permissão para criar novos perfis.',
|
||||||
|
role_origem_nao_encontrada: 'Não foi possível carregar o perfil escolhido como base.'
|
||||||
|
};
|
||||||
|
const mensagemErro =
|
||||||
|
mapaErros[resultado.erro] ?? 'Não foi possível criar o perfil. Tente novamente.';
|
||||||
|
mostrarMensagem('error', mensagemErro);
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const mensagemErro =
|
||||||
mostrarMensagem("error", `Erro ao criar perfil: ${message}`);
|
error instanceof Error ? error.message : 'Erro inesperado ao criar o perfil.';
|
||||||
|
mostrarMensagem('error', mensagemErro);
|
||||||
} finally {
|
} finally {
|
||||||
processando = false;
|
criandoNovoPerfil = 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>
|
</script>
|
||||||
|
|
||||||
<ProtectedRoute allowedRoles={["ti_master", "admin"]} maxLevel={1}>
|
<ProtectedRoute allowedRoles={['ti_master', 'admin']} maxLevel={1}>
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="text-sm breadcrumbs mb-4">
|
<div class="breadcrumbs mb-4 text-sm">
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a href="/" class="text-primary hover:text-primary-focus">
|
<a href={resolve('/')} class="text-primary hover:text-primary-focus">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-4 w-4"
|
class="h-4 w-4"
|
||||||
@@ -253,7 +234,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/ti" class="text-primary hover:text-primary-focus">TI</a>
|
<a href={resolve('/ti')} class="text-primary hover:text-primary-focus">TI</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="font-semibold">Gerenciar Perfis & Permissões</li>
|
<li class="font-semibold">Gerenciar Perfis & Permissões</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -261,11 +242,11 @@
|
|||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="mb-2 flex flex-wrap items-center gap-3">
|
||||||
<div class="p-3 bg-primary/10 rounded-xl">
|
<div class="bg-primary/10 rounded-xl p-3">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-8 w-8 text-primary"
|
class="text-primary h-8 w-8"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -279,14 +260,14 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h1 class="text-3xl font-bold text-base-content">
|
<h1 class="text-base-content text-3xl font-bold">
|
||||||
Gerenciar Perfis & Permissões de Acesso
|
Gerenciar Perfis & Permissões de Acesso
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-base-content/60 mt-1">
|
<p class="text-base-content/60 mt-1">
|
||||||
Configure as permissões de acesso aos menus do sistema por função
|
Configure as permissões de acesso aos menus do sistema por função
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary gap-2" onclick={abrirModalCriarPerfil}>
|
<button class="btn btn-primary gap-2" onclick={abrirModalNovoPerfil}>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-5 w-5"
|
class="h-5 w-5"
|
||||||
@@ -301,9 +282,9 @@
|
|||||||
d="M12 4v16m8-8H4"
|
d="M12 4v16m8-8H4"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Criar Novo Perfil
|
Criar novo perfil
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-ghost gap-2" onclick={() => goto("/ti")}>
|
<button class="btn btn-ghost gap-2" onclick={() => goto(resolve('/ti'))}>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-5 w-5"
|
class="h-5 w-5"
|
||||||
@@ -327,13 +308,13 @@
|
|||||||
{#if mensagem}
|
{#if mensagem}
|
||||||
<div
|
<div
|
||||||
class="alert mb-6 shadow-lg"
|
class="alert mb-6 shadow-lg"
|
||||||
class:alert-success={mensagem.tipo === "success"}
|
class:alert-success={mensagem.tipo === 'success'}
|
||||||
class:alert-error={mensagem.tipo === "error"}
|
class:alert-error={mensagem.tipo === 'error'}
|
||||||
>
|
>
|
||||||
{#if mensagem.tipo === "success"}
|
{#if mensagem.tipo === 'success'}
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="stroke-current shrink-0 h-6 w-6"
|
class="h-6 w-6 shrink-0 stroke-current"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
@@ -347,7 +328,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="stroke-current shrink-0 h-6 w-6"
|
class="h-6 w-6 shrink-0 stroke-current"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
@@ -364,9 +345,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Filtros e Busca -->
|
<!-- Filtros e Busca -->
|
||||||
<div class="card bg-base-100 shadow-xl mb-6">
|
<div class="card bg-base-100 mb-6 shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<!-- Busca por menu -->
|
<!-- Busca por menu -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="busca">
|
<label class="label" for="busca">
|
||||||
@@ -382,7 +363,7 @@
|
|||||||
/>
|
/>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-5 w-5 absolute right-3 top-3.5 text-base-content/40"
|
class="text-base-content/40 absolute top-3.5 right-3 h-5 w-5"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -402,14 +383,10 @@
|
|||||||
<label class="label" for="filtroRole">
|
<label class="label" for="filtroRole">
|
||||||
<span class="label-text font-semibold">Filtrar por Perfil</span>
|
<span class="label-text font-semibold">Filtrar por Perfil</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select id="filtroRole" class="select select-bordered w-full" bind:value={filtroRole}>
|
||||||
id="filtroRole"
|
|
||||||
class="select select-bordered w-full"
|
|
||||||
bind:value={filtroRole}
|
|
||||||
>
|
|
||||||
<option value="">Todos os perfis</option>
|
<option value="">Todos os perfis</option>
|
||||||
{#if rolesQuery.data}
|
{#if rolesQuery.data}
|
||||||
{#each rolesQuery.data as roleRow}
|
{#each rolesQuery.data as roleRow (roleRow._id)}
|
||||||
<option value={roleRow._id}>
|
<option value={roleRow._id}>
|
||||||
{roleRow.descricao} ({roleRow.nome})
|
{roleRow.descricao} ({roleRow.nome})
|
||||||
</option>
|
</option>
|
||||||
@@ -420,14 +397,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if busca || filtroRole}
|
{#if busca || filtroRole}
|
||||||
<div class="flex items-center gap-2 mt-2">
|
<div class="mt-2 flex items-center gap-2">
|
||||||
<span class="text-sm text-base-content/60">Filtros ativos:</span>
|
<span class="text-base-content/60 text-sm">Filtros ativos:</span>
|
||||||
{#if busca}
|
{#if busca}
|
||||||
<div class="badge badge-primary gap-2">
|
<div class="badge badge-primary gap-2">
|
||||||
Busca: {busca}
|
Busca: {busca}
|
||||||
<button
|
<button
|
||||||
class="btn btn-ghost btn-xs"
|
class="btn btn-ghost btn-xs"
|
||||||
onclick={() => (busca = "")}
|
onclick={() => (busca = '')}
|
||||||
aria-label="Limpar busca"
|
aria-label="Limpar busca"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
@@ -439,7 +416,7 @@
|
|||||||
Perfil filtrado
|
Perfil filtrado
|
||||||
<button
|
<button
|
||||||
class="btn btn-ghost btn-xs"
|
class="btn btn-ghost btn-xs"
|
||||||
onclick={() => (filtroRole = "")}
|
onclick={() => (filtroRole = '')}
|
||||||
aria-label="Limpar filtro"
|
aria-label="Limpar filtro"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
@@ -451,60 +428,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Informações sobre o sistema de permissões -->
|
|
||||||
<div class="alert alert-info mb-6 shadow-lg">
|
|
||||||
<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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-lg">Como funciona o sistema de permissões:</h3>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
|
|
||||||
<div>
|
|
||||||
<h4 class="font-semibold text-sm">Tipos de Permissão:</h4>
|
|
||||||
<ul class="text-sm mt-1 space-y-1">
|
|
||||||
<li>
|
|
||||||
• <strong>Acessar:</strong> Visualizar menu e acessar página
|
|
||||||
</li>
|
|
||||||
<li>• <strong>Consultar:</strong> Ver dados (requer "Acessar")</li>
|
|
||||||
<li>
|
|
||||||
• <strong>Gravar:</strong> Criar/editar/excluir (requer "Consultar")
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="font-semibold text-sm">Perfis Especiais:</h4>
|
|
||||||
<ul class="text-sm mt-1 space-y-1">
|
|
||||||
<li>• <strong>Admin e TI:</strong> Acesso total automático</li>
|
|
||||||
<li>• <strong>Dashboard:</strong> Público para todos</li>
|
|
||||||
<li>
|
|
||||||
• <strong>Perfil Customizado:</strong> Permissões personalizadas
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Matriz de Permissões por Ação -->
|
<!-- Matriz de Permissões por Ação -->
|
||||||
{#if rolesQuery.isLoading || catalogoQuery.isLoading}
|
{#if rolesQuery.isLoading || catalogoQuery.isLoading}
|
||||||
<div class="flex justify-center items-center py-12">
|
<div class="flex items-center justify-center py-12">
|
||||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
</div>
|
</div>
|
||||||
{:else if rolesQuery.error}
|
{:else if rolesQuery.error}
|
||||||
<div class="alert alert-error">
|
<div class="alert alert-error">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="stroke-current shrink-0 h-6 w-6"
|
class="h-6 w-6 shrink-0 stroke-current"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
@@ -523,7 +456,7 @@
|
|||||||
<div class="card-body items-center text-center">
|
<div class="card-body items-center text-center">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-16 w-16 text-base-content/30"
|
class="text-base-content/30 h-16 w-16"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -535,17 +468,17 @@
|
|||||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<h3 class="text-xl font-bold mt-4">Nenhum resultado encontrado</h3>
|
<h3 class="mt-4 text-xl font-bold">Nenhum resultado encontrado</h3>
|
||||||
<p class="text-base-content/60">
|
<p class="text-base-content/60">
|
||||||
{busca
|
{busca
|
||||||
? `Não foram encontrados perfis com "${busca}"`
|
? `Não foram encontrados perfis com "${busca}"`
|
||||||
: "Nenhum perfil corresponde aos filtros aplicados"}
|
: 'Nenhum perfil corresponde aos filtros aplicados'}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary btn-sm mt-4"
|
class="btn btn-primary btn-sm mt-4"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
busca = "";
|
busca = '';
|
||||||
filtroRole = "";
|
filtroRole = '';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Limpar Filtros
|
Limpar Filtros
|
||||||
@@ -554,13 +487,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#each rolesFiltradas as roleRow}
|
{#each rolesFiltradas as roleRow (roleRow._id)}
|
||||||
{@const roleId = roleRow._id}
|
{@const roleId = roleRow._id}
|
||||||
<div class="card bg-base-100 shadow-xl mb-6">
|
<div class="card bg-base-100 mb-6 shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="flex items-center justify-between mb-4 flex-wrap gap-4">
|
<div class="mb-4 flex flex-wrap items-center gap-4">
|
||||||
<div class="flex-1 min-w-[200px]">
|
<div class="min-w-[200px] flex-1">
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="mb-2 flex items-center gap-3">
|
||||||
<h2 class="card-title text-2xl">{roleRow.descricao}</h2>
|
<h2 class="card-title text-2xl">{roleRow.descricao}</h2>
|
||||||
<div class="badge badge-lg badge-primary">
|
<div class="badge badge-lg badge-primary">
|
||||||
Nível {roleRow.nivel}
|
Nível {roleRow.nivel}
|
||||||
@@ -585,28 +518,17 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-base-content/60">
|
<p class="text-base-content/60 text-sm">
|
||||||
<span class="font-mono bg-base-200 px-2 py-1 rounded"
|
<span class="bg-base-200 rounded px-2 py-1 font-mono">{roleRow.nome}</span>
|
||||||
>{roleRow.nome}</span
|
|
||||||
>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline btn-sm"
|
|
||||||
onclick={() => prepararEdicaoPerfil(roleRow)}
|
|
||||||
>
|
|
||||||
Editar
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if roleRow.nivel <= 1}
|
{#if roleRow.nivel <= 1}
|
||||||
<div class="alert alert-success shadow-md">
|
<div class="alert alert-success shadow-md">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="stroke-current shrink-0 h-6 w-6"
|
class="h-6 w-6 shrink-0 stroke-current"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
@@ -620,27 +542,24 @@
|
|||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold">Perfil Administrativo</h3>
|
<h3 class="font-bold">Perfil Administrativo</h3>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
Este perfil possui acesso total ao sistema automaticamente,
|
Este perfil possui acesso total ao sistema automaticamente, sem necessidade de
|
||||||
sem necessidade de configuração manual.
|
configuração manual.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if catalogoQuery.data}
|
{:else if catalogoQuery.data}
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{#each catalogoQuery.data as item}
|
{#each catalogoQuery.data as item (item.recurso)}
|
||||||
{@const recursoExpandido = isRecursoExpandido(
|
{@const recursoExpandido = isRecursoExpandido(roleId, item.recurso)}
|
||||||
roleId,
|
<div class="border-base-300 overflow-hidden rounded-lg border">
|
||||||
item.recurso,
|
|
||||||
)}
|
|
||||||
<div class="border border-base-300 rounded-lg overflow-hidden">
|
|
||||||
<!-- Cabeçalho do recurso (clicável) -->
|
<!-- Cabeçalho do recurso (clicável) -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-full px-4 py-3 bg-base-200 hover:bg-base-300 transition-colors flex items-center justify-between"
|
class="bg-base-200 hover:bg-base-300 flex w-full items-center justify-between px-4 py-3 transition-colors"
|
||||||
onclick={() => toggleRecurso(roleId, item.recurso)}
|
onclick={() => toggleRecurso(roleId, item.recurso)}
|
||||||
disabled={salvando}
|
disabled={salvando}
|
||||||
>
|
>
|
||||||
<span class="font-semibold text-lg">{item.recurso}</span>
|
<span class="text-lg font-semibold">{item.recurso}</span>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-5 w-5 transition-transform"
|
class="h-5 w-5 transition-transform"
|
||||||
@@ -660,11 +579,11 @@
|
|||||||
|
|
||||||
<!-- Lista de ações (visível quando expandido) -->
|
<!-- Lista de ações (visível quando expandido) -->
|
||||||
{#if recursoExpandido}
|
{#if recursoExpandido}
|
||||||
<div class="px-4 py-3 bg-base-100 border-t border-base-300">
|
<div class="bg-base-100 border-base-300 border-t px-4 py-3">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{#each ["ver", "listar", "criar", "editar", "excluir"] as acao}
|
{#each ['ver', 'listar', 'criar', 'editar', 'excluir'] as acao (acao)}
|
||||||
<label
|
<label
|
||||||
class="flex items-center gap-3 cursor-pointer hover:bg-base-200 p-2 rounded transition-colors"
|
class="hover:bg-base-200 flex cursor-pointer items-center gap-3 rounded p-2 transition-colors"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -672,16 +591,9 @@
|
|||||||
checked={isConcedida(roleId, item.recurso, acao)}
|
checked={isConcedida(roleId, item.recurso, acao)}
|
||||||
disabled={salvando}
|
disabled={salvando}
|
||||||
onchange={(e) =>
|
onchange={(e) =>
|
||||||
toggleAcao(
|
toggleAcao(roleId, item.recurso, acao, e.currentTarget.checked)}
|
||||||
roleId,
|
|
||||||
item.recurso,
|
|
||||||
acao,
|
|
||||||
e.currentTarget.checked,
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<span class="flex-1 capitalize font-medium"
|
<span class="flex-1 font-medium capitalize">{acao}</span>
|
||||||
>{acao}</span
|
|
||||||
>
|
|
||||||
</label>
|
</label>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -696,140 +608,176 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Modal Gerenciar Perfis -->
|
{#if modalNovoPerfilAberto}
|
||||||
{#if modalGerenciarPerfisAberto}
|
|
||||||
<dialog class="modal modal-open">
|
<dialog class="modal modal-open">
|
||||||
<div
|
<div
|
||||||
class="modal-box max-w-4xl w-full overflow-hidden border border-base-200/60 bg-base-200/40 p-0 shadow-2xl"
|
class="modal-box bg-base-100 w-full max-w-4xl overflow-hidden rounded-2xl p-0 shadow-2xl"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="relative bg-linear-to-r from-primary via-primary/90 to-secondary/80 px-8 py-6 text-base-100"
|
class="from-primary via-primary/85 to-secondary/80 text-base-100 relative bg-linear-to-r px-8 py-6"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-circle btn-ghost absolute right-4 top-4 text-base-100/80 hover:text-base-100"
|
class="btn btn-circle btn-ghost btn-sm text-base-100/80 hover:text-base-100 absolute top-4 right-4"
|
||||||
onclick={fecharModalGerenciarPerfis}
|
onclick={fecharModalNovoPerfil}
|
||||||
aria-label="Fechar modal"
|
aria-label="Fechar"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
|
<div class="space-y-2 pr-10">
|
||||||
<h3 class="text-3xl font-black tracking-tight">
|
<h3 class="text-3xl font-black tracking-tight">Criar novo perfil de acesso</h3>
|
||||||
Gerenciar Perfis de Acesso
|
<p class="text-base-100/80 max-w-2xl text-sm md:text-base">
|
||||||
</h3>
|
Defina as informações do perfil e, se desejar, reutilize as permissões de um perfil
|
||||||
<p class="mt-2 max-w-2xl text-sm text-base-100/80 md:text-base">
|
existente para acelerar a configuração.
|
||||||
{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>
|
</p>
|
||||||
</div>
|
{#if identificadorSugerido}
|
||||||
|
<div class="text-base-100/80 flex flex-wrap items-center gap-2 text-xs">
|
||||||
<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
|
<span
|
||||||
class="badge badge-outline badge-primary badge-lg self-start flex flex-col items-center gap-1 px-5 py-3 text-center"
|
class="badge badge-outline badge-sm border-base-100/40 tracking-widest uppercase"
|
||||||
>
|
>
|
||||||
<span
|
Identificador
|
||||||
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>
|
</span>
|
||||||
|
<span class="font-mono text-sm">{identificadorSugerido}</span>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-6 px-8 py-6">
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
<div class="form-control md:col-span-2">
|
<div class="form-control md:col-span-2">
|
||||||
<label class="label" for="nome-perfil-input">
|
<label class="label" for="nome-novo-perfil">
|
||||||
<span class="label-text">Nome do Perfil *</span>
|
<span class="label-text font-semibold">Nome do perfil *</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="nome-perfil-input"
|
id="nome-novo-perfil"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={nomeNovoPerfil}
|
|
||||||
class="input input-bordered input-primary"
|
class="input input-bordered input-primary"
|
||||||
placeholder="Ex: RH, Financeiro, Gestor"
|
placeholder="Ex.: Financeiro, RH, Supervisão de Campo"
|
||||||
disabled={perfilSendoEditado !== null &&
|
bind:value={nomeNovoPerfil}
|
||||||
!perfilSendoEditado.customizado}
|
maxlength={60}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control md:col-span-2">
|
<div class="form-control md:col-span-2">
|
||||||
<label class="label" for="descricao-perfil-input">
|
<label class="label" for="descricao-novo-perfil">
|
||||||
<span class="label-text">Descrição</span>
|
<span class="label-text font-semibold">Descrição</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="descricao-perfil-input"
|
id="descricao-novo-perfil"
|
||||||
|
class="textarea textarea-bordered textarea-primary min-h-[96px]"
|
||||||
|
placeholder="Explique como este perfil será utilizado e quais áreas ele atende."
|
||||||
bind:value={descricaoNovoPerfil}
|
bind:value={descricaoNovoPerfil}
|
||||||
class="textarea textarea-bordered textarea-primary min-h-[120px]"
|
maxlength={240}
|
||||||
placeholder="Breve descrição das responsabilidades e limites de atuação deste perfil."
|
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control max-w-xs">
|
<div class="form-control">
|
||||||
<label class="label" for="nivel-perfil-input">
|
<label class="label" for="setor-novo-perfil">
|
||||||
<span class="label-text">Nível de Acesso (0-5) *</span>
|
<span class="label-text font-semibold">Setor (opcional)</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="nivel-perfil-input"
|
id="setor-novo-perfil"
|
||||||
type="number"
|
type="text"
|
||||||
bind:value={nivelNovoPerfil}
|
class="input input-bordered"
|
||||||
min="0"
|
placeholder="Informe o setor ou departamento responsável"
|
||||||
max="5"
|
bind:value={setorNovoPerfil}
|
||||||
class="input input-bordered input-secondary"
|
maxlength={40}
|
||||||
disabled={perfilSendoEditado !== null &&
|
|
||||||
!perfilSendoEditado.customizado}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div
|
<div class="form-control">
|
||||||
class="mt-8 flex flex-col-reverse gap-3 sm:flex-row sm:items-center sm:justify-between"
|
<label class="label" for="nivel-novo-perfil">
|
||||||
|
<span class="label-text font-semibold">Nível de acesso (0 a 10)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="nivel-novo-perfil"
|
||||||
|
type="number"
|
||||||
|
class="input input-bordered"
|
||||||
|
min={0}
|
||||||
|
max={10}
|
||||||
|
step={1}
|
||||||
|
bind:value={nivelNovoPerfil}
|
||||||
|
/>
|
||||||
|
<span class="text-base-content/60 mt-1 text-xs">
|
||||||
|
Níveis menores representam maior privilégio (ex.: 0 = administrativo).
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control md:col-span-2">
|
||||||
|
<label class="label" for="duplicar-novo-perfil">
|
||||||
|
<span class="label-text font-semibold">Duplicar permissões de</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="duplicar-novo-perfil"
|
||||||
|
class="select select-bordered"
|
||||||
|
bind:value={roleParaDuplicar}
|
||||||
>
|
>
|
||||||
<p class="text-sm text-base-content/60">
|
<option value="">Iniciar perfil vazio</option>
|
||||||
Campos marcados com * são obrigatórios.
|
{#if rolesQuery.data}
|
||||||
|
{#each rolesQuery.data as roleDuplicavel (roleDuplicavel._id)}
|
||||||
|
<option value={roleDuplicavel._id}>
|
||||||
|
{roleDuplicavel.descricao} — nível {roleDuplicavel.nivel}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if roleDuplicacaoSelecionada}
|
||||||
|
<div class="border-base-200 bg-base-100/80 rounded-2xl border p-4">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<p class="text-base-content font-semibold">
|
||||||
|
Permissões a serem copiadas de <span class="font-bold"
|
||||||
|
>{roleDuplicacaoSelecionada.descricao}</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p class="text-base-content/70 text-sm">
|
||||||
|
As permissões atuais serão reaproveitadas automaticamente para o novo perfil.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{#if resumoPermissoesDuplicacao}
|
||||||
|
<div class="flex items-center gap-4 text-sm">
|
||||||
|
<span class="badge badge-outline badge-sm">
|
||||||
|
{resumoPermissoesDuplicacao.totalRecursos} recursos
|
||||||
|
</span>
|
||||||
|
<span class="badge badge-outline badge-sm">
|
||||||
|
{resumoPermissoesDuplicacao.totalAcoes} ações
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<span class="loading loading-dots loading-sm text-primary"></span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex flex-col-reverse gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<p class="text-base-content/60 text-xs">
|
||||||
|
Campos marcados com * são obrigatórios. As alterações entram em vigor imediatamente
|
||||||
|
após a criação.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-wrap gap-3">
|
<div class="flex flex-wrap gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost"
|
class="btn btn-ghost"
|
||||||
onclick={fecharModalGerenciarPerfis}
|
onclick={fecharModalNovoPerfil}
|
||||||
disabled={processando}
|
disabled={criandoNovoPerfil}
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
disabled={!nomeNovoPerfil.trim() || processando}
|
disabled={!podeSalvarNovoPerfil}
|
||||||
onclick={perfilSendoEditado ? editarPerfil : criarNovoPerfil}
|
onclick={criarNovoPerfil}
|
||||||
>
|
>
|
||||||
{#if processando}
|
{#if criandoNovoPerfil}
|
||||||
<span class="loading loading-spinner"></span>
|
<span class="loading loading-spinner"></span>
|
||||||
Processando...
|
Criando perfil...
|
||||||
{:else}
|
{:else}
|
||||||
{perfilSendoEditado ? "Salvar Alterações" : "Criar Perfil"}
|
Salvar perfil
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -837,11 +785,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="dialog" class="modal-backdrop">
|
<form method="dialog" class="modal-backdrop">
|
||||||
<button
|
<button type="button" onclick={fecharModalNovoPerfil} aria-label="Fechar modal">
|
||||||
type="button"
|
fechar
|
||||||
onclick={fecharModalGerenciarPerfis}
|
</button>
|
||||||
aria-label="Fechar modal">Fechar</button
|
|
||||||
>
|
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useConvexClient, useQuery } from "convex-svelte";
|
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from '$app/navigation';
|
||||||
import type { Id, Doc } from "@sgse-app/backend/convex/_generated/dataModel";
|
import { resolve } from '$app/paths';
|
||||||
|
import type { Id, Doc } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
|
|
||||||
// Tipos baseados nos retornos das queries do backend
|
// Tipos baseados nos retornos das queries do backend
|
||||||
type Usuario = {
|
type Usuario = {
|
||||||
_id: Id<"usuarios">;
|
_id: Id<'usuarios'>;
|
||||||
matricula: string;
|
matricula: string;
|
||||||
nome: string;
|
nome: string;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -18,9 +19,9 @@
|
|||||||
ultimoAcesso?: number;
|
ultimoAcesso?: number;
|
||||||
criadoEm: number;
|
criadoEm: number;
|
||||||
role: {
|
role: {
|
||||||
_id: Id<"roles">;
|
_id: Id<'roles'>;
|
||||||
_creationTime?: number;
|
_creationTime?: number;
|
||||||
criadoPor?: Id<"usuarios">;
|
criadoPor?: Id<'usuarios'>;
|
||||||
customizado?: boolean;
|
customizado?: boolean;
|
||||||
descricao: string;
|
descricao: string;
|
||||||
editavel?: boolean;
|
editavel?: boolean;
|
||||||
@@ -30,20 +31,20 @@
|
|||||||
erro?: boolean;
|
erro?: boolean;
|
||||||
};
|
};
|
||||||
funcionario?: {
|
funcionario?: {
|
||||||
_id: Id<"funcionarios">;
|
_id: Id<'funcionarios'>;
|
||||||
nome: string;
|
nome: string;
|
||||||
matricula?: string;
|
matricula?: string;
|
||||||
descricaoCargo?: string;
|
descricaoCargo?: string;
|
||||||
simboloTipo: "cargo_comissionado" | "funcao_gratificada";
|
simboloTipo: 'cargo_comissionado' | 'funcao_gratificada';
|
||||||
};
|
};
|
||||||
avisos?: Array<{
|
avisos?: Array<{
|
||||||
tipo: "erro" | "aviso" | "info";
|
tipo: 'erro' | 'aviso' | 'info';
|
||||||
mensagem: string;
|
mensagem: string;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Funcionario = {
|
type Funcionario = {
|
||||||
_id: Id<"funcionarios">;
|
_id: Id<'funcionarios'>;
|
||||||
nome: string;
|
nome: string;
|
||||||
matricula?: string;
|
matricula?: string;
|
||||||
cpf?: string;
|
cpf?: string;
|
||||||
@@ -55,25 +56,25 @@
|
|||||||
cep?: string;
|
cep?: string;
|
||||||
cidade?: string;
|
cidade?: string;
|
||||||
uf?: string;
|
uf?: string;
|
||||||
simboloId: Id<"simbolos">;
|
simboloId: Id<'simbolos'>;
|
||||||
simboloTipo: "cargo_comissionado" | "funcao_gratificada";
|
simboloTipo: 'cargo_comissionado' | 'funcao_gratificada';
|
||||||
admissaoData?: string;
|
admissaoData?: string;
|
||||||
desligamentoData?: string;
|
desligamentoData?: string;
|
||||||
descricaoCargo?: string;
|
descricaoCargo?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Gestor = Doc<"usuarios"> | null;
|
type Gestor = Doc<'usuarios'> | null;
|
||||||
|
|
||||||
type TimeComDetalhes = Doc<"times"> & {
|
type TimeComDetalhes = Doc<'times'> & {
|
||||||
gestor: Gestor;
|
gestor: Gestor;
|
||||||
totalMembros: number;
|
totalMembros: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MembroTime = Doc<"timesMembros"> & {
|
type MembroTime = Doc<'timesMembros'> & {
|
||||||
funcionario: Doc<"funcionarios"> | null;
|
funcionario: Doc<'funcionarios'> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TimeComMembros = Doc<"times"> & {
|
type TimeComMembros = Doc<'times'> & {
|
||||||
gestor: Gestor;
|
gestor: Gestor;
|
||||||
membros: MembroTime[];
|
membros: MembroTime[];
|
||||||
};
|
};
|
||||||
@@ -87,14 +88,10 @@
|
|||||||
|
|
||||||
const times = $derived((timesQuery?.data || []) as TimeComDetalhes[]);
|
const times = $derived((timesQuery?.data || []) as TimeComDetalhes[]);
|
||||||
const usuarios = $derived((usuariosQuery?.data || []) as Usuario[]);
|
const usuarios = $derived((usuariosQuery?.data || []) as Usuario[]);
|
||||||
const funcionarios = $derived(
|
const funcionarios = $derived((funcionariosQuery?.data || []) as Funcionario[]);
|
||||||
(funcionariosQuery?.data || []) as Funcionario[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const carregando = $derived(
|
const carregando = $derived(
|
||||||
timesQuery === undefined ||
|
timesQuery === undefined || usuariosQuery === undefined || funcionariosQuery === undefined
|
||||||
usuariosQuery === undefined ||
|
|
||||||
funcionariosQuery === undefined,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Estados
|
// Estados
|
||||||
@@ -107,65 +104,64 @@
|
|||||||
let processando = $state(false);
|
let processando = $state(false);
|
||||||
|
|
||||||
// Form
|
// Form
|
||||||
let formNome = $state("");
|
let formNome = $state('');
|
||||||
let formDescricao = $state("");
|
let formDescricao = $state('');
|
||||||
let formGestorId = $state("");
|
let formGestorId = $state('');
|
||||||
let formCor = $state("#3B82F6");
|
let formCor = $state('#3B82F6');
|
||||||
|
|
||||||
// Membros
|
// Membros
|
||||||
let membrosDisponiveis = $derived(
|
let membrosDisponiveis = $derived(
|
||||||
funcionarios.filter((f: Funcionario) => {
|
funcionarios.filter((f: Funcionario) => {
|
||||||
// Verificar se o funcionário já está em algum time ativo
|
// Verificar se o funcionário já está em algum time ativo
|
||||||
const jaNaEquipe = timeParaMembros?.membros?.some(
|
const jaNaEquipe = timeParaMembros?.membros?.some(
|
||||||
(m: MembroTime) => m.funcionario?._id === f._id,
|
(m: MembroTime) => m.funcionario?._id === f._id
|
||||||
);
|
);
|
||||||
return !jaNaEquipe;
|
return !jaNaEquipe;
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cores predefinidas
|
// Cores predefinidas
|
||||||
const coresDisponiveis = [
|
const coresDisponiveis = [
|
||||||
"#3B82F6", // Blue
|
'#3B82F6', // Blue
|
||||||
"#10B981", // Green
|
'#10B981', // Green
|
||||||
"#F59E0B", // Yellow
|
'#F59E0B', // Yellow
|
||||||
"#EF4444", // Red
|
'#EF4444', // Red
|
||||||
"#8B5CF6", // Purple
|
'#8B5CF6', // Purple
|
||||||
"#EC4899", // Pink
|
'#EC4899', // Pink
|
||||||
"#14B8A6", // Teal
|
'#14B8A6', // Teal
|
||||||
"#F97316", // Orange
|
'#F97316' // Orange
|
||||||
];
|
];
|
||||||
|
|
||||||
function novoTime() {
|
function novoTime() {
|
||||||
modoEdicao = true;
|
modoEdicao = true;
|
||||||
timeEmEdicao = null;
|
timeEmEdicao = null;
|
||||||
formNome = "";
|
formNome = '';
|
||||||
formDescricao = "";
|
formDescricao = '';
|
||||||
formGestorId = "";
|
formGestorId = '';
|
||||||
formCor =
|
formCor = coresDisponiveis[Math.floor(Math.random() * coresDisponiveis.length)];
|
||||||
coresDisponiveis[Math.floor(Math.random() * coresDisponiveis.length)];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function editarTime(time: TimeComDetalhes) {
|
function editarTime(time: TimeComDetalhes) {
|
||||||
modoEdicao = true;
|
modoEdicao = true;
|
||||||
timeEmEdicao = time;
|
timeEmEdicao = time;
|
||||||
formNome = time.nome;
|
formNome = time.nome;
|
||||||
formDescricao = time.descricao || "";
|
formDescricao = time.descricao || '';
|
||||||
formGestorId = time.gestorId;
|
formGestorId = time.gestorId;
|
||||||
formCor = time.cor || "#3B82F6";
|
formCor = time.cor || '#3B82F6';
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelarEdicao() {
|
function cancelarEdicao() {
|
||||||
modoEdicao = false;
|
modoEdicao = false;
|
||||||
timeEmEdicao = null;
|
timeEmEdicao = null;
|
||||||
formNome = "";
|
formNome = '';
|
||||||
formDescricao = "";
|
formDescricao = '';
|
||||||
formGestorId = "";
|
formGestorId = '';
|
||||||
formCor = "#3B82F6";
|
formCor = '#3B82F6';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function salvarTime() {
|
async function salvarTime() {
|
||||||
if (!formNome.trim() || !formGestorId) {
|
if (!formNome.trim() || !formGestorId) {
|
||||||
alert("Preencha todos os campos obrigatórios!");
|
alert('Preencha todos os campos obrigatórios!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,21 +172,21 @@
|
|||||||
id: timeEmEdicao._id,
|
id: timeEmEdicao._id,
|
||||||
nome: formNome,
|
nome: formNome,
|
||||||
descricao: formDescricao || undefined,
|
descricao: formDescricao || undefined,
|
||||||
gestorId: formGestorId as Id<"usuarios">,
|
gestorId: formGestorId as Id<'usuarios'>,
|
||||||
cor: formCor,
|
cor: formCor
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await client.mutation(api.times.criar, {
|
await client.mutation(api.times.criar, {
|
||||||
nome: formNome,
|
nome: formNome,
|
||||||
descricao: formDescricao || undefined,
|
descricao: formDescricao || undefined,
|
||||||
gestorId: formGestorId as Id<"usuarios">,
|
gestorId: formGestorId as Id<'usuarios'>,
|
||||||
cor: formCor,
|
cor: formCor
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
cancelarEdicao();
|
cancelarEdicao();
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const errorMessage = e instanceof Error ? e.message : String(e);
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||||
alert("Erro ao salvar: " + errorMessage);
|
alert('Erro ao salvar: ' + errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
processando = false;
|
processando = false;
|
||||||
}
|
}
|
||||||
@@ -211,7 +207,7 @@
|
|||||||
timeParaExcluir = null;
|
timeParaExcluir = null;
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const errorMessage = e instanceof Error ? e.message : String(e);
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||||
alert("Erro ao excluir: " + errorMessage);
|
alert('Erro ao excluir: ' + errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
processando = false;
|
processando = false;
|
||||||
}
|
}
|
||||||
@@ -232,37 +228,37 @@
|
|||||||
try {
|
try {
|
||||||
await client.mutation(api.times.adicionarMembro, {
|
await client.mutation(api.times.adicionarMembro, {
|
||||||
timeId: timeParaMembros._id,
|
timeId: timeParaMembros._id,
|
||||||
funcionarioId: funcionarioId as Id<"funcionarios">,
|
funcionarioId: funcionarioId as Id<'funcionarios'>
|
||||||
});
|
});
|
||||||
|
|
||||||
// Recarregar detalhes do time
|
// Recarregar detalhes do time
|
||||||
const detalhes = await client.query(api.times.obterPorId, {
|
const detalhes = await client.query(api.times.obterPorId, {
|
||||||
id: timeParaMembros._id,
|
id: timeParaMembros._id
|
||||||
});
|
});
|
||||||
if (detalhes) {
|
if (detalhes) {
|
||||||
timeParaMembros = detalhes as TimeComMembros;
|
timeParaMembros = detalhes as TimeComMembros;
|
||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const errorMessage = e instanceof Error ? e.message : String(e);
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||||
alert("Erro: " + errorMessage);
|
alert('Erro: ' + errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
processando = false;
|
processando = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removerMembro(membroId: string) {
|
async function removerMembro(membroId: string) {
|
||||||
if (!confirm("Deseja realmente remover este membro do time?")) return;
|
if (!confirm('Deseja realmente remover este membro do time?')) return;
|
||||||
|
|
||||||
processando = true;
|
processando = true;
|
||||||
try {
|
try {
|
||||||
await client.mutation(api.times.removerMembro, {
|
await client.mutation(api.times.removerMembro, {
|
||||||
membroId: membroId as Id<"timesMembros">,
|
membroId: membroId as Id<'timesMembros'>
|
||||||
});
|
});
|
||||||
|
|
||||||
// Recarregar detalhes do time
|
// Recarregar detalhes do time
|
||||||
if (timeParaMembros) {
|
if (timeParaMembros) {
|
||||||
const detalhes = await client.query(api.times.obterPorId, {
|
const detalhes = await client.query(api.times.obterPorId, {
|
||||||
id: timeParaMembros._id,
|
id: timeParaMembros._id
|
||||||
});
|
});
|
||||||
if (detalhes) {
|
if (detalhes) {
|
||||||
timeParaMembros = detalhes as TimeComMembros;
|
timeParaMembros = detalhes as TimeComMembros;
|
||||||
@@ -270,7 +266,7 @@
|
|||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const errorMessage = e instanceof Error ? e.message : String(e);
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||||
alert("Erro: " + errorMessage);
|
alert('Erro: ' + errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
processando = false;
|
processando = false;
|
||||||
}
|
}
|
||||||
@@ -282,18 +278,13 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ProtectedRoute
|
<ProtectedRoute allowedRoles={['ti_master', 'admin', 'ti_usuario']} maxLevel={2}>
|
||||||
allowedRoles={["ti_master", "admin", "ti_usuario"]}
|
<main class="container mx-auto max-w-7xl px-4 py-6">
|
||||||
maxLevel={2}
|
|
||||||
>
|
|
||||||
<main class="container mx-auto px-4 py-6 max-w-7xl">
|
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="text-sm breadcrumbs mb-4">
|
<div class="breadcrumbs mb-4 text-sm">
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a href="/ti" class="text-primary hover:underline"
|
<a href={resolve('/ti')} class="text-primary hover:underline">Tecnologia da Informação</a>
|
||||||
>Tecnologia da Informação</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>Gestão de Times</li>
|
<li>Gestão de Times</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -303,10 +294,10 @@
|
|||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div class="p-3 bg-secondary/10 rounded-xl">
|
<div class="bg-secondary/10 rounded-xl p-3">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-8 w-8 text-secondary"
|
class="text-secondary h-8 w-8"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -320,16 +311,14 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-bold text-base-content">
|
<h1 class="text-base-content text-3xl font-bold">Gestão de Times</h1>
|
||||||
Gestão de Times
|
|
||||||
</h1>
|
|
||||||
<p class="text-base-content/60 mt-1">
|
<p class="text-base-content/60 mt-1">
|
||||||
Organize funcionários em equipes e defina gestores
|
Organize funcionários em equipes e defina gestores
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button class="btn btn-ghost gap-2" onclick={() => goto("/ti")}>
|
<button class="btn btn-ghost gap-2" onclick={() => goto(resolve('/ti'))}>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-5 w-5"
|
class="h-5 w-5"
|
||||||
@@ -370,14 +359,14 @@
|
|||||||
<!-- Formulário de Edição -->
|
<!-- Formulário de Edição -->
|
||||||
{#if modoEdicao}
|
{#if modoEdicao}
|
||||||
<div
|
<div
|
||||||
class="card bg-linear-to-br from-primary/10 to-primary/5 shadow-xl mb-6 border-2 border-primary/20"
|
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">
|
<div class="card-body">
|
||||||
<h2 class="card-title text-xl mb-4">
|
<h2 class="card-title mb-4 text-xl">
|
||||||
{timeEmEdicao ? "Editar Time" : "Novo Time"}
|
{timeEmEdicao ? 'Editar Time' : 'Novo Time'}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="nome">
|
<label class="label" for="nome">
|
||||||
<span class="label-text font-semibold">Nome do Time *</span>
|
<span class="label-text font-semibold">Nome do Time *</span>
|
||||||
@@ -395,13 +384,9 @@
|
|||||||
<label class="label" for="gestor">
|
<label class="label" for="gestor">
|
||||||
<span class="label-text font-semibold">Gestor *</span>
|
<span class="label-text font-semibold">Gestor *</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select id="gestor" class="select select-bordered" bind:value={formGestorId}>
|
||||||
id="gestor"
|
|
||||||
class="select select-bordered"
|
|
||||||
bind:value={formGestorId}
|
|
||||||
>
|
|
||||||
<option value="">Selecione um gestor</option>
|
<option value="">Selecione um gestor</option>
|
||||||
{#each usuarios as usuario}
|
{#each usuarios as usuario (usuario._id)}
|
||||||
<option value={usuario._id}>{usuario.nome}</option>
|
<option value={usuario._id}>{usuario.nome}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
@@ -424,15 +409,12 @@
|
|||||||
<label class="label" for="cor">
|
<label class="label" for="cor">
|
||||||
<span class="label-text font-semibold">Cor do Time</span>
|
<span class="label-text font-semibold">Cor do Time</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="flex gap-2 flex-wrap">
|
<div class="flex flex-wrap gap-2">
|
||||||
{#each coresDisponiveis as cor}
|
{#each coresDisponiveis as cor (cor)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-10 h-10 rounded-lg border-2 transition-all hover:scale-110"
|
class="h-10 w-10 rounded-lg border-2 transition-all hover:scale-110"
|
||||||
style="background-color: {cor}; border-color: {formCor ===
|
style="background-color: {cor}; border-color: {formCor === cor ? '#000' : cor}"
|
||||||
cor
|
|
||||||
? '#000'
|
|
||||||
: cor}"
|
|
||||||
onclick={() => (formCor = cor)}
|
onclick={() => (formCor = cor)}
|
||||||
aria-label="Selecionar cor"
|
aria-label="Selecionar cor"
|
||||||
></button>
|
></button>
|
||||||
@@ -441,20 +423,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions justify-end mt-6">
|
<div class="card-actions mt-6 justify-end">
|
||||||
<button
|
<button class="btn btn-ghost" onclick={cancelarEdicao} disabled={processando}>
|
||||||
class="btn btn-ghost"
|
|
||||||
onclick={cancelarEdicao}
|
|
||||||
disabled={processando}
|
|
||||||
>
|
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="btn btn-primary" onclick={salvarTime} disabled={processando}>
|
||||||
class="btn btn-primary"
|
{processando ? 'Salvando...' : 'Salvar'}
|
||||||
onclick={salvarTime}
|
|
||||||
disabled={processando}
|
|
||||||
>
|
|
||||||
{processando ? "Salvando..." : "Salvar"}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -463,21 +437,21 @@
|
|||||||
|
|
||||||
<!-- Lista de Times -->
|
<!-- Lista de Times -->
|
||||||
{#if carregando}
|
{#if carregando}
|
||||||
<div class="flex justify-center items-center py-20">
|
<div class="flex items-center justify-center py-20">
|
||||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<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}
|
{#each times.filter((t: TimeComDetalhes) => t.ativo) as time (time._id)}
|
||||||
<div
|
<div
|
||||||
class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all border-l-4"
|
class="card bg-base-100 border-l-4 shadow-xl transition-all hover:shadow-2xl"
|
||||||
style="border-color: {time.cor || '#3B82F6'}"
|
style="border-color: {time.cor || '#3B82F6'}"
|
||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="flex items-start justify-between mb-2">
|
<div class="mb-2 flex items-start justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
class="w-3 h-3 rounded-full"
|
class="h-3 w-3 rounded-full"
|
||||||
style="background-color: {time.cor || '#3B82F6'}"
|
style="background-color: {time.cor || '#3B82F6'}"
|
||||||
></div>
|
></div>
|
||||||
<h2 class="card-title text-lg">{time.nome}</h2>
|
<h2 class="card-title text-lg">{time.nome}</h2>
|
||||||
@@ -505,7 +479,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<ul
|
<ul
|
||||||
role="menu"
|
role="menu"
|
||||||
class="dropdown-content z-[1] menu p-2 shadow-xl bg-base-100 rounded-box w-52 border border-base-300"
|
class="dropdown-content menu bg-base-100 rounded-box border-base-300 z-1 w-52 border p-2 shadow-xl"
|
||||||
>
|
>
|
||||||
<li>
|
<li>
|
||||||
<button type="button" onclick={() => editarTime(time)}>
|
<button type="button" onclick={() => editarTime(time)}>
|
||||||
@@ -527,10 +501,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button type="button" onclick={() => abrirGerenciarMembros(time)}>
|
||||||
type="button"
|
|
||||||
onclick={() => abrirGerenciarMembros(time)}
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-4 w-4"
|
class="h-4 w-4"
|
||||||
@@ -575,8 +546,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-sm text-base-content/70 mb-3 min-h-[2rem]">
|
<p class="text-base-content/70 mb-3 min-h-8 text-sm">
|
||||||
{time.descricao || "Sem descrição"}
|
{time.descricao || 'Sem descrição'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="divider my-2"></div>
|
<div class="divider my-2"></div>
|
||||||
@@ -585,7 +556,7 @@
|
|||||||
<div class="flex items-center gap-2 text-sm">
|
<div class="flex items-center gap-2 text-sm">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-4 w-4 text-primary"
|
class="text-primary h-4 w-4"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -599,13 +570,13 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span class="text-base-content/70"
|
<span class="text-base-content/70"
|
||||||
><strong>Gestor:</strong>
|
><strong>Gestor:</strong>
|
||||||
{time.gestor?.nome || "Não definido"}</span
|
{time.gestor?.nome || 'Não definido'}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 text-sm">
|
<div class="flex items-center gap-2 text-sm">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-4 w-4 text-primary"
|
class="text-primary h-4 w-4"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -619,14 +590,12 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span class="text-base-content/70"
|
<span class="text-base-content/70"
|
||||||
><strong>Membros:</strong>
|
><strong>Membros:</strong>
|
||||||
<span class="badge badge-primary badge-sm"
|
<span class="badge badge-primary badge-sm">{time.totalMembros || 0}</span></span
|
||||||
>{time.totalMembros || 0}</span
|
|
||||||
></span
|
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions justify-end mt-4">
|
<div class="card-actions mt-4 justify-end">
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-outline btn-primary"
|
class="btn btn-sm btn-outline btn-primary"
|
||||||
onclick={() => abrirGerenciarMembros(time)}
|
onclick={() => abrirGerenciarMembros(time)}
|
||||||
@@ -640,12 +609,10 @@
|
|||||||
|
|
||||||
{#if times.filter((t: TimeComDetalhes) => t.ativo).length === 0}
|
{#if times.filter((t: TimeComDetalhes) => t.ativo).length === 0}
|
||||||
<div class="col-span-full">
|
<div class="col-span-full">
|
||||||
<div
|
<div class="flex flex-col items-center justify-center py-16 text-center">
|
||||||
class="flex flex-col items-center justify-center py-16 text-center"
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-16 w-16 text-base-content/30"
|
class="text-base-content/30 h-16 w-16"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -657,7 +624,7 @@
|
|||||||
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"
|
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>
|
</svg>
|
||||||
<h3 class="text-xl font-semibold mt-4">Nenhum time cadastrado</h3>
|
<h3 class="mt-4 text-xl font-semibold">Nenhum time cadastrado</h3>
|
||||||
<p class="text-base-content/60 mt-2">
|
<p class="text-base-content/60 mt-2">
|
||||||
Clique em "Novo Time" para criar seu primeiro time
|
Clique em "Novo Time" para criar seu primeiro time
|
||||||
</p>
|
</p>
|
||||||
@@ -671,35 +638,26 @@
|
|||||||
{#if mostrarModalMembros && timeParaMembros}
|
{#if mostrarModalMembros && timeParaMembros}
|
||||||
<dialog class="modal modal-open">
|
<dialog class="modal modal-open">
|
||||||
<div class="modal-box max-w-4xl">
|
<div class="modal-box max-w-4xl">
|
||||||
<h3 class="font-bold text-2xl mb-4 flex items-center gap-2">
|
<h3 class="mb-4 flex items-center gap-2 text-2xl font-bold">
|
||||||
<div
|
<div class="h-3 w-3 rounded-full" style="background-color: {timeParaMembros.cor}"></div>
|
||||||
class="w-3 h-3 rounded-full"
|
|
||||||
style="background-color: {timeParaMembros.cor}"
|
|
||||||
></div>
|
|
||||||
{timeParaMembros.nome}
|
{timeParaMembros.nome}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<!-- Membros Atuais -->
|
<!-- Membros Atuais -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h4 class="font-bold text-lg mb-3">
|
<h4 class="mb-3 text-lg font-bold">
|
||||||
Membros Atuais ({timeParaMembros.membros?.length || 0})
|
Membros Atuais ({timeParaMembros.membros?.length || 0})
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
{#if timeParaMembros.membros && timeParaMembros.membros.length > 0}
|
{#if timeParaMembros.membros && timeParaMembros.membros.length > 0}
|
||||||
<div class="space-y-2 max-h-60 overflow-y-auto">
|
<div class="max-h-60 space-y-2 overflow-y-auto">
|
||||||
{#each timeParaMembros.membros as membro}
|
{#each timeParaMembros.membros as membro (membro._id)}
|
||||||
<div
|
<div class="bg-base-200 flex items-center justify-between rounded-lg p-3">
|
||||||
class="flex items-center justify-between p-3 bg-base-200 rounded-lg"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="avatar placeholder">
|
<div class="avatar placeholder">
|
||||||
<div
|
<div class="bg-primary text-primary-content w-10 rounded-full">
|
||||||
class="bg-primary text-primary-content rounded-full w-10"
|
|
||||||
>
|
|
||||||
<span class="text-xs"
|
<span class="text-xs"
|
||||||
>{membro.funcionario?.nome
|
>{membro.funcionario?.nome.substring(0, 2).toUpperCase()}</span
|
||||||
.substring(0, 2)
|
|
||||||
.toUpperCase()}</span
|
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -707,10 +665,8 @@
|
|||||||
<div class="font-semibold">
|
<div class="font-semibold">
|
||||||
{membro.funcionario?.nome}
|
{membro.funcionario?.nome}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-base-content/50">
|
<div class="text-base-content/50 text-xs">
|
||||||
Desde {new Date(
|
Desde {new Date(membro.dataEntrada).toLocaleDateString('pt-BR')}
|
||||||
membro.dataEntrada,
|
|
||||||
).toLocaleDateString("pt-BR")}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -744,7 +700,7 @@
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
class="stroke-current shrink-0 w-6 h-6"
|
class="h-6 w-6 shrink-0 stroke-current"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
@@ -762,30 +718,26 @@
|
|||||||
|
|
||||||
<!-- Adicionar Membros -->
|
<!-- Adicionar Membros -->
|
||||||
<div>
|
<div>
|
||||||
<h4 class="font-bold text-lg mb-3">Adicionar Membros</h4>
|
<h4 class="mb-3 text-lg font-bold">Adicionar Membros</h4>
|
||||||
|
|
||||||
{#if membrosDisponiveis.length > 0}
|
{#if membrosDisponiveis.length > 0}
|
||||||
<div class="space-y-2 max-h-60 overflow-y-auto">
|
<div class="max-h-60 space-y-2 overflow-y-auto">
|
||||||
{#each membrosDisponiveis as funcionario}
|
{#each membrosDisponiveis as funcionario (funcionario._id)}
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors"
|
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="flex items-center gap-3">
|
||||||
<div class="avatar placeholder">
|
<div class="avatar placeholder">
|
||||||
<div
|
<div class="bg-neutral text-neutral-content w-10 rounded-full">
|
||||||
class="bg-neutral text-neutral-content rounded-full w-10"
|
|
||||||
>
|
|
||||||
<span class="text-xs"
|
<span class="text-xs"
|
||||||
>{funcionario.nome
|
>{funcionario.nome.substring(0, 2).toUpperCase()}</span
|
||||||
.substring(0, 2)
|
|
||||||
.toUpperCase()}</span
|
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-semibold">{funcionario.nome}</div>
|
<div class="font-semibold">{funcionario.nome}</div>
|
||||||
<div class="text-xs text-base-content/50">
|
<div class="text-base-content/50 text-xs">
|
||||||
{funcionario.matricula || "S/N"}
|
{funcionario.matricula || 'S/N'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -805,7 +757,7 @@
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
class="stroke-current shrink-0 w-6 h-6"
|
class="h-6 w-6 shrink-0 stroke-current"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
@@ -824,10 +776,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="dialog" class="modal-backdrop">
|
<form method="dialog" class="modal-backdrop">
|
||||||
<button
|
<button type="button" onclick={fecharModalMembros} aria-label="Fechar modal"
|
||||||
type="button"
|
>Fechar</button
|
||||||
onclick={fecharModalMembros}
|
|
||||||
aria-label="Fechar modal">Fechar</button
|
|
||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
@@ -837,11 +787,10 @@
|
|||||||
{#if mostrarConfirmacaoExclusao && timeParaExcluir}
|
{#if mostrarConfirmacaoExclusao && timeParaExcluir}
|
||||||
<dialog class="modal modal-open">
|
<dialog class="modal modal-open">
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
<h3 class="font-bold text-lg">Confirmar Desativação</h3>
|
<h3 class="text-lg font-bold">Confirmar Desativação</h3>
|
||||||
<p class="py-4">
|
<p class="py-4">
|
||||||
Tem certeza que deseja desativar o time <strong
|
Tem certeza que deseja desativar o time <strong>{timeParaExcluir.nome}</strong>? Todos
|
||||||
>{timeParaExcluir.nome}</strong
|
os membros serão removidos.
|
||||||
>? Todos os membros serão removidos.
|
|
||||||
</p>
|
</p>
|
||||||
<div class="modal-action">
|
<div class="modal-action">
|
||||||
<button
|
<button
|
||||||
@@ -851,12 +800,8 @@
|
|||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="btn btn-error" onclick={excluirTime} disabled={processando}>
|
||||||
class="btn btn-error"
|
{processando ? 'Processando...' : 'Desativar'}
|
||||||
onclick={excluirTime}
|
|
||||||
disabled={processando}
|
|
||||||
>
|
|
||||||
{processando ? "Processando..." : "Desativar"}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
1
bun.lock
1
bun.lock
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 0,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sgse-app",
|
"name": "sgse-app",
|
||||||
|
|||||||
@@ -27,13 +27,13 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-svelte": "^3.13.0",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
"jiti": "^2.6.1",
|
"jiti": "^2.6.1",
|
||||||
"turbo": "^2.5.8",
|
|
||||||
"typescript-eslint": "^8.46.3",
|
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"eslint-plugin-svelte": "^3.13.0",
|
"prettier-plugin-tailwindcss": "^0.7.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.1"
|
"turbo": "^2.5.8",
|
||||||
|
"typescript-eslint": "^8.46.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/svelte-form": "^1.23.8",
|
"@tanstack/svelte-form": "^1.23.8",
|
||||||
|
|||||||
@@ -8,7 +8,16 @@ import { getCurrentUserFunction } from './auth';
|
|||||||
export const CATALOGO_RECURSOS = [
|
export const CATALOGO_RECURSOS = [
|
||||||
{
|
{
|
||||||
recurso: 'funcionarios',
|
recurso: 'funcionarios',
|
||||||
acoes: ['dashboard', 'ver', 'listar', 'criar', 'editar', 'excluir']
|
acoes: [
|
||||||
|
'dashboard',
|
||||||
|
'ver',
|
||||||
|
'listar',
|
||||||
|
'criar',
|
||||||
|
'editar',
|
||||||
|
'excluir',
|
||||||
|
'aprovar_ausencias',
|
||||||
|
'aprovar_ferias'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
recurso: 'simbolos',
|
recurso: 'simbolos',
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { v } from "convex/values";
|
import { v } from 'convex/values';
|
||||||
import { query } from "./_generated/server";
|
import { query, mutation } from './_generated/server';
|
||||||
|
import type { Id } from './_generated/dataModel';
|
||||||
|
import { getCurrentUserFunction } from './auth';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listar todas as roles
|
* Listar todas as roles
|
||||||
@@ -7,8 +9,8 @@ import { query } from "./_generated/server";
|
|||||||
export const listar = query({
|
export const listar = query({
|
||||||
args: {},
|
args: {},
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
return await ctx.db.query("roles").collect();
|
return await ctx.db.query('roles').collect();
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,20 +18,108 @@ export const listar = query({
|
|||||||
*/
|
*/
|
||||||
export const buscarPorId = query({
|
export const buscarPorId = query({
|
||||||
args: {
|
args: {
|
||||||
roleId: v.id("roles"),
|
roleId: v.id('roles')
|
||||||
},
|
},
|
||||||
returns: v.union(
|
returns: v.union(
|
||||||
v.object({
|
v.object({
|
||||||
_id: v.id("roles"),
|
_id: v.id('roles'),
|
||||||
nome: v.string(),
|
nome: v.string(),
|
||||||
descricao: v.string(),
|
descricao: v.string(),
|
||||||
nivel: v.number(),
|
nivel: v.number(),
|
||||||
setor: v.optional(v.string()),
|
setor: v.optional(v.string())
|
||||||
}),
|
}),
|
||||||
v.null()
|
v.null()
|
||||||
),
|
),
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
return await ctx.db.get(args.roleId);
|
return await ctx.db.get(args.roleId);
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const slugify = (value: string) =>
|
||||||
|
value
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '_')
|
||||||
|
.replace(/^_+|_+$/g, '')
|
||||||
|
.replace(/_{2,}/g, '_');
|
||||||
|
|
||||||
|
export const criar = mutation({
|
||||||
|
args: {
|
||||||
|
nome: v.string(),
|
||||||
|
descricao: v.string(),
|
||||||
|
nivel: v.number(),
|
||||||
|
setor: v.optional(v.string()),
|
||||||
|
copiarDeRoleId: v.optional(v.id('roles'))
|
||||||
|
},
|
||||||
|
returns: v.union(
|
||||||
|
v.object({ sucesso: v.literal(true), roleId: v.id('roles') }),
|
||||||
|
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||||
|
),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const usuarioAtual = await getCurrentUserFunction(ctx);
|
||||||
|
if (!usuarioAtual) {
|
||||||
|
return { sucesso: false as const, erro: 'nao_autenticado' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleAtual = await ctx.db.get(usuarioAtual.roleId);
|
||||||
|
if (!roleAtual || roleAtual.nivel > 1) {
|
||||||
|
return { sucesso: false as const, erro: 'sem_permissao' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const nomeNormalizado = slugify(args.nome);
|
||||||
|
if (!nomeNormalizado) {
|
||||||
|
return { sucesso: false as const, erro: 'nome_invalido' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const existente = await ctx.db
|
||||||
|
.query('roles')
|
||||||
|
.withIndex('by_nome', (q) => q.eq('nome', nomeNormalizado))
|
||||||
|
.unique();
|
||||||
|
|
||||||
|
if (existente) {
|
||||||
|
return { sucesso: false as const, erro: 'nome_ja_utilizado' };
|
||||||
|
}
|
||||||
|
|
||||||
|
let permissoesParaCopiar: Array<Id<'permissoes'>> = [];
|
||||||
|
|
||||||
|
if (args.copiarDeRoleId) {
|
||||||
|
const roleOrigem = await ctx.db.get(args.copiarDeRoleId);
|
||||||
|
if (!roleOrigem) {
|
||||||
|
return { sucesso: false as const, erro: 'role_origem_nao_encontrada' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissoesOrigem = await ctx.db
|
||||||
|
.query('rolePermissoes')
|
||||||
|
.withIndex('by_role', (q) => q.eq('roleId', args.copiarDeRoleId!))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
permissoesParaCopiar = permissoesOrigem.map((item) => item.permissaoId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nivelAjustado = Math.min(Math.max(Math.round(args.nivel), 0), 10);
|
||||||
|
const setor = args.setor?.trim();
|
||||||
|
|
||||||
|
const roleId = await ctx.db.insert('roles', {
|
||||||
|
nome: nomeNormalizado,
|
||||||
|
descricao: args.descricao.trim() || args.nome.trim(),
|
||||||
|
nivel: nivelAjustado,
|
||||||
|
setor: setor && setor.length > 0 ? setor : undefined,
|
||||||
|
customizado: true,
|
||||||
|
criadoPor: usuarioAtual._id,
|
||||||
|
editavel: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (permissoesParaCopiar.length > 0) {
|
||||||
|
for (const permissaoId of permissoesParaCopiar) {
|
||||||
|
await ctx.db.insert('rolePermissoes', {
|
||||||
|
roleId,
|
||||||
|
permissaoId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sucesso: true as const, roleId };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -562,15 +562,12 @@ export const obterPerfil = query({
|
|||||||
),
|
),
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
const usuarioAutenticado = await getCurrentUserFunction(ctx);
|
const usuarioAutenticado = await getCurrentUserFunction(ctx);
|
||||||
console.log('Usuario autenticado:', usuarioAutenticado);
|
|
||||||
if (!usuarioAutenticado) {
|
if (!usuarioAutenticado) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const usuarioAtual = usuarioAutenticado;
|
const usuarioAtual = usuarioAutenticado;
|
||||||
|
|
||||||
console.log('✅ Usuário encontrado:', usuarioAtual.nome);
|
|
||||||
|
|
||||||
// Buscar fotoPerfil URL se existir
|
// Buscar fotoPerfil URL se existir
|
||||||
let fotoPerfilUrl = null;
|
let fotoPerfilUrl = null;
|
||||||
if (usuarioAtual.fotoPerfil) {
|
if (usuarioAtual.fotoPerfil) {
|
||||||
|
|||||||
Reference in New Issue
Block a user