feat: add Svelte DnD action and enhance flow management features

- Added "svelte-dnd-action" dependency to facilitate drag-and-drop functionality.
- Introduced new "Fluxos de Trabalho" section in the dashboard for managing workflow templates and instances.
- Updated permission handling for sectors and flow templates in the backend.
- Enhanced schema definitions to support flow templates, instances, and associated documents.
- Improved UI components to include new workflow management features across various dashboard pages.
This commit is contained in:
2025-11-25 00:21:35 -03:00
parent 409872352c
commit f8d9c17f63
16 changed files with 4073 additions and 5 deletions

View File

@@ -35,6 +35,7 @@ import type * as email from "../email.js";
import type * as empresas from "../empresas.js";
import type * as enderecosMarcacao from "../enderecosMarcacao.js";
import type * as ferias from "../ferias.js";
import type * as flows from "../flows.js";
import type * as funcionarioEnderecos from "../funcionarioEnderecos.js";
import type * as funcionarios from "../funcionarios.js";
import type * as healthCheck from "../healthCheck.js";
@@ -51,6 +52,7 @@ import type * as roles from "../roles.js";
import type * as saldoFerias from "../saldoFerias.js";
import type * as security from "../security.js";
import type * as seed from "../seed.js";
import type * as setores from "../setores.js";
import type * as simbolos from "../simbolos.js";
import type * as templatesMensagens from "../templatesMensagens.js";
import type * as times from "../times.js";
@@ -93,6 +95,7 @@ declare const fullApi: ApiFromModules<{
empresas: typeof empresas;
enderecosMarcacao: typeof enderecosMarcacao;
ferias: typeof ferias;
flows: typeof flows;
funcionarioEnderecos: typeof funcionarioEnderecos;
funcionarios: typeof funcionarios;
healthCheck: typeof healthCheck;
@@ -109,6 +112,7 @@ declare const fullApi: ApiFromModules<{
saldoFerias: typeof saldoFerias;
security: typeof security;
seed: typeof seed;
setores: typeof setores;
simbolos: typeof simbolos;
templatesMensagens: typeof templatesMensagens;
times: typeof times;

File diff suppressed because it is too large Load Diff

View File

@@ -295,6 +295,106 @@ const PERMISSOES_BASE = {
recurso: 'gestao_pessoas',
acao: 'ver',
descricao: 'Acessar telas do módulo de gestão de pessoas'
},
// Setores
{
nome: 'setores.listar',
recurso: 'setores',
acao: 'listar',
descricao: 'Listar setores'
},
{
nome: 'setores.criar',
recurso: 'setores',
acao: 'criar',
descricao: 'Criar novos setores'
},
{
nome: 'setores.editar',
recurso: 'setores',
acao: 'editar',
descricao: 'Editar setores'
},
{
nome: 'setores.excluir',
recurso: 'setores',
acao: 'excluir',
descricao: 'Excluir setores'
},
// Flow Templates
{
nome: 'fluxos.templates.listar',
recurso: 'fluxos_templates',
acao: 'listar',
descricao: 'Listar templates de fluxo'
},
{
nome: 'fluxos.templates.criar',
recurso: 'fluxos_templates',
acao: 'criar',
descricao: 'Criar templates de fluxo'
},
{
nome: 'fluxos.templates.editar',
recurso: 'fluxos_templates',
acao: 'editar',
descricao: 'Editar templates de fluxo'
},
{
nome: 'fluxos.templates.excluir',
recurso: 'fluxos_templates',
acao: 'excluir',
descricao: 'Excluir templates de fluxo'
},
// Flow Instances
{
nome: 'fluxos.instancias.listar',
recurso: 'fluxos_instancias',
acao: 'listar',
descricao: 'Listar instâncias de fluxo'
},
{
nome: 'fluxos.instancias.criar',
recurso: 'fluxos_instancias',
acao: 'criar',
descricao: 'Criar instâncias de fluxo'
},
{
nome: 'fluxos.instancias.ver',
recurso: 'fluxos_instancias',
acao: 'ver',
descricao: 'Visualizar detalhes de instâncias de fluxo'
},
{
nome: 'fluxos.instancias.atualizar_status',
recurso: 'fluxos_instancias',
acao: 'atualizar_status',
descricao: 'Atualizar status de instâncias de fluxo'
},
{
nome: 'fluxos.instancias.atribuir',
recurso: 'fluxos_instancias',
acao: 'atribuir',
descricao: 'Atribuir responsáveis em instâncias de fluxo'
},
// Flow Documents
{
nome: 'fluxos.documentos.listar',
recurso: 'fluxos_documentos',
acao: 'listar',
descricao: 'Listar documentos de fluxo'
},
{
nome: 'fluxos.documentos.upload',
recurso: 'fluxos_documentos',
acao: 'upload',
descricao: 'Fazer upload de documentos em fluxos'
},
{
nome: 'fluxos.documentos.excluir',
recurso: 'fluxos_documentos',
acao: 'excluir',
descricao: 'Excluir documentos de fluxos'
}
]
} as const;

View File

@@ -120,6 +120,31 @@ export const reportStatus = v.union(
v.literal("falhou")
);
// Status de templates de fluxo
export const flowTemplateStatus = v.union(
v.literal("draft"),
v.literal("published"),
v.literal("archived")
);
export type FlowTemplateStatus = Infer<typeof flowTemplateStatus>;
// Status de instâncias de fluxo
export const flowInstanceStatus = v.union(
v.literal("active"),
v.literal("completed"),
v.literal("cancelled")
);
export type FlowInstanceStatus = Infer<typeof flowInstanceStatus>;
// Status de passos de instância de fluxo
export const flowInstanceStepStatus = v.union(
v.literal("pending"),
v.literal("in_progress"),
v.literal("completed"),
v.literal("blocked")
);
export type FlowInstanceStepStatus = Infer<typeof flowInstanceStepStatus>;
export const situacaoContrato = v.union(
v.literal("em_execucao"),
v.literal("rescendido"),
@@ -128,6 +153,85 @@ export const situacaoContrato = v.union(
);
export default defineSchema({
// Setores da organização
setores: defineTable({
nome: v.string(),
sigla: v.string(),
criadoPor: v.id("usuarios"),
createdAt: v.number(),
})
.index("by_nome", ["nome"])
.index("by_sigla", ["sigla"]),
// Templates de fluxo
flowTemplates: defineTable({
name: v.string(),
description: v.optional(v.string()),
status: flowTemplateStatus,
createdBy: v.id("usuarios"),
createdAt: v.number(),
})
.index("by_status", ["status"])
.index("by_createdBy", ["createdBy"]),
// Passos de template de fluxo
flowSteps: defineTable({
flowTemplateId: v.id("flowTemplates"),
name: v.string(),
description: v.optional(v.string()),
position: v.number(),
expectedDuration: v.number(), // em dias
setorId: v.id("setores"),
defaultAssigneeId: v.optional(v.id("usuarios")),
requiredDocuments: v.optional(v.array(v.string())),
})
.index("by_flowTemplateId", ["flowTemplateId"])
.index("by_flowTemplateId_and_position", ["flowTemplateId", "position"]),
// Instâncias de fluxo
flowInstances: defineTable({
flowTemplateId: v.id("flowTemplates"),
targetType: v.string(), // ex: 'contrato', 'projeto'
targetId: v.string(), // ID genérico do alvo
managerId: v.id("usuarios"),
status: flowInstanceStatus,
startedAt: v.number(),
finishedAt: v.optional(v.number()),
currentStepId: v.optional(v.id("flowInstanceSteps")),
})
.index("by_flowTemplateId", ["flowTemplateId"])
.index("by_targetType_and_targetId", ["targetType", "targetId"])
.index("by_managerId", ["managerId"])
.index("by_status", ["status"]),
// Passos de instância de fluxo
flowInstanceSteps: defineTable({
flowInstanceId: v.id("flowInstances"),
flowStepId: v.id("flowSteps"),
setorId: v.id("setores"),
assignedToId: v.optional(v.id("usuarios")),
status: flowInstanceStepStatus,
startedAt: v.optional(v.number()),
finishedAt: v.optional(v.number()),
notes: v.optional(v.string()),
dueDate: v.optional(v.number()),
})
.index("by_flowInstanceId", ["flowInstanceId"])
.index("by_flowInstanceId_and_status", ["flowInstanceId", "status"])
.index("by_setorId", ["setorId"])
.index("by_assignedToId", ["assignedToId"]),
// Documentos de instância de fluxo
flowInstanceDocuments: defineTable({
flowInstanceStepId: v.id("flowInstanceSteps"),
uploadedById: v.id("usuarios"),
storageId: v.id("_storage"),
name: v.string(),
uploadedAt: v.number(),
})
.index("by_flowInstanceStepId", ["flowInstanceStepId"])
.index("by_uploadedById", ["uploadedById"]),
contratos: defineTable({
contratadaId: v.id("empresas"),
objeto: v.string(),
@@ -210,6 +314,7 @@ export default defineSchema({
simboloId: v.id("simbolos"),
simboloTipo: simboloTipo,
gestorId: v.optional(v.id("usuarios")),
setorId: v.optional(v.id("setores")), // Setor do funcionário
statusFerias: v.optional(
v.union(v.literal("ativo"), v.literal("em_ferias"))
),
@@ -349,7 +454,8 @@ export default defineSchema({
.index("by_simboloTipo", ["simboloTipo"])
.index("by_cpf", ["cpf"])
.index("by_rg", ["rg"])
.index("by_gestor", ["gestorId"]),
.index("by_gestor", ["gestorId"])
.index("by_setor", ["setorId"]),
atestados: defineTable({
funcionarioId: v.id("funcionarios"),

View File

@@ -0,0 +1,178 @@
import { query, mutation } from './_generated/server';
import { v } from 'convex/values';
import { getCurrentUserFunction } from './auth';
/**
* Listar todos os setores
*/
export const list = query({
args: {},
returns: v.array(
v.object({
_id: v.id('setores'),
_creationTime: v.number(),
nome: v.string(),
sigla: v.string(),
criadoPor: v.id('usuarios'),
createdAt: v.number()
})
),
handler: async (ctx) => {
const setores = await ctx.db.query('setores').order('asc').collect();
return setores;
}
});
/**
* Obter um setor pelo ID
*/
export const getById = query({
args: { id: v.id('setores') },
returns: v.union(
v.object({
_id: v.id('setores'),
_creationTime: v.number(),
nome: v.string(),
sigla: v.string(),
criadoPor: v.id('usuarios'),
createdAt: v.number()
}),
v.null()
),
handler: async (ctx, args) => {
const setor = await ctx.db.get(args.id);
return setor;
}
});
/**
* Criar um novo setor
*/
export const create = mutation({
args: {
nome: v.string(),
sigla: v.string()
},
returns: v.id('setores'),
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
throw new Error('Usuário não autenticado');
}
// Verificar se já existe setor com mesmo nome ou sigla
const existenteNome = await ctx.db
.query('setores')
.withIndex('by_nome', (q) => q.eq('nome', args.nome))
.first();
if (existenteNome) {
throw new Error('Já existe um setor com este nome');
}
const existenteSigla = await ctx.db
.query('setores')
.withIndex('by_sigla', (q) => q.eq('sigla', args.sigla))
.first();
if (existenteSigla) {
throw new Error('Já existe um setor com esta sigla');
}
const setorId = await ctx.db.insert('setores', {
nome: args.nome,
sigla: args.sigla.toUpperCase(),
criadoPor: usuario._id,
createdAt: Date.now()
});
return setorId;
}
});
/**
* Atualizar um setor existente
*/
export const update = mutation({
args: {
id: v.id('setores'),
nome: v.string(),
sigla: v.string()
},
returns: v.null(),
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
throw new Error('Usuário não autenticado');
}
const setor = await ctx.db.get(args.id);
if (!setor) {
throw new Error('Setor não encontrado');
}
// Verificar se já existe outro setor com mesmo nome
const existenteNome = await ctx.db
.query('setores')
.withIndex('by_nome', (q) => q.eq('nome', args.nome))
.first();
if (existenteNome && existenteNome._id !== args.id) {
throw new Error('Já existe um setor com este nome');
}
// Verificar se já existe outro setor com mesma sigla
const existenteSigla = await ctx.db
.query('setores')
.withIndex('by_sigla', (q) => q.eq('sigla', args.sigla))
.first();
if (existenteSigla && existenteSigla._id !== args.id) {
throw new Error('Já existe um setor com esta sigla');
}
await ctx.db.patch(args.id, {
nome: args.nome,
sigla: args.sigla.toUpperCase()
});
return null;
}
});
/**
* Excluir um setor
*/
export const remove = mutation({
args: { id: v.id('setores') },
returns: v.null(),
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
throw new Error('Usuário não autenticado');
}
const setor = await ctx.db.get(args.id);
if (!setor) {
throw new Error('Setor não encontrado');
}
// Verificar se há funcionários vinculados
const funcionariosVinculados = await ctx.db
.query('funcionarios')
.withIndex('by_setor', (q) => q.eq('setorId', args.id))
.first();
if (funcionariosVinculados) {
throw new Error('Não é possível excluir um setor com funcionários vinculados');
}
// Verificar se há passos de fluxo vinculados
const passosVinculados = await ctx.db
.query('flowSteps')
.collect();
const temPassosVinculados = passosVinculados.some((p) => p.setorId === args.id);
if (temPassosVinculados) {
throw new Error('Não é possível excluir um setor vinculado a passos de fluxo');
}
await ctx.db.delete(args.id);
return null;
}
});