Emp perfis #25
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useConvexClient } from "convex-svelte";
|
import { useConvexClient } from 'convex-svelte';
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
import {
|
import {
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
FileText,
|
FileText,
|
||||||
@@ -7,8 +8,8 @@
|
|||||||
Upload,
|
Upload,
|
||||||
Trash2,
|
Trash2,
|
||||||
Eye,
|
Eye,
|
||||||
RefreshCw,
|
RefreshCw
|
||||||
} from "lucide-svelte";
|
} from 'lucide-svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -27,48 +28,47 @@
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
required = false,
|
required = false,
|
||||||
onUpload,
|
onUpload,
|
||||||
onRemove,
|
onRemove
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient() as unknown as {
|
||||||
|
storage: {
|
||||||
|
getUrl: (id: string) => Promise<string | null>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
let fileInput: HTMLInputElement;
|
let fileInput: HTMLInputElement;
|
||||||
let uploading = $state(false);
|
let uploading = $state(false);
|
||||||
let error = $state<string | null>(null);
|
let error = $state<string | null>(null);
|
||||||
let fileName = $state<string>("");
|
let fileName = $state<string>('');
|
||||||
let fileType = $state<string>("");
|
let fileType = $state<string>('');
|
||||||
let previewUrl = $state<string | null>(null);
|
let previewUrl = $state<string | null>(null);
|
||||||
let fileUrl = $state<string | null>(null);
|
let fileUrl = $state<string | null>(null);
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||||
const ALLOWED_TYPES = [
|
const ALLOWED_TYPES = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png'];
|
||||||
"application/pdf",
|
|
||||||
"image/jpeg",
|
|
||||||
"image/jpg",
|
|
||||||
"image/png",
|
|
||||||
];
|
|
||||||
|
|
||||||
// Buscar URL do arquivo quando houver um storageId
|
// Buscar URL do arquivo quando houver um storageId
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (value && !fileName) {
|
if (value && !fileName) {
|
||||||
// Tem storageId mas não é um upload recente
|
// Tem storageId mas não é um upload recente
|
||||||
loadExistingFile(value);
|
void loadExistingFile(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadExistingFile(storageId: string) {
|
async function loadExistingFile(storageId: string) {
|
||||||
try {
|
try {
|
||||||
const url = await client.storage.getUrl(storageId as any);
|
const url = await client.storage.getUrl(storageId);
|
||||||
if (url) {
|
if (url) {
|
||||||
async function handleFileSelect(event: Event) {
|
fileUrl = url;
|
||||||
const target = event.target as HTMLInputElement;
|
|
||||||
const file = target.files?.[0];
|
|
||||||
// Detectar tipo pelo URL ou assumir PDF
|
// Detectar tipo pelo URL ou assumir PDF
|
||||||
if (url.includes('.pdf') || url.includes('application/pdf')) {
|
if (url.includes('.pdf') || url.includes('application/pdf')) {
|
||||||
fileType = 'application/pdf';
|
fileType = 'application/pdf';
|
||||||
} else {
|
} else {
|
||||||
fileType = 'image/jpeg';
|
fileType = 'image/jpeg';
|
||||||
previewUrl = url; // Para imagens, a URL serve como preview
|
// Para imagens, a URL serve como preview
|
||||||
|
previewUrl = url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -76,19 +76,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleFileSelect(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
const file = target.files?.[0];
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
error = null;
|
error = null;
|
||||||
|
|
||||||
// Validate file size
|
// Validate file size
|
||||||
if (file.size > MAX_FILE_SIZE) {
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
error = "Arquivo muito grande. Tamanho máximo: 10MB";
|
error = 'Arquivo muito grande. Tamanho máximo: 10MB';
|
||||||
target.value = "";
|
target.value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate file type
|
// Validate file type
|
||||||
if (!ALLOWED_TYPES.includes(file.type)) {
|
if (!ALLOWED_TYPES.includes(file.type)) {
|
||||||
error = "Tipo de arquivo não permitido. Use PDF ou imagens (JPG, PNG)";
|
error = 'Tipo de arquivo não permitido. Use PDF ou imagens (JPG, PNG)';
|
||||||
target.value = "";
|
target.value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,38 +106,52 @@
|
|||||||
fileType = file.type;
|
fileType = file.type;
|
||||||
|
|
||||||
// Create preview for images
|
// Create preview for images
|
||||||
if (file.type.startsWith("image/")) {
|
if (file.type.startsWith('image/')) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
previewUrl = e.target?.result as string;
|
const result = e.target?.result;
|
||||||
|
if (typeof result === 'string') {
|
||||||
|
previewUrl = result;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
|
} else {
|
||||||
|
previewUrl = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await onUpload(file);
|
await onUpload(file);
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
error = err?.message || "Erro ao fazer upload do arquivo";
|
if (err instanceof Error) {
|
||||||
|
error = err.message || 'Erro ao fazer upload do arquivo';
|
||||||
|
} else {
|
||||||
|
error = 'Erro ao fazer upload do arquivo';
|
||||||
|
}
|
||||||
previewUrl = null;
|
previewUrl = null;
|
||||||
} finally {
|
} finally {
|
||||||
uploading = false;
|
uploading = false;
|
||||||
target.value = "";
|
target.value = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemove() {
|
async function handleRemove() {
|
||||||
if (!confirm("Tem certeza que deseja remover este arquivo?")) {
|
if (!confirm('Tem certeza que deseja remover este arquivo?')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
uploading = true;
|
uploading = true;
|
||||||
await onRemove();
|
await onRemove();
|
||||||
fileName = "";
|
fileName = '';
|
||||||
fileType = "";
|
fileType = '';
|
||||||
previewUrl = null;
|
previewUrl = null;
|
||||||
fileUrl = null;
|
fileUrl = null;
|
||||||
} catch (err: any) {
|
error = null;
|
||||||
error = err?.message || "Erro ao remover arquivo";
|
} catch (err: unknown) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
error = err.message || 'Erro ao remover arquivo';
|
||||||
|
} else {
|
||||||
|
error = 'Erro ao remover arquivo';
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
uploading = false;
|
uploading = false;
|
||||||
}
|
}
|
||||||
@@ -137,7 +159,7 @@
|
|||||||
|
|
||||||
function handleView() {
|
function handleView() {
|
||||||
if (fileUrl) {
|
if (fileUrl) {
|
||||||
window.open(fileUrl, "_blank");
|
window.open(fileUrl, '_blank');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,18 +170,15 @@
|
|||||||
|
|
||||||
<div class="form-control w-full">
|
<div class="form-control w-full">
|
||||||
<label class="label" for="file-upload-input">
|
<label class="label" for="file-upload-input">
|
||||||
<span class="label-text font-medium flex items-center gap-2">
|
<span class="label-text flex items-center gap-2 font-medium">
|
||||||
{label}
|
{label}
|
||||||
{#if required}
|
{#if required}
|
||||||
<span class="text-error">*</span>
|
<span class="text-error">*</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if helpUrl}
|
{#if helpUrl}
|
||||||
<div
|
<div class="tooltip tooltip-right" data-tip="Clique para acessar o link">
|
||||||
class="tooltip tooltip-right"
|
|
||||||
data-tip="Clique para acessar o link"
|
|
||||||
>
|
|
||||||
<a
|
<a
|
||||||
href={helpUrl}
|
href={resolve(helpUrl)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="text-primary hover:text-primary-focus transition-colors"
|
class="text-primary hover:text-primary-focus transition-colors"
|
||||||
@@ -194,17 +213,17 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="bg-success/10 flex h-12 w-12 items-center justify-center rounded">
|
<div class="bg-success/10 flex h-12 w-12 items-center justify-center rounded">
|
||||||
<File class="text-success h-6 w-6" strokeWidth={2} />
|
<FileIcon class="text-success h-6 w-6" strokeWidth={2} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- File info -->
|
<!-- File info -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="min-w-0 flex-1">
|
||||||
<p class="text-sm font-medium truncate">
|
<p class="truncate text-sm font-medium">
|
||||||
{fileName || "Arquivo anexado"}
|
{fileName || 'Arquivo anexado'}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-base-content/60">
|
<p class="text-base-content/60 text-xs">
|
||||||
{#if uploading}
|
{#if uploading}
|
||||||
Carregando...
|
Carregando...
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useQuery } from "convex-svelte";
|
import { useQuery } from 'convex-svelte';
|
||||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import { onMount } from "svelte";
|
import { onMount } from 'svelte';
|
||||||
import { page } from "$app/stores";
|
import type { Snippet } from 'svelte';
|
||||||
import type { Snippet } from "svelte";
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
children,
|
children,
|
||||||
requireAuth = true,
|
requireAuth = true,
|
||||||
allowedRoles = [],
|
allowedRoles = [],
|
||||||
maxLevel = 3,
|
maxLevel = 3,
|
||||||
redirectTo = "/",
|
redirectTo = '/'
|
||||||
}: {
|
}: {
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
requireAuth?: boolean;
|
requireAuth?: boolean;
|
||||||
@@ -41,9 +40,7 @@
|
|||||||
|
|
||||||
// Verificar roles
|
// Verificar roles
|
||||||
if (allowedRoles.length > 0 && currentUser?.data) {
|
if (allowedRoles.length > 0 && currentUser?.data) {
|
||||||
const hasRole = allowedRoles.includes(
|
const hasRole = allowedRoles.includes(currentUser.data.role?.nome ?? '');
|
||||||
currentUser.data.role?.nome ?? "",
|
|
||||||
);
|
|
||||||
if (!hasRole) {
|
if (!hasRole) {
|
||||||
const currentPath = window.location.pathname;
|
const currentPath = window.location.pathname;
|
||||||
window.location.href = `${redirectTo}?error=access_denied&route=${encodeURIComponent(currentPath)}`;
|
window.location.href = `${redirectTo}?error=access_denied&route=${encodeURIComponent(currentPath)}`;
|
||||||
@@ -69,10 +66,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isChecking}
|
{#if isChecking}
|
||||||
<div class="flex justify-center items-center min-h-screen">
|
<div class="flex min-h-screen items-center justify-center">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
<p class="mt-4 text-base-content/70">Verificando permissões...</p>
|
<p class="text-base-content/70 mt-4">Verificando permissões...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if hasAccess}
|
{:else if hasAccess}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import { resolve } from "$app/paths";
|
import { resolve } from "$app/paths";
|
||||||
import { UserPlus, Mail } from "lucide-svelte";
|
import { UserPlus, Mail } from "lucide-svelte";
|
||||||
import { useAuth } from "@mmailaender/convex-better-auth-svelte/svelte";
|
import { useAuth } from "@mmailaender/convex-better-auth-svelte/svelte";
|
||||||
|
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
@@ -128,6 +129,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<ProtectedRoute>
|
||||||
<main class="container mx-auto px-4 py-4">
|
<main class="container mx-auto px-4 py-4">
|
||||||
<!-- Alerta de Acesso Negado / Autenticação -->
|
<!-- Alerta de Acesso Negado / Autenticação -->
|
||||||
{#if showAlert}
|
{#if showAlert}
|
||||||
@@ -823,6 +825,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
</ProtectedRoute>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ShoppingCart, ShoppingBag, Plus } from "lucide-svelte";
|
import { ShoppingCart, ShoppingBag, Plus } from "lucide-svelte";
|
||||||
import { resolve } from "$app/paths";
|
import { resolve } from "$app/paths";
|
||||||
|
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<ProtectedRoute>
|
||||||
<main class="container mx-auto px-4 py-4">
|
<main class="container mx-auto px-4 py-4">
|
||||||
<div class="text-sm breadcrumbs mb-4">
|
<div class="text-sm breadcrumbs mb-4">
|
||||||
<ul>
|
<ul>
|
||||||
@@ -41,4 +43,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
</ProtectedRoute>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Megaphone, Edit, Plus } from "lucide-svelte";
|
import { Megaphone, Edit, Plus } from "lucide-svelte";
|
||||||
import { resolve } from "$app/paths";
|
import { resolve } from "$app/paths";
|
||||||
|
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<ProtectedRoute>
|
||||||
<main class="container mx-auto px-4 py-4">
|
<main class="container mx-auto px-4 py-4">
|
||||||
<div class="text-sm breadcrumbs mb-4">
|
<div class="text-sm breadcrumbs mb-4">
|
||||||
<ul>
|
<ul>
|
||||||
@@ -41,4 +43,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
</ProtectedRoute>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { BarChart3, ClipboardCheck, Plus, CheckCircle2, Clock, TrendingUp } from "lucide-svelte";
|
import { BarChart3, ClipboardCheck, Plus, CheckCircle2, Clock, TrendingUp } from "lucide-svelte";
|
||||||
import { resolve } from "$app/paths";
|
import { resolve } from "$app/paths";
|
||||||
|
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<ProtectedRoute>
|
||||||
<main class="container mx-auto px-4 py-4">
|
<main class="container mx-auto px-4 py-4">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="text-sm breadcrumbs mb-4">
|
<div class="text-sm breadcrumbs mb-4">
|
||||||
@@ -86,4 +88,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
</ProtectedRoute>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DollarSign, Building2, Plus, Calculator, TrendingUp, FileText } from "lucide-svelte";
|
import { DollarSign, Building2, Plus, Calculator, TrendingUp, FileText } from "lucide-svelte";
|
||||||
import { resolve } from "$app/paths";
|
import { resolve } from "$app/paths";
|
||||||
|
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<ProtectedRoute>
|
||||||
<main class="container mx-auto px-4 py-4">
|
<main class="container mx-auto px-4 py-4">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="text-sm breadcrumbs mb-4">
|
<div class="text-sm breadcrumbs mb-4">
|
||||||
@@ -86,4 +88,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
</ProtectedRoute>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Scale, BookOpen, Plus } from "lucide-svelte";
|
import { Scale, BookOpen, Plus } from "lucide-svelte";
|
||||||
import { resolve } from "$app/paths";
|
import { resolve } from "$app/paths";
|
||||||
|
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<ProtectedRoute>
|
||||||
<main class="container mx-auto px-4 py-4">
|
<main class="container mx-auto px-4 py-4">
|
||||||
<div class="text-sm breadcrumbs mb-4">
|
<div class="text-sm breadcrumbs mb-4">
|
||||||
<ul>
|
<ul>
|
||||||
@@ -41,4 +43,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
</ProtectedRoute>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { FileText, ClipboardCopy, Plus, Users, FileDoc } from "lucide-svelte";
|
import { FileText, ClipboardCopy, Plus, Users, FileDoc } from "lucide-svelte";
|
||||||
import { resolve } from "$app/paths";
|
import { resolve } from "$app/paths";
|
||||||
|
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<ProtectedRoute>
|
||||||
<main class="container mx-auto px-4 py-4">
|
<main class="container mx-auto px-4 py-4">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="text-sm breadcrumbs mb-4">
|
<div class="text-sm breadcrumbs mb-4">
|
||||||
@@ -86,4 +88,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
</ProtectedRoute>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import AprovarAusencias from '$lib/components/AprovarAusencias.svelte';
|
import AprovarAusencias from '$lib/components/AprovarAusencias.svelte';
|
||||||
import CalendarioAusencias from '$lib/components/ausencias/CalendarioAusencias.svelte';
|
import CalendarioAusencias from '$lib/components/ausencias/CalendarioAusencias.svelte';
|
||||||
import { generateAvatarGallery } from '$lib/utils/avatars';
|
import { generateAvatarGallery } from '$lib/utils/avatars';
|
||||||
|
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
|
||||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import { X, Calendar } from 'lucide-svelte';
|
import { X, Calendar } from 'lucide-svelte';
|
||||||
import type { FunctionReturnType } from 'convex/server';
|
import type { FunctionReturnType } from 'convex/server';
|
||||||
@@ -365,6 +366,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<ProtectedRoute>
|
||||||
<main class="min-h-screen pb-12">
|
<main class="min-h-screen pb-12">
|
||||||
<!-- BANNER HERO PREMIUM -->
|
<!-- BANNER HERO PREMIUM -->
|
||||||
<div class="relative mb-8 overflow-hidden">
|
<div class="relative mb-8 overflow-hidden">
|
||||||
@@ -2264,6 +2266,7 @@
|
|||||||
</dialog>
|
</dialog>
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
</ProtectedRoute>
|
||||||
|
|
||||||
<!-- Modal Wizard Solicitação de Férias -->
|
<!-- Modal Wizard Solicitação de Férias -->
|
||||||
{#if mostrarWizard && funcionarioIdDisponivel}
|
{#if mostrarWizard && funcionarioIdDisponivel}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Trophy, Award, Plus } from "lucide-svelte";
|
import { Trophy, Award, Plus } from "lucide-svelte";
|
||||||
import { resolve } from "$app/paths";
|
import { resolve } from "$app/paths";
|
||||||
|
import ProtectedRoute from "$lib/components/ProtectedRoute.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<ProtectedRoute>
|
||||||
<main class="container mx-auto px-4 py-4">
|
<main class="container mx-auto px-4 py-4">
|
||||||
<div class="text-sm breadcrumbs mb-4">
|
<div class="text-sm breadcrumbs mb-4">
|
||||||
<ul>
|
<ul>
|
||||||
@@ -41,4 +43,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
</ProtectedRoute>
|
||||||
|
|
||||||
|
|||||||
@@ -543,7 +543,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if catalogoQuery.data}
|
{:else if catalogoQuery.data && catalogoQuery.data.length > 0}
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{#each catalogoQuery.data as item (item.recurso)}
|
{#each catalogoQuery.data as item (item.recurso)}
|
||||||
{@const recursoExpandido = isRecursoExpandido(roleId, item.recurso)}
|
{@const recursoExpandido = isRecursoExpandido(roleId, item.recurso)}
|
||||||
@@ -576,8 +576,13 @@
|
|||||||
<!-- Lista de ações (visível quando expandido) -->
|
<!-- Lista de ações (visível quando expandido) -->
|
||||||
{#if recursoExpandido}
|
{#if recursoExpandido}
|
||||||
<div class="bg-base-100 border-base-300 border-t px-4 py-3">
|
<div class="bg-base-100 border-base-300 border-t px-4 py-3">
|
||||||
|
{#if item.acoes.length === 0}
|
||||||
|
<p class="text-base-content/60 text-sm">
|
||||||
|
Nenhuma permissão cadastrada para este recurso.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{#each ['ver', 'listar', 'criar', 'editar', 'excluir'] as acao (acao)}
|
{#each item.acoes as acao (acao)}
|
||||||
<label
|
<label
|
||||||
class="hover:bg-base-200 flex cursor-pointer items-center gap-3 rounded p-2 transition-colors"
|
class="hover:bg-base-200 flex cursor-pointer items-center gap-3 rounded p-2 transition-colors"
|
||||||
>
|
>
|
||||||
@@ -589,15 +594,22 @@
|
|||||||
onchange={(e) =>
|
onchange={(e) =>
|
||||||
toggleAcao(roleId, item.recurso, acao, e.currentTarget.checked)}
|
toggleAcao(roleId, item.recurso, acao, e.currentTarget.checked)}
|
||||||
/>
|
/>
|
||||||
<span class="flex-1 font-medium capitalize">{acao}</span>
|
<span class="flex-1 font-medium">{acao}</span>
|
||||||
</label>
|
</label>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="alert alert-info mt-4">
|
||||||
|
<span class="font-semibold">
|
||||||
|
Nenhuma permissão cadastrada ainda. Use o botão “Criar permissão” para começar.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,19 +41,15 @@
|
|||||||
const stats = $derived.by(() => {
|
const stats = $derived.by(() => {
|
||||||
if (carregando) return null;
|
if (carregando) return null;
|
||||||
|
|
||||||
const porNivel = {
|
const nivelMaximo = roles.filter((r) => r.nivel === 0).length;
|
||||||
0: roles.filter((r) => r.nivel === 0).length,
|
const nivelAdministrativo = roles.filter((r) => r.nivel === 1).length;
|
||||||
1: roles.filter((r) => r.nivel === 1).length,
|
const niveisLegado = roles.filter((r) => r.nivel > 1).length;
|
||||||
2: roles.filter((r) => r.nivel === 2).length,
|
|
||||||
3: roles.filter((r) => r.nivel >= 3).length
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total: roles.length,
|
total: roles.length,
|
||||||
nivelMaximo: porNivel[0],
|
nivelMaximo,
|
||||||
nivelAlto: porNivel[1],
|
nivelAdministrativo,
|
||||||
nivelMedio: porNivel[2],
|
niveisLegado,
|
||||||
nivelBaixo: porNivel[3],
|
|
||||||
comSetor: roles.filter((r) => r.setor).length
|
comSetor: roles.filter((r) => r.setor).length
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -78,10 +74,11 @@
|
|||||||
|
|
||||||
// Filtro por nível
|
// Filtro por nível
|
||||||
if (filtroNivel !== '') {
|
if (filtroNivel !== '') {
|
||||||
if (filtroNivel === 3) {
|
if (filtroNivel === 0 || filtroNivel === 1) {
|
||||||
resultado = resultado.filter((r) => r.nivel >= 3);
|
|
||||||
} else {
|
|
||||||
resultado = resultado.filter((r) => r.nivel === filtroNivel);
|
resultado = resultado.filter((r) => r.nivel === filtroNivel);
|
||||||
|
} else {
|
||||||
|
// Qualquer outro valor é considerado legado
|
||||||
|
resultado = resultado.filter((r) => r.nivel > 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,22 +93,19 @@
|
|||||||
function obterCorNivel(nivel: number): string {
|
function obterCorNivel(nivel: number): string {
|
||||||
if (nivel === 0) return 'badge-error';
|
if (nivel === 0) return 'badge-error';
|
||||||
if (nivel === 1) return 'badge-warning';
|
if (nivel === 1) return 'badge-warning';
|
||||||
if (nivel === 2) return 'badge-info';
|
// Níveis > 1 são considerados legado
|
||||||
return 'badge-ghost';
|
return 'badge-ghost';
|
||||||
}
|
}
|
||||||
|
|
||||||
function obterTextoNivel(nivel: number): string {
|
function obterTextoNivel(nivel: number): string {
|
||||||
if (nivel === 0) return 'Máximo';
|
if (nivel === 0) return 'Máximo';
|
||||||
if (nivel === 1) return 'Alto';
|
if (nivel === 1) return 'Administrativo';
|
||||||
if (nivel === 2) return 'Médio';
|
return `Legado (${nivel})`;
|
||||||
if (nivel === 3) return 'Baixo';
|
|
||||||
return `Nível ${nivel}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function obterCorCardNivel(nivel: number): string {
|
function obterCorCardNivel(nivel: number): string {
|
||||||
if (nivel === 0) return 'border-l-4 border-error';
|
if (nivel === 0) return 'border-l-4 border-error';
|
||||||
if (nivel === 1) return 'border-l-4 border-warning';
|
if (nivel === 1) return 'border-l-4 border-warning';
|
||||||
if (nivel === 2) return 'border-l-4 border-info';
|
|
||||||
return 'border-l-4 border-base-300';
|
return 'border-l-4 border-base-300';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +138,7 @@
|
|||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ProtectedRoute allowedRoles={['ti_master', 'admin', 'ti_usuario']} maxLevel={3}>
|
<ProtectedRoute allowedRoles={['ti_master', 'admin', 'ti_usuario']} maxLevel={1}>
|
||||||
<div class="container mx-auto max-w-7xl px-4 py-6">
|
<div class="container mx-auto max-w-7xl px-4 py-6">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="mb-8 flex items-center justify-between">
|
<div class="mb-8 flex items-center justify-between">
|
||||||
@@ -176,31 +170,24 @@
|
|||||||
|
|
||||||
<!-- Estatísticas -->
|
<!-- Estatísticas -->
|
||||||
{#if stats}
|
{#if stats}
|
||||||
<div class="mb-8 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-5">
|
<div class="mb-8 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
<StatsCard title="Total de Perfis" value={stats.total} Icon={Users} color="primary" />
|
<StatsCard title="Total de Perfis" value={stats.total} Icon={Users} color="primary" />
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Nível Máximo"
|
title="Nível Máximo (0)"
|
||||||
value={stats.nivelMaximo}
|
value={stats.nivelMaximo}
|
||||||
description="Acesso total"
|
description="Acesso total ao sistema"
|
||||||
Icon={Shield}
|
Icon={Shield}
|
||||||
color="error"
|
color="error"
|
||||||
/>
|
/>
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Nível Alto"
|
title="Nível Administrativo (1)"
|
||||||
value={stats.nivelAlto}
|
value={stats.nivelAdministrativo}
|
||||||
description="Acesso elevado"
|
description="Perfis administrativos com acesso total"
|
||||||
Icon={AlertTriangle}
|
Icon={AlertTriangle}
|
||||||
color="warning"
|
color="warning"
|
||||||
/>
|
/>
|
||||||
<StatsCard
|
<StatsCard
|
||||||
title="Nível Médio"
|
title="Perfis com Setor"
|
||||||
value={stats.nivelMedio}
|
|
||||||
description="Acesso padrão"
|
|
||||||
Icon={Info}
|
|
||||||
color="info"
|
|
||||||
/>
|
|
||||||
<StatsCard
|
|
||||||
title="Com Setor"
|
|
||||||
value={stats.comSetor}
|
value={stats.comSetor}
|
||||||
description={stats.total > 0
|
description={stats.total > 0
|
||||||
? ((stats.comSetor / stats.total) * 100).toFixed(0) + '% do total'
|
? ((stats.comSetor / stats.total) * 100).toFixed(0) + '% do total'
|
||||||
@@ -209,6 +196,18 @@
|
|||||||
color="secondary"
|
color="secondary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{#if stats.niveisLegado > 0}
|
||||||
|
<div class="alert alert-warning mb-6">
|
||||||
|
<Info class="h-5 w-5" />
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold">Perfis com níveis legados</h3>
|
||||||
|
<p class="text-sm">
|
||||||
|
Existem {stats.niveisLegado} perfis com nível acima de 1. Esses perfis continuarão
|
||||||
|
sendo tratados como nível 1 (administrativo) após a migração.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Filtros -->
|
<!-- Filtros -->
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ProtectedRoute allowedRoles={['admin', 'ti']} maxLevel={1}>
|
<ProtectedRoute allowedRoles={['ti_master', 'admin']} maxLevel={1}>
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="breadcrumbs mb-4 text-sm">
|
<div class="breadcrumbs mb-4 text-sm">
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
@@ -257,7 +257,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ProtectedRoute allowedRoles={['ti_master', 'admin', 'ti_usuario']} maxLevel={3}>
|
<ProtectedRoute allowedRoles={['ti_master', 'admin', 'ti_usuario']} maxLevel={1}>
|
||||||
<div class="container mx-auto max-w-7xl px-4 py-6">
|
<div class="container mx-auto max-w-7xl px-4 py-6">
|
||||||
<!-- Mensagem de Feedback -->
|
<!-- Mensagem de Feedback -->
|
||||||
{#if mensagem}
|
{#if mensagem}
|
||||||
|
|||||||
@@ -278,7 +278,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ProtectedRoute allowedRoles={['ti_master', 'admin', 'ti_usuario']} maxLevel={2}>
|
<ProtectedRoute allowedRoles={['ti_master', 'admin', 'ti_usuario']} maxLevel={1}>
|
||||||
<main class="container mx-auto max-w-7xl px-4 py-6">
|
<main class="container mx-auto max-w-7xl px-4 py-6">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="breadcrumbs mb-4 text-sm">
|
<div class="breadcrumbs mb-4 text-sm">
|
||||||
|
|||||||
@@ -535,7 +535,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ProtectedRoute allowedRoles={['ti_master', 'admin', 'ti_usuario']} maxLevel={3}>
|
<ProtectedRoute allowedRoles={['ti_master', 'admin', 'ti_usuario']} maxLevel={1}>
|
||||||
<div class="container mx-auto max-w-7xl px-4 py-6">
|
<div class="container mx-auto max-w-7xl px-4 py-6">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="mb-6 flex items-center justify-between">
|
<div class="mb-6 flex items-center justify-between">
|
||||||
|
|||||||
36
packages/backend/convex/_generated/api.d.ts
vendored
36
packages/backend/convex/_generated/api.d.ts
vendored
@@ -15,8 +15,8 @@ import type * as actions_smtp from "../actions/smtp.js";
|
|||||||
import type * as actions_utils_nodeCrypto from "../actions/utils/nodeCrypto.js";
|
import type * as actions_utils_nodeCrypto from "../actions/utils/nodeCrypto.js";
|
||||||
import type * as atestadosLicencas from "../atestadosLicencas.js";
|
import type * as atestadosLicencas from "../atestadosLicencas.js";
|
||||||
import type * as ausencias from "../ausencias.js";
|
import type * as ausencias from "../ausencias.js";
|
||||||
import type * as auth from "../auth.js";
|
|
||||||
import type * as auth_utils from "../auth/utils.js";
|
import type * as auth_utils from "../auth/utils.js";
|
||||||
|
import type * as auth from "../auth.js";
|
||||||
import type * as chat from "../chat.js";
|
import type * as chat from "../chat.js";
|
||||||
import type * as configuracaoEmail from "../configuracaoEmail.js";
|
import type * as configuracaoEmail from "../configuracaoEmail.js";
|
||||||
import type * as crons from "../crons.js";
|
import type * as crons from "../crons.js";
|
||||||
@@ -28,7 +28,6 @@ import type * as ferias from "../ferias.js";
|
|||||||
import type * as funcionarios from "../funcionarios.js";
|
import type * as funcionarios from "../funcionarios.js";
|
||||||
import type * as healthCheck from "../healthCheck.js";
|
import type * as healthCheck from "../healthCheck.js";
|
||||||
import type * as http from "../http.js";
|
import type * as http from "../http.js";
|
||||||
import type * as limparPerfisAntigos from "../limparPerfisAntigos.js";
|
|
||||||
import type * as logsAcesso from "../logsAcesso.js";
|
import type * as logsAcesso from "../logsAcesso.js";
|
||||||
import type * as logsAtividades from "../logsAtividades.js";
|
import type * as logsAtividades from "../logsAtividades.js";
|
||||||
import type * as logsLogin from "../logsLogin.js";
|
import type * as logsLogin from "../logsLogin.js";
|
||||||
@@ -54,6 +53,14 @@ import type {
|
|||||||
FunctionReference,
|
FunctionReference,
|
||||||
} from "convex/server";
|
} from "convex/server";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility for referencing Convex functions in your app's API.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```js
|
||||||
|
* const myFunctionReference = api.myModule.myFunction;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
declare const fullApi: ApiFromModules<{
|
declare const fullApi: ApiFromModules<{
|
||||||
"actions/email": typeof actions_email;
|
"actions/email": typeof actions_email;
|
||||||
"actions/linkPreview": typeof actions_linkPreview;
|
"actions/linkPreview": typeof actions_linkPreview;
|
||||||
@@ -62,8 +69,8 @@ declare const fullApi: ApiFromModules<{
|
|||||||
"actions/utils/nodeCrypto": typeof actions_utils_nodeCrypto;
|
"actions/utils/nodeCrypto": typeof actions_utils_nodeCrypto;
|
||||||
atestadosLicencas: typeof atestadosLicencas;
|
atestadosLicencas: typeof atestadosLicencas;
|
||||||
ausencias: typeof ausencias;
|
ausencias: typeof ausencias;
|
||||||
auth: typeof auth;
|
|
||||||
"auth/utils": typeof auth_utils;
|
"auth/utils": typeof auth_utils;
|
||||||
|
auth: typeof auth;
|
||||||
chat: typeof chat;
|
chat: typeof chat;
|
||||||
configuracaoEmail: typeof configuracaoEmail;
|
configuracaoEmail: typeof configuracaoEmail;
|
||||||
crons: typeof crons;
|
crons: typeof crons;
|
||||||
@@ -75,7 +82,6 @@ declare const fullApi: ApiFromModules<{
|
|||||||
funcionarios: typeof funcionarios;
|
funcionarios: typeof funcionarios;
|
||||||
healthCheck: typeof healthCheck;
|
healthCheck: typeof healthCheck;
|
||||||
http: typeof http;
|
http: typeof http;
|
||||||
limparPerfisAntigos: typeof limparPerfisAntigos;
|
|
||||||
logsAcesso: typeof logsAcesso;
|
logsAcesso: typeof logsAcesso;
|
||||||
logsAtividades: typeof logsAtividades;
|
logsAtividades: typeof logsAtividades;
|
||||||
logsLogin: typeof logsLogin;
|
logsLogin: typeof logsLogin;
|
||||||
@@ -95,30 +101,14 @@ declare const fullApi: ApiFromModules<{
|
|||||||
"utils/getClientIP": typeof utils_getClientIP;
|
"utils/getClientIP": typeof utils_getClientIP;
|
||||||
verificarMatriculas: typeof verificarMatriculas;
|
verificarMatriculas: typeof verificarMatriculas;
|
||||||
}>;
|
}>;
|
||||||
|
declare const fullApiWithMounts: typeof fullApi;
|
||||||
|
|
||||||
/**
|
|
||||||
* A utility for referencing Convex functions in your app's public API.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* ```js
|
|
||||||
* const myFunctionReference = api.myModule.myFunction;
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export declare const api: FilterApi<
|
export declare const api: FilterApi<
|
||||||
typeof fullApi,
|
typeof fullApiWithMounts,
|
||||||
FunctionReference<any, "public">
|
FunctionReference<any, "public">
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
|
||||||
* A utility for referencing Convex functions in your app's internal API.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* ```js
|
|
||||||
* const myFunctionReference = internal.myModule.myFunction;
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export declare const internal: FilterApi<
|
export declare const internal: FilterApi<
|
||||||
typeof fullApi,
|
typeof fullApiWithMounts,
|
||||||
FunctionReference<any, "internal">
|
FunctionReference<any, "internal">
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
|||||||
16
packages/backend/convex/_generated/server.d.ts
vendored
16
packages/backend/convex/_generated/server.d.ts
vendored
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ActionBuilder,
|
ActionBuilder,
|
||||||
|
AnyComponents,
|
||||||
HttpActionBuilder,
|
HttpActionBuilder,
|
||||||
MutationBuilder,
|
MutationBuilder,
|
||||||
QueryBuilder,
|
QueryBuilder,
|
||||||
@@ -18,9 +19,15 @@ import {
|
|||||||
GenericQueryCtx,
|
GenericQueryCtx,
|
||||||
GenericDatabaseReader,
|
GenericDatabaseReader,
|
||||||
GenericDatabaseWriter,
|
GenericDatabaseWriter,
|
||||||
|
FunctionReference,
|
||||||
} from "convex/server";
|
} from "convex/server";
|
||||||
import type { DataModel } from "./dataModel.js";
|
import type { DataModel } from "./dataModel.js";
|
||||||
|
|
||||||
|
type GenericCtx =
|
||||||
|
| GenericActionCtx<DataModel>
|
||||||
|
| GenericMutationCtx<DataModel>
|
||||||
|
| GenericQueryCtx<DataModel>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a query in this Convex app's public API.
|
* Define a query in this Convex app's public API.
|
||||||
*
|
*
|
||||||
@@ -85,12 +92,11 @@ export declare const internalAction: ActionBuilder<DataModel, "internal">;
|
|||||||
/**
|
/**
|
||||||
* Define an HTTP action.
|
* Define an HTTP action.
|
||||||
*
|
*
|
||||||
* The wrapped function will be used to respond to HTTP requests received
|
* This function will be used to respond to HTTP requests received by a Convex
|
||||||
* by a Convex deployment if the requests matches the path and method where
|
* deployment if the requests matches the path and method where this action
|
||||||
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
* is routed. Be sure to route your action in `convex/http.js`.
|
||||||
*
|
*
|
||||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||||
* and a Fetch API `Request` object as its second.
|
|
||||||
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||||
*/
|
*/
|
||||||
export declare const httpAction: HttpActionBuilder;
|
export declare const httpAction: HttpActionBuilder;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
internalActionGeneric,
|
internalActionGeneric,
|
||||||
internalMutationGeneric,
|
internalMutationGeneric,
|
||||||
internalQueryGeneric,
|
internalQueryGeneric,
|
||||||
|
componentsGeneric,
|
||||||
} from "convex/server";
|
} from "convex/server";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,14 +81,10 @@ export const action = actionGeneric;
|
|||||||
export const internalAction = internalActionGeneric;
|
export const internalAction = internalActionGeneric;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define an HTTP action.
|
* Define a Convex HTTP action.
|
||||||
*
|
*
|
||||||
* The wrapped function will be used to respond to HTTP requests received
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object
|
||||||
* by a Convex deployment if the requests matches the path and method where
|
* as its second.
|
||||||
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
* @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`.
|
||||||
*
|
|
||||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument
|
|
||||||
* and a Fetch API `Request` object as its second.
|
|
||||||
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
|
||||||
*/
|
*/
|
||||||
export const httpAction = httpActionGeneric;
|
export const httpAction = httpActionGeneric;
|
||||||
|
|||||||
@@ -1,285 +0,0 @@
|
|||||||
import { internalMutation, query } from "./_generated/server";
|
|
||||||
import { v } from "convex/values";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listar todos os perfis (roles) do sistema
|
|
||||||
*/
|
|
||||||
export const listarTodosRoles = query({
|
|
||||||
args: {},
|
|
||||||
returns: v.array(
|
|
||||||
v.object({
|
|
||||||
_id: v.id("roles"),
|
|
||||||
nome: v.string(),
|
|
||||||
descricao: v.string(),
|
|
||||||
nivel: v.number(),
|
|
||||||
setor: v.optional(v.string()),
|
|
||||||
customizado: v.optional(v.boolean()),
|
|
||||||
editavel: v.optional(v.boolean()),
|
|
||||||
_creationTime: v.number(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
handler: async (ctx) => {
|
|
||||||
const roles = await ctx.db.query("roles").collect();
|
|
||||||
return roles.map((role) => ({
|
|
||||||
_id: role._id,
|
|
||||||
nome: role.nome,
|
|
||||||
descricao: role.descricao,
|
|
||||||
nivel: role.nivel,
|
|
||||||
setor: role.setor,
|
|
||||||
customizado: role.customizado,
|
|
||||||
editavel: role.editavel,
|
|
||||||
_creationTime: role._creationTime,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Limpar perfis antigos/duplicados
|
|
||||||
*
|
|
||||||
* CRITÉRIOS:
|
|
||||||
* - Manter apenas: ti_master (nível 0), admin (nível 2), ti_usuario (nível 2)
|
|
||||||
* - Remover: admin antigo (nível 0), ti genérico (nível 1), outros duplicados
|
|
||||||
*/
|
|
||||||
export const limparPerfisAntigos = internalMutation({
|
|
||||||
args: {},
|
|
||||||
returns: v.object({
|
|
||||||
removidos: v.array(
|
|
||||||
v.object({
|
|
||||||
nome: v.string(),
|
|
||||||
descricao: v.string(),
|
|
||||||
nivel: v.number(),
|
|
||||||
motivo: v.string(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
mantidos: v.array(
|
|
||||||
v.object({
|
|
||||||
nome: v.string(),
|
|
||||||
descricao: v.string(),
|
|
||||||
nivel: v.number(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
handler: async (ctx) => {
|
|
||||||
const roles = await ctx.db.query("roles").collect();
|
|
||||||
|
|
||||||
const removidos: Array<{
|
|
||||||
nome: string;
|
|
||||||
descricao: string;
|
|
||||||
nivel: number;
|
|
||||||
motivo: string;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
const mantidos: Array<{
|
|
||||||
nome: string;
|
|
||||||
descricao: string;
|
|
||||||
nivel: number;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
// Perfis que devem ser mantidos (apenas 1 de cada)
|
|
||||||
const perfisCorretos = new Map<string, boolean>();
|
|
||||||
perfisCorretos.set("ti_master", false);
|
|
||||||
perfisCorretos.set("admin", false);
|
|
||||||
perfisCorretos.set("ti_usuario", false);
|
|
||||||
|
|
||||||
for (const role of roles) {
|
|
||||||
let deveManter = false;
|
|
||||||
let motivo = "";
|
|
||||||
|
|
||||||
// TI_MASTER - Manter apenas o de nível 0
|
|
||||||
if (role.nome === "ti_master") {
|
|
||||||
if (role.nivel === 0 && !perfisCorretos.get("ti_master")) {
|
|
||||||
deveManter = true;
|
|
||||||
perfisCorretos.set("ti_master", true);
|
|
||||||
} else {
|
|
||||||
motivo =
|
|
||||||
role.nivel !== 0
|
|
||||||
? "TI_MASTER deve ser nível 0, este é nível " + role.nivel
|
|
||||||
: "TI_MASTER duplicado";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ADMIN - Manter apenas o de nível 2
|
|
||||||
else if (role.nome === "admin") {
|
|
||||||
if (role.nivel === 2 && !perfisCorretos.get("admin")) {
|
|
||||||
deveManter = true;
|
|
||||||
perfisCorretos.set("admin", true);
|
|
||||||
} else {
|
|
||||||
motivo =
|
|
||||||
role.nivel !== 2
|
|
||||||
? "ADMIN deve ser nível 2, este é nível " + role.nivel
|
|
||||||
: "ADMIN duplicado";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TI_USUARIO - Manter apenas o de nível 2
|
|
||||||
else if (role.nome === "ti_usuario") {
|
|
||||||
if (role.nivel === 2 && !perfisCorretos.get("ti_usuario")) {
|
|
||||||
deveManter = true;
|
|
||||||
perfisCorretos.set("ti_usuario", true);
|
|
||||||
} else {
|
|
||||||
motivo =
|
|
||||||
role.nivel !== 2
|
|
||||||
? "TI_USUARIO deve ser nível 2, este é nível " + role.nivel
|
|
||||||
: "TI_USUARIO duplicado";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Perfis genéricos antigos (remover)
|
|
||||||
else if (role.nome === "ti") {
|
|
||||||
motivo =
|
|
||||||
"Perfil genérico 'ti' obsoleto - usar 'ti_master' ou 'ti_usuario'";
|
|
||||||
}
|
|
||||||
// Outros perfis específicos de setores (manter se forem nível >= 2)
|
|
||||||
else if (
|
|
||||||
role.nome === "rh" ||
|
|
||||||
role.nome === "financeiro" ||
|
|
||||||
role.nome === "controladoria" ||
|
|
||||||
role.nome === "licitacoes" ||
|
|
||||||
role.nome === "compras" ||
|
|
||||||
role.nome === "juridico" ||
|
|
||||||
role.nome === "comunicacao" ||
|
|
||||||
role.nome === "programas_esportivos" ||
|
|
||||||
role.nome === "secretaria_executiva" ||
|
|
||||||
role.nome === "gestao_pessoas" ||
|
|
||||||
role.nome === "usuario"
|
|
||||||
) {
|
|
||||||
if (role.nivel >= 2) {
|
|
||||||
deveManter = true;
|
|
||||||
} else {
|
|
||||||
motivo = `Perfil de setor com nível incorreto (${role.nivel}), deveria ser >= 2`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Perfis customizados (manter sempre)
|
|
||||||
else if (role.customizado) {
|
|
||||||
deveManter = true;
|
|
||||||
}
|
|
||||||
// Outros perfis desconhecidos
|
|
||||||
else {
|
|
||||||
motivo = "Perfil desconhecido ou obsoleto";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deveManter) {
|
|
||||||
mantidos.push({
|
|
||||||
nome: role.nome,
|
|
||||||
descricao: role.descricao,
|
|
||||||
nivel: role.nivel,
|
|
||||||
});
|
|
||||||
console.log(
|
|
||||||
`✅ MANTIDO: ${role.nome} (${role.descricao}) - Nível ${role.nivel}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Verificar se há usuários usando este perfil
|
|
||||||
const usuariosComRole = await ctx.db
|
|
||||||
.query("usuarios")
|
|
||||||
.withIndex("by_role", (q) => q.eq("roleId", role._id))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if (usuariosComRole.length > 0) {
|
|
||||||
console.log(
|
|
||||||
`⚠️ AVISO: Não é possível remover "${role.nome}" porque ${usuariosComRole.length} usuário(s) ainda usa(m) este perfil`
|
|
||||||
);
|
|
||||||
mantidos.push({
|
|
||||||
nome: role.nome,
|
|
||||||
descricao: role.descricao,
|
|
||||||
nivel: role.nivel,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Remover permissões associadas
|
|
||||||
const permissoes = await ctx.db
|
|
||||||
.query("rolePermissoes")
|
|
||||||
.withIndex("by_role", (q) => q.eq("roleId", role._id))
|
|
||||||
.collect();
|
|
||||||
for (const perm of permissoes) {
|
|
||||||
await ctx.db.delete(perm._id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remover o role
|
|
||||||
await ctx.db.delete(role._id);
|
|
||||||
|
|
||||||
removidos.push({
|
|
||||||
nome: role.nome,
|
|
||||||
descricao: role.descricao,
|
|
||||||
nivel: role.nivel,
|
|
||||||
motivo: motivo || "Não especificado",
|
|
||||||
});
|
|
||||||
console.log(
|
|
||||||
`🗑️ REMOVIDO: ${role.nome} (${role.descricao}) - Nível ${role.nivel} - Motivo: ${motivo}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { removidos, mantidos };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verificar se existem perfis com níveis incorretos
|
|
||||||
*/
|
|
||||||
export const verificarNiveisIncorretos = query({
|
|
||||||
args: {},
|
|
||||||
returns: v.array(
|
|
||||||
v.object({
|
|
||||||
nome: v.string(),
|
|
||||||
descricao: v.string(),
|
|
||||||
nivelAtual: v.number(),
|
|
||||||
nivelCorreto: v.number(),
|
|
||||||
problema: v.string(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
handler: async (ctx) => {
|
|
||||||
const roles = await ctx.db.query("roles").collect();
|
|
||||||
const problemas: Array<{
|
|
||||||
nome: string;
|
|
||||||
descricao: string;
|
|
||||||
nivelAtual: number;
|
|
||||||
nivelCorreto: number;
|
|
||||||
problema: string;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
for (const role of roles) {
|
|
||||||
// TI_MASTER deve ser nível 0
|
|
||||||
if (role.nome === "ti_master" && role.nivel !== 0) {
|
|
||||||
problemas.push({
|
|
||||||
nome: role.nome,
|
|
||||||
descricao: role.descricao,
|
|
||||||
nivelAtual: role.nivel,
|
|
||||||
nivelCorreto: 0,
|
|
||||||
problema: "TI_MASTER deve ter acesso total (nível 0)",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ADMIN deve ser nível 2
|
|
||||||
if (role.nome === "admin" && role.nivel !== 2) {
|
|
||||||
problemas.push({
|
|
||||||
nome: role.nome,
|
|
||||||
descricao: role.descricao,
|
|
||||||
nivelAtual: role.nivel,
|
|
||||||
nivelCorreto: 2,
|
|
||||||
problema: "ADMIN deve ser editável (nível 2)",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TI_USUARIO deve ser nível 2
|
|
||||||
if (role.nome === "ti_usuario" && role.nivel !== 2) {
|
|
||||||
problemas.push({
|
|
||||||
nome: role.nome,
|
|
||||||
descricao: role.descricao,
|
|
||||||
nivelAtual: role.nivel,
|
|
||||||
nivelCorreto: 2,
|
|
||||||
problema: "TI_USUARIO deve ser editável (nível 2)",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perfil genérico "ti" não deveria existir
|
|
||||||
if (role.nome === "ti") {
|
|
||||||
problemas.push({
|
|
||||||
nome: role.nome,
|
|
||||||
descricao: role.descricao,
|
|
||||||
nivelAtual: role.nivel,
|
|
||||||
nivelCorreto: -1, // Indica que deve ser removido
|
|
||||||
problema: "Perfil genérico obsoleto - usar ti_master ou ti_usuario",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return problemas;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -3,27 +3,271 @@ import { v } from 'convex/values';
|
|||||||
import type { Doc } from './_generated/dataModel';
|
import type { Doc } from './_generated/dataModel';
|
||||||
import { getCurrentUserFunction } from './auth';
|
import { getCurrentUserFunction } from './auth';
|
||||||
|
|
||||||
// Catálogo base de recursos e ações
|
// Catálogo de permissões base para seed controlado via mutation
|
||||||
// Ajuste/expanda conforme os módulos disponíveis no sistema
|
const PERMISSOES_BASE = {
|
||||||
export const CATALOGO_RECURSOS = [
|
permissoes: [
|
||||||
|
// Funcionários
|
||||||
{
|
{
|
||||||
|
nome: 'funcionarios.dashboard',
|
||||||
recurso: 'funcionarios',
|
recurso: 'funcionarios',
|
||||||
acoes: [
|
acao: 'dashboard',
|
||||||
'dashboard',
|
descricao: 'Acessar o painel de funcionários'
|
||||||
'ver',
|
|
||||||
'listar',
|
|
||||||
'criar',
|
|
||||||
'editar',
|
|
||||||
'excluir',
|
|
||||||
'aprovar_ausencias',
|
|
||||||
'aprovar_ferias'
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
nome: 'funcionarios.ver',
|
||||||
|
recurso: 'funcionarios',
|
||||||
|
acao: 'ver',
|
||||||
|
descricao: 'Visualizar detalhes de funcionários'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: 'funcionarios.listar',
|
||||||
|
recurso: 'funcionarios',
|
||||||
|
acao: 'listar',
|
||||||
|
descricao: 'Listar funcionários'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: 'funcionarios.criar',
|
||||||
|
recurso: 'funcionarios',
|
||||||
|
acao: 'criar',
|
||||||
|
descricao: 'Criar novos funcionários'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: 'funcionarios.editar',
|
||||||
|
recurso: 'funcionarios',
|
||||||
|
acao: 'editar',
|
||||||
|
descricao: 'Editar dados de funcionários'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: 'funcionarios.excluir',
|
||||||
|
recurso: 'funcionarios',
|
||||||
|
acao: 'excluir',
|
||||||
|
descricao: 'Excluir funcionários'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: 'funcionarios.aprovar_ausencias',
|
||||||
|
recurso: 'funcionarios',
|
||||||
|
acao: 'aprovar_ausencias',
|
||||||
|
descricao: 'Aprovar ausências de funcionários'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: 'funcionarios.aprovar_ferias',
|
||||||
|
recurso: 'funcionarios',
|
||||||
|
acao: 'aprovar_ferias',
|
||||||
|
descricao: 'Aprovar férias de funcionários'
|
||||||
|
},
|
||||||
|
// Símbolos
|
||||||
|
{
|
||||||
|
nome: 'simbolos.dashboard',
|
||||||
recurso: 'simbolos',
|
recurso: 'simbolos',
|
||||||
acoes: ['dashboard', 'ver', 'listar', 'criar', 'editar', 'excluir']
|
acao: 'dashboard',
|
||||||
|
descricao: 'Acessar o painel de símbolos'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: 'simbolos.ver',
|
||||||
|
recurso: 'simbolos',
|
||||||
|
acao: 'ver',
|
||||||
|
descricao: 'Visualizar detalhes de símbolos'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: 'simbolos.listar',
|
||||||
|
recurso: 'simbolos',
|
||||||
|
acao: 'listar',
|
||||||
|
descricao: 'Listar símbolos'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: 'simbolos.criar',
|
||||||
|
recurso: 'simbolos',
|
||||||
|
acao: 'criar',
|
||||||
|
descricao: 'Criar novos símbolos'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: 'simbolos.editar',
|
||||||
|
recurso: 'simbolos',
|
||||||
|
acao: 'editar',
|
||||||
|
descricao: 'Editar símbolos'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: 'simbolos.excluir',
|
||||||
|
recurso: 'simbolos',
|
||||||
|
acao: 'excluir',
|
||||||
|
descricao: 'Excluir símbolos'
|
||||||
|
},
|
||||||
|
// TI - Usuários
|
||||||
|
{
|
||||||
|
nome: 'ti_usuarios.listar',
|
||||||
|
recurso: 'ti_usuarios',
|
||||||
|
acao: 'listar',
|
||||||
|
descricao: 'Listar usuários do sistema'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: 'ti_usuarios.criar',
|
||||||
|
recurso: 'ti_usuarios',
|
||||||
|
acao: 'criar',
|
||||||
|
descricao: 'Criar novos usuários de acesso'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: 'ti_usuarios.editar',
|
||||||
|
recurso: 'ti_usuarios',
|
||||||
|
acao: 'editar',
|
||||||
|
descricao: 'Editar usuários de acesso'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: 'ti_usuarios.bloquear',
|
||||||
|
recurso: 'ti_usuarios',
|
||||||
|
acao: 'bloquear',
|
||||||
|
descricao: 'Bloquear ou desbloquear usuários'
|
||||||
|
},
|
||||||
|
// TI - Perfis
|
||||||
|
{
|
||||||
|
nome: 'ti_perfis.listar',
|
||||||
|
recurso: 'ti_perfis',
|
||||||
|
acao: 'listar',
|
||||||
|
descricao: 'Listar perfis de acesso'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: 'ti_perfis.criar',
|
||||||
|
recurso: 'ti_perfis',
|
||||||
|
acao: 'criar',
|
||||||
|
descricao: 'Criar novos perfis de acesso'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: 'ti_perfis.editar',
|
||||||
|
recurso: 'ti_perfis',
|
||||||
|
acao: 'editar',
|
||||||
|
descricao: 'Editar perfis de acesso'
|
||||||
|
},
|
||||||
|
// TI - Painel de Permissões
|
||||||
|
{
|
||||||
|
nome: 'ti_painel_permissoes.gerenciar',
|
||||||
|
recurso: 'ti_painel_permissoes',
|
||||||
|
acao: 'gerenciar',
|
||||||
|
descricao: 'Gerenciar matriz de permissões por perfil'
|
||||||
|
},
|
||||||
|
// TI - Solicitações de Acesso
|
||||||
|
{
|
||||||
|
nome: 'ti_solicitacoes_acesso.ver',
|
||||||
|
recurso: 'ti_solicitacoes_acesso',
|
||||||
|
acao: 'ver',
|
||||||
|
descricao: 'Visualizar solicitações de acesso'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: 'ti_solicitacoes_acesso.aprovar',
|
||||||
|
recurso: 'ti_solicitacoes_acesso',
|
||||||
|
acao: 'aprovar',
|
||||||
|
descricao: 'Aprovar solicitações de acesso'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: 'ti_solicitacoes_acesso.reprovar',
|
||||||
|
recurso: 'ti_solicitacoes_acesso',
|
||||||
|
acao: 'reprovar',
|
||||||
|
descricao: 'Reprovar solicitações de acesso'
|
||||||
|
},
|
||||||
|
// TI - Configurações de E-mail
|
||||||
|
{
|
||||||
|
nome: 'ti_configuracoes_email.configurar',
|
||||||
|
recurso: 'ti_configuracoes_email',
|
||||||
|
acao: 'configurar',
|
||||||
|
descricao: 'Configurar parâmetros de envio de e-mail'
|
||||||
|
},
|
||||||
|
// TI - Monitoramento
|
||||||
|
{
|
||||||
|
nome: 'ti_monitoramento.ver',
|
||||||
|
recurso: 'ti_monitoramento',
|
||||||
|
acao: 'ver',
|
||||||
|
descricao: 'Acessar painel de monitoramento geral'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nome: 'ti_monitoramento_emails.ver',
|
||||||
|
recurso: 'ti_monitoramento_emails',
|
||||||
|
acao: 'ver',
|
||||||
|
descricao: 'Acessar monitoramento de envio de e-mails'
|
||||||
|
},
|
||||||
|
// TI - Notificações
|
||||||
|
{
|
||||||
|
nome: 'ti_notificacoes.configurar',
|
||||||
|
recurso: 'ti_notificacoes',
|
||||||
|
acao: 'configurar',
|
||||||
|
descricao: 'Configurar notificações do sistema'
|
||||||
|
},
|
||||||
|
// TI - Times
|
||||||
|
{
|
||||||
|
nome: 'ti_times.gerenciar',
|
||||||
|
recurso: 'ti_times',
|
||||||
|
acao: 'gerenciar',
|
||||||
|
descricao: 'Gerenciar times/equipes de TI'
|
||||||
|
},
|
||||||
|
// TI - Painel Administrativo
|
||||||
|
{
|
||||||
|
nome: 'ti_painel_administrativo.ver',
|
||||||
|
recurso: 'ti_painel_administrativo',
|
||||||
|
acao: 'ver',
|
||||||
|
descricao: 'Acessar painel administrativo de TI'
|
||||||
|
},
|
||||||
|
// Financeiro
|
||||||
|
{
|
||||||
|
nome: 'financeiro.ver',
|
||||||
|
recurso: 'financeiro',
|
||||||
|
acao: 'ver',
|
||||||
|
descricao: 'Acessar telas do módulo de financeiro'
|
||||||
|
},
|
||||||
|
// Controladoria
|
||||||
|
{
|
||||||
|
nome: 'controladoria.ver',
|
||||||
|
recurso: 'controladoria',
|
||||||
|
acao: 'ver',
|
||||||
|
descricao: 'Acessar telas do módulo de controladoria'
|
||||||
|
},
|
||||||
|
// Licitações
|
||||||
|
{
|
||||||
|
nome: 'licitacoes.ver',
|
||||||
|
recurso: 'licitacoes',
|
||||||
|
acao: 'ver',
|
||||||
|
descricao: 'Acessar telas do módulo de licitações'
|
||||||
|
},
|
||||||
|
// Compras
|
||||||
|
{
|
||||||
|
nome: 'compras.ver',
|
||||||
|
recurso: 'compras',
|
||||||
|
acao: 'ver',
|
||||||
|
descricao: 'Acessar telas do módulo de compras'
|
||||||
|
},
|
||||||
|
// Jurídico
|
||||||
|
{
|
||||||
|
nome: 'juridico.ver',
|
||||||
|
recurso: 'juridico',
|
||||||
|
acao: 'ver',
|
||||||
|
descricao: 'Acessar telas do módulo jurídico'
|
||||||
|
},
|
||||||
|
// Comunicação
|
||||||
|
{
|
||||||
|
nome: 'comunicacao.ver',
|
||||||
|
recurso: 'comunicacao',
|
||||||
|
acao: 'ver',
|
||||||
|
descricao: 'Acessar telas do módulo de comunicação'
|
||||||
|
},
|
||||||
|
// Programas Esportivos
|
||||||
|
{
|
||||||
|
nome: 'programas_esportivos.ver',
|
||||||
|
recurso: 'programas_esportivos',
|
||||||
|
acao: 'ver',
|
||||||
|
descricao: 'Acessar telas do módulo de programas esportivos'
|
||||||
|
},
|
||||||
|
// Secretaria Executiva
|
||||||
|
{
|
||||||
|
nome: 'secretaria_executiva.ver',
|
||||||
|
recurso: 'secretaria_executiva',
|
||||||
|
acao: 'ver',
|
||||||
|
descricao: 'Acessar telas do módulo de secretaria executiva'
|
||||||
|
},
|
||||||
|
// Gestão de Pessoas
|
||||||
|
{
|
||||||
|
nome: 'gestao_pessoas.ver',
|
||||||
|
recurso: 'gestao_pessoas',
|
||||||
|
acao: 'ver',
|
||||||
|
descricao: 'Acessar telas do módulo de gestão de pessoas'
|
||||||
}
|
}
|
||||||
] as const;
|
]
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const listarRecursosEAcoes = query({
|
export const listarRecursosEAcoes = query({
|
||||||
args: {},
|
args: {},
|
||||||
@@ -33,10 +277,18 @@ export const listarRecursosEAcoes = query({
|
|||||||
acoes: v.array(v.string())
|
acoes: v.array(v.string())
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
handler: async () => {
|
handler: async (ctx) => {
|
||||||
return CATALOGO_RECURSOS.map((r) => ({
|
const permissoes = await ctx.db.query('permissoes').collect();
|
||||||
recurso: r.recurso,
|
|
||||||
acoes: [...r.acoes]
|
const recursos: Record<string, Set<string>> = {};
|
||||||
|
for (const perm of permissoes) {
|
||||||
|
const set = (recursos[perm.recurso] ||= new Set<string>());
|
||||||
|
set.add(perm.acao);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(recursos).map(([recurso, acoes]) => ({
|
||||||
|
recurso,
|
||||||
|
acoes: Array.from(acoes).sort()
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -56,7 +308,7 @@ export const listarPermissoesAcoesPorRole = query({
|
|||||||
.withIndex('by_role', (q) => q.eq('roleId', args.roleId))
|
.withIndex('by_role', (q) => q.eq('roleId', args.roleId))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Carregar documentos de permissões
|
// Carregar documentos de permissões vinculadas a este role
|
||||||
const actionsByResource: Record<string, Set<string>> = {};
|
const actionsByResource: Record<string, Set<string>> = {};
|
||||||
for (const rp of rolePerms) {
|
for (const rp of rolePerms) {
|
||||||
const perm = await ctx.db.get(rp.permissaoId);
|
const perm = await ctx.db.get(rp.permissaoId);
|
||||||
@@ -65,13 +317,10 @@ export const listarPermissoesAcoesPorRole = query({
|
|||||||
set.add(perm.acao);
|
set.add(perm.acao);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalizar para todos os recursos do catálogo
|
return Object.entries(actionsByResource).map(([recurso, acoes]) => ({
|
||||||
const result: Array<{ recurso: string; acoes: Array<string> }> = [];
|
recurso,
|
||||||
for (const item of CATALOGO_RECURSOS) {
|
acoes: Array.from(acoes).sort()
|
||||||
const granted = Array.from(actionsByResource[item.recurso] ?? new Set<string>());
|
}));
|
||||||
result.push({ recurso: item.recurso, acoes: granted });
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,24 +333,12 @@ export const atualizarPermissaoAcao = mutation({
|
|||||||
},
|
},
|
||||||
returns: v.null(),
|
returns: v.null(),
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
// Garantir documento de permissão (recurso+acao)
|
// Buscar documento de permissão (recurso+acao)
|
||||||
let permissao = await ctx.db
|
const permissao = await ctx.db
|
||||||
.query('permissoes')
|
.query('permissoes')
|
||||||
.withIndex('by_recurso_e_acao', (q) => q.eq('recurso', args.recurso).eq('acao', args.acao))
|
.withIndex('by_recurso_e_acao', (q) => q.eq('recurso', args.recurso).eq('acao', args.acao))
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (!permissao) {
|
|
||||||
const nome = `${args.recurso}.${args.acao}`;
|
|
||||||
const descricao = `Permite ${args.acao} em ${args.recurso}`;
|
|
||||||
const id = await ctx.db.insert('permissoes', {
|
|
||||||
nome,
|
|
||||||
descricao,
|
|
||||||
recurso: args.recurso,
|
|
||||||
acao: args.acao
|
|
||||||
});
|
|
||||||
permissao = await ctx.db.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!permissao) return null;
|
if (!permissao) return null;
|
||||||
|
|
||||||
// Verificar vínculo atual
|
// Verificar vínculo atual
|
||||||
@@ -128,6 +365,36 @@ export const atualizarPermissaoAcao = mutation({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const seedPermissoesBase = mutation({
|
||||||
|
args: {},
|
||||||
|
returns: v.null(),
|
||||||
|
handler: async (ctx) => {
|
||||||
|
console.log('🔐 Seed de permissões base...');
|
||||||
|
|
||||||
|
for (const perm of PERMISSOES_BASE.permissoes) {
|
||||||
|
const existente = await ctx.db
|
||||||
|
.query('permissoes')
|
||||||
|
.withIndex('by_nome', (q) => q.eq('nome', perm.nome))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (existente) {
|
||||||
|
console.log(` ℹ️ Permissão já existe: ${perm.nome}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.db.insert('permissoes', {
|
||||||
|
nome: perm.nome,
|
||||||
|
descricao: perm.descricao,
|
||||||
|
recurso: perm.recurso,
|
||||||
|
acao: perm.acao
|
||||||
|
});
|
||||||
|
console.log(` ✅ Permissão criada: ${perm.nome}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const verificarAcao = query({
|
export const verificarAcao = query({
|
||||||
args: {
|
args: {
|
||||||
usuarioId: v.id('usuarios'),
|
usuarioId: v.id('usuarios'),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { v } from 'convex/values';
|
import { v } from 'convex/values';
|
||||||
import { query, mutation } from './_generated/server';
|
import { internalMutation, query, mutation } from './_generated/server';
|
||||||
import type { Id } from './_generated/dataModel';
|
import type { Id } from './_generated/dataModel';
|
||||||
import { getCurrentUserFunction } from './auth';
|
import { getCurrentUserFunction } from './auth';
|
||||||
|
|
||||||
@@ -98,13 +98,16 @@ export const criar = mutation({
|
|||||||
permissoesParaCopiar = permissoesOrigem.map((item) => item.permissaoId);
|
permissoesParaCopiar = permissoesOrigem.map((item) => item.permissaoId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nivelAjustado = Math.min(Math.max(Math.round(args.nivel), 0), 10);
|
// Agora só existem níveis 0 e 1.
|
||||||
|
// 0 = máximo (acesso total), 1 = administrativo (também com acesso total).
|
||||||
|
// Qualquer valor informado diferente de 0 é normalizado para 1.
|
||||||
|
const nivelNormalizado = Math.round(args.nivel) <= 0 ? 0 : 1;
|
||||||
const setor = args.setor?.trim();
|
const setor = args.setor?.trim();
|
||||||
|
|
||||||
const roleId = await ctx.db.insert('roles', {
|
const roleId = await ctx.db.insert('roles', {
|
||||||
nome: nomeNormalizado,
|
nome: nomeNormalizado,
|
||||||
descricao: args.descricao.trim() || args.nome.trim(),
|
descricao: args.descricao.trim() || args.nome.trim(),
|
||||||
nivel: nivelAjustado,
|
nivel: nivelNormalizado,
|
||||||
setor: setor && setor.length > 0 ? setor : undefined,
|
setor: setor && setor.length > 0 ? setor : undefined,
|
||||||
customizado: true,
|
customizado: true,
|
||||||
criadoPor: usuarioAtual._id,
|
criadoPor: usuarioAtual._id,
|
||||||
@@ -123,3 +126,26 @@ export const criar = mutation({
|
|||||||
return { sucesso: true as const, roleId };
|
return { sucesso: true as const, roleId };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migração de níveis de roles para o novo modelo (apenas 0 e 1).
|
||||||
|
* - Mantém níveis 0 e 1 como estão.
|
||||||
|
* - Converte qualquer nível > 1 para 1.
|
||||||
|
*/
|
||||||
|
export const migrarNiveisRoles = internalMutation({
|
||||||
|
args: {},
|
||||||
|
returns: v.null(),
|
||||||
|
handler: async (ctx) => {
|
||||||
|
const roles = await ctx.db.query('roles').collect();
|
||||||
|
|
||||||
|
for (const role of roles) {
|
||||||
|
if (role.nivel <= 1) continue;
|
||||||
|
|
||||||
|
await ctx.db.patch(role._id, {
|
||||||
|
nivel: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user