feat: implement sub-steps management in workflow editor
- Added functionality for creating, updating, and deleting sub-steps within the workflow editor. - Introduced a modal for adding new sub-steps, including fields for name and description. - Enhanced the UI to display sub-steps with status indicators and options for updating their status. - Updated navigation links to reflect changes in the workflow structure, ensuring consistency across the application. - Refactored related components to accommodate the new sub-steps feature, improving overall workflow management.
This commit is contained in:
124
apps/web/src/lib/components/RelogioPrazo.svelte
Normal file
124
apps/web/src/lib/components/RelogioPrazo.svelte
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
const {
|
||||||
|
dueDate,
|
||||||
|
startedAt,
|
||||||
|
finishedAt,
|
||||||
|
status,
|
||||||
|
expectedDuration
|
||||||
|
} = $props<{
|
||||||
|
dueDate: number | undefined;
|
||||||
|
startedAt: number | undefined;
|
||||||
|
finishedAt: number | undefined;
|
||||||
|
status: 'pending' | 'in_progress' | 'completed' | 'blocked';
|
||||||
|
expectedDuration: number | undefined;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let now = $state(Date.now());
|
||||||
|
|
||||||
|
// Atualizar a cada minuto
|
||||||
|
$effect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
now = Date.now();
|
||||||
|
}, 60000); // Atualizar a cada minuto
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
});
|
||||||
|
|
||||||
|
const tempoInfo = $derived.by(() => {
|
||||||
|
// Para etapas concluídas
|
||||||
|
if (status === 'completed' && finishedAt && startedAt) {
|
||||||
|
const tempoExecucao = finishedAt - startedAt;
|
||||||
|
const diasExecucao = Math.floor(tempoExecucao / (1000 * 60 * 60 * 24));
|
||||||
|
const horasExecucao = Math.floor((tempoExecucao % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||||
|
|
||||||
|
// Verificar se foi dentro ou fora do prazo
|
||||||
|
const dentroDoPrazo = dueDate ? finishedAt <= dueDate : true;
|
||||||
|
const diasAtrasado = !dentroDoPrazo && dueDate
|
||||||
|
? Math.floor((finishedAt - dueDate) / (1000 * 60 * 60 * 24))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
tipo: 'concluida',
|
||||||
|
dias: diasExecucao,
|
||||||
|
horas: horasExecucao,
|
||||||
|
dentroDoPrazo,
|
||||||
|
diasAtrasado
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para etapas em andamento
|
||||||
|
if (status === 'in_progress' && startedAt && expectedDuration) {
|
||||||
|
// Calcular prazo baseado em startedAt + expectedDuration
|
||||||
|
const prazoCalculado = startedAt + expectedDuration * 24 * 60 * 60 * 1000;
|
||||||
|
const diff = prazoCalculado - now;
|
||||||
|
const dias = Math.floor(Math.abs(diff) / (1000 * 60 * 60 * 24));
|
||||||
|
const horas = Math.floor((Math.abs(diff) % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||||
|
|
||||||
|
return {
|
||||||
|
tipo: 'andamento',
|
||||||
|
atrasado: diff < 0,
|
||||||
|
dias,
|
||||||
|
horas
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para etapas pendentes ou bloqueadas, não mostrar nada
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if tempoInfo}
|
||||||
|
{@const info = tempoInfo}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
{#if info.tipo === 'concluida'}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-4 w-4 {info.dentroDoPrazo ? 'text-info' : 'text-error'}"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm font-medium {info.dentroDoPrazo ? 'text-info' : 'text-error'}">
|
||||||
|
Concluída em {info.dias > 0 ? `${info.dias} ${info.dias === 1 ? 'dia' : 'dias'} e ` : ''}
|
||||||
|
{info.horas} {info.horas === 1 ? 'hora' : 'horas'}
|
||||||
|
{#if !info.dentroDoPrazo && info.diasAtrasado > 0}
|
||||||
|
<span> ({info.diasAtrasado} {info.diasAtrasado === 1 ? 'dia' : 'dias'} fora do prazo)</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{:else if info.tipo === 'andamento'}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-4 w-4 {info.atrasado ? 'text-error' : 'text-success'}"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm font-medium {info.atrasado ? 'text-error' : 'text-success'}">
|
||||||
|
{#if info.atrasado}
|
||||||
|
{info.dias > 0 ? `${info.dias} ${info.dias === 1 ? 'dia' : 'dias'} e ` : ''}
|
||||||
|
{info.horas} {info.horas === 1 ? 'hora' : 'horas'} atrasado
|
||||||
|
{:else}
|
||||||
|
{info.dias > 0 ? `${info.dias} ${info.dias === 1 ? 'dia' : 'dias'} e ` : ''}
|
||||||
|
{info.horas} {info.horas === 1 ? 'hora' : 'horas'} para concluir
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
@@ -302,11 +302,11 @@
|
|||||||
|
|
||||||
<!-- Link para Instâncias -->
|
<!-- Link para Instâncias -->
|
||||||
<section class="flex justify-center">
|
<section class="flex justify-center">
|
||||||
<a href="/fluxos/instancias" class="btn btn-outline btn-lg">
|
<a href="/licitacoes/fluxos" class="btn btn-outline btn-lg">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||||
</svg>
|
</svg>
|
||||||
Ver Instâncias de Fluxo
|
Ver Fluxos de Trabalho
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -12,9 +12,12 @@
|
|||||||
// Query da instância com passos
|
// Query da instância com passos
|
||||||
const instanceQuery = useQuery(api.flows.getInstanceWithSteps, () => ({ id: instanceId }));
|
const instanceQuery = useQuery(api.flows.getInstanceWithSteps, () => ({ id: instanceId }));
|
||||||
|
|
||||||
// Query de usuários (para reatribuição)
|
// Query de usuários (para reatribuição) - será filtrado por setor no modal
|
||||||
const usuariosQuery = useQuery(api.usuarios.listar, {});
|
const usuariosQuery = useQuery(api.usuarios.listar, {});
|
||||||
|
|
||||||
|
// Query de usuários por setor para atribuição
|
||||||
|
let usuariosPorSetorQuery = $state<ReturnType<typeof useQuery<typeof api.flows.getUsuariosBySetorForAssignment>> | null>(null);
|
||||||
|
|
||||||
// Estado de operações
|
// Estado de operações
|
||||||
let isProcessing = $state(false);
|
let isProcessing = $state(false);
|
||||||
let processingError = $state<string | null>(null);
|
let processingError = $state<string | null>(null);
|
||||||
@@ -286,8 +289,8 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" class="text-base-content/30 h-16 w-16" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
<svg xmlns="http://www.w3.org/2000/svg" class="text-base-content/30 h-16 w-16" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<h3 class="text-base-content/70 mt-4 text-lg font-semibold">Instância não encontrada</h3>
|
<h3 class="text-base-content/70 mt-4 text-lg font-semibold">Fluxo não encontrado</h3>
|
||||||
<a href={resolve('/(dashboard)/fluxos/instancias')} class="btn btn-ghost mt-4">Voltar para lista</a>
|
<a href={resolve('/(dashboard)/licitacoes/fluxos')} class="btn btn-ghost mt-4">Voltar para lista</a>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{@const instance = instanceQuery.data.instance}
|
{@const instance = instanceQuery.data.instance}
|
||||||
@@ -302,7 +305,7 @@
|
|||||||
<div class="bg-secondary/20 absolute right-0 -bottom-16 h-56 w-56 rounded-full blur-3xl"></div>
|
<div class="bg-secondary/20 absolute right-0 -bottom-16 h-56 w-56 rounded-full blur-3xl"></div>
|
||||||
<div class="relative z-10">
|
<div class="relative z-10">
|
||||||
<div class="flex items-center gap-4 mb-6">
|
<div class="flex items-center gap-4 mb-6">
|
||||||
<a href={resolve('/(dashboard)/fluxos/instancias')} class="btn btn-ghost btn-sm">
|
<a href={resolve('/(dashboard)/licitacoes/fluxos')} class="btn btn-ghost btn-sm">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -317,10 +320,12 @@
|
|||||||
{instance.templateName ?? 'Fluxo'}
|
{instance.templateName ?? 'Fluxo'}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="flex flex-wrap gap-4">
|
<div class="flex flex-wrap gap-4">
|
||||||
<div class="flex items-center gap-2">
|
{#if instance.contratoId}
|
||||||
<span class="badge badge-outline">{instance.targetType}</span>
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-base-content/70 font-medium">{instance.targetId}</span>
|
<span class="badge badge-outline">Contrato</span>
|
||||||
</div>
|
<span class="text-base-content/70 font-medium">{instance.contratoId}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="flex items-center gap-2 text-base-content/60">
|
<div class="flex items-center gap-2 text-base-content/60">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||||
@@ -693,7 +698,7 @@
|
|||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
<h3 class="text-lg font-bold text-error">Cancelar Fluxo</h3>
|
<h3 class="text-lg font-bold text-error">Cancelar Fluxo</h3>
|
||||||
<p class="py-4">
|
<p class="py-4">
|
||||||
Tem certeza que deseja cancelar esta instância de fluxo?
|
Tem certeza que deseja cancelar este fluxo?
|
||||||
</p>
|
</p>
|
||||||
<p class="text-base-content/60 text-sm">
|
<p class="text-base-content/60 text-sm">
|
||||||
Esta ação não pode ser desfeita. Todos os passos pendentes serão marcados como cancelados.
|
Esta ação não pode ser desfeita. Todos os passos pendentes serão marcados como cancelados.
|
||||||
@@ -14,6 +14,12 @@
|
|||||||
const stepsQuery = useQuery(api.flows.listStepsByTemplate, () => ({ flowTemplateId: templateId }));
|
const stepsQuery = useQuery(api.flows.listStepsByTemplate, () => ({ flowTemplateId: templateId }));
|
||||||
const setoresQuery = useQuery(api.setores.list, {});
|
const setoresQuery = useQuery(api.setores.list, {});
|
||||||
|
|
||||||
|
// Query de sub-etapas (reativa baseada no step selecionado)
|
||||||
|
const subEtapasQuery = useQuery(
|
||||||
|
api.flows.listarSubEtapas,
|
||||||
|
() => selectedStepId ? { flowStepId: selectedStepId } : 'skip'
|
||||||
|
);
|
||||||
|
|
||||||
// Estado local para drag and drop
|
// Estado local para drag and drop
|
||||||
let localSteps = $state<NonNullable<typeof stepsQuery.data>>([]);
|
let localSteps = $state<NonNullable<typeof stepsQuery.data>>([]);
|
||||||
let isDragging = $state(false);
|
let isDragging = $state(false);
|
||||||
@@ -48,6 +54,13 @@
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
let isSavingStep = $state(false);
|
let isSavingStep = $state(false);
|
||||||
|
|
||||||
|
// Estado de sub-etapas
|
||||||
|
let showSubEtapaModal = $state(false);
|
||||||
|
let subEtapaNome = $state('');
|
||||||
|
let subEtapaDescricao = $state('');
|
||||||
|
let isCriandoSubEtapa = $state(false);
|
||||||
|
let subEtapaError = $state<string | null>(null);
|
||||||
|
|
||||||
// Inicializar edição quando selecionar passo
|
// Inicializar edição quando selecionar passo
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (selectedStep) {
|
if (selectedStep) {
|
||||||
@@ -215,6 +228,67 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Funções de sub-etapas
|
||||||
|
function openSubEtapaModal() {
|
||||||
|
subEtapaNome = '';
|
||||||
|
subEtapaDescricao = '';
|
||||||
|
subEtapaError = null;
|
||||||
|
showSubEtapaModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSubEtapaModal() {
|
||||||
|
showSubEtapaModal = false;
|
||||||
|
subEtapaNome = '';
|
||||||
|
subEtapaDescricao = '';
|
||||||
|
subEtapaError = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCriarSubEtapa() {
|
||||||
|
if (!selectedStepId || !subEtapaNome.trim()) {
|
||||||
|
subEtapaError = 'O nome é obrigatório';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isCriandoSubEtapa = true;
|
||||||
|
subEtapaError = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.mutation(api.flows.criarSubEtapa, {
|
||||||
|
flowStepId: selectedStepId,
|
||||||
|
name: subEtapaNome.trim(),
|
||||||
|
description: subEtapaDescricao.trim() || undefined
|
||||||
|
});
|
||||||
|
closeSubEtapaModal();
|
||||||
|
} catch (e) {
|
||||||
|
subEtapaError = e instanceof Error ? e.message : 'Erro ao criar sub-etapa';
|
||||||
|
} finally {
|
||||||
|
isCriandoSubEtapa = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeletarSubEtapa(subEtapaId: Id<'flowSubSteps'>) {
|
||||||
|
if (!confirm('Tem certeza que deseja excluir esta sub-etapa?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.mutation(api.flows.deletarSubEtapa, { subEtapaId });
|
||||||
|
} catch (e) {
|
||||||
|
alert(e instanceof Error ? e.message : 'Erro ao deletar sub-etapa');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAtualizarStatusSubEtapa(subEtapaId: Id<'flowSubSteps'>, novoStatus: 'pending' | 'in_progress' | 'completed' | 'blocked') {
|
||||||
|
try {
|
||||||
|
await client.mutation(api.flows.atualizarSubEtapa, {
|
||||||
|
subEtapaId,
|
||||||
|
status: novoStatus
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
alert(e instanceof Error ? e.message : 'Erro ao atualizar status');
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="flex h-[calc(100vh-4rem)] flex-col">
|
<main class="flex h-[calc(100vh-4rem)] flex-col">
|
||||||
@@ -472,6 +546,73 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Sub-etapas -->
|
||||||
|
<div class="form-control">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text font-semibold">Sub-etapas</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-xs"
|
||||||
|
onclick={openSubEtapaModal}
|
||||||
|
aria-label="Adicionar sub-etapa"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
Adicionar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
{#if subEtapasQuery.isLoading}
|
||||||
|
<div class="flex items-center justify-center py-4">
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
</div>
|
||||||
|
{:else if subEtapasQuery.data && subEtapasQuery.data.length > 0}
|
||||||
|
{#each subEtapasQuery.data as subEtapa (subEtapa._id)}
|
||||||
|
<div class="flex items-center gap-2 rounded-lg border border-base-300 bg-base-100 p-2">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="font-medium text-sm">{subEtapa.name}</div>
|
||||||
|
{#if subEtapa.description}
|
||||||
|
<div class="text-base-content/60 text-xs">{subEtapa.description}</div>
|
||||||
|
{/if}
|
||||||
|
<div class="mt-1">
|
||||||
|
<span class="badge badge-xs {subEtapa.status === 'completed' ? 'badge-success' : subEtapa.status === 'in_progress' ? 'badge-info' : subEtapa.status === 'blocked' ? 'badge-error' : 'badge-ghost'}">
|
||||||
|
{subEtapa.status === 'completed' ? 'Concluída' : subEtapa.status === 'in_progress' ? 'Em Andamento' : subEtapa.status === 'blocked' ? 'Bloqueada' : 'Pendente'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<select
|
||||||
|
class="select select-xs select-bordered"
|
||||||
|
value={subEtapa.status}
|
||||||
|
onchange={(e) => handleAtualizarStatusSubEtapa(subEtapa._id, e.currentTarget.value as 'pending' | 'in_progress' | 'completed' | 'blocked')}
|
||||||
|
>
|
||||||
|
<option value="pending">Pendente</option>
|
||||||
|
<option value="in_progress">Em Andamento</option>
|
||||||
|
<option value="completed">Concluída</option>
|
||||||
|
<option value="blocked">Bloqueada</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-xs text-error"
|
||||||
|
onclick={() => handleDeletarSubEtapa(subEtapa._id)}
|
||||||
|
aria-label="Deletar sub-etapa"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<div class="text-base-content/40 rounded-lg border border-dashed border-base-300 bg-base-200/50 p-4 text-center text-sm">
|
||||||
|
Nenhuma sub-etapa adicionada
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 pt-4">
|
<div class="flex gap-2 pt-4">
|
||||||
<button
|
<button
|
||||||
class="btn btn-error btn-outline flex-1"
|
class="btn btn-error btn-outline flex-1"
|
||||||
@@ -597,3 +738,63 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Modal de Nova Sub-etapa -->
|
||||||
|
{#if showSubEtapaModal}
|
||||||
|
<div class="modal modal-open">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3 class="text-lg font-bold">Nova Sub-etapa</h3>
|
||||||
|
|
||||||
|
{#if subEtapaError}
|
||||||
|
<div class="alert alert-error mt-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span>{subEtapaError}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<form onsubmit={(e) => { e.preventDefault(); handleCriarSubEtapa(); }} class="mt-4 space-y-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="sub-etapa-nome">
|
||||||
|
<span class="label-text">Nome da Sub-etapa</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="sub-etapa-nome"
|
||||||
|
bind:value={subEtapaNome}
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
placeholder="Ex: Revisar documentação"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="sub-etapa-descricao">
|
||||||
|
<span class="label-text">Descrição (opcional)</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="sub-etapa-descricao"
|
||||||
|
bind:value={subEtapaDescricao}
|
||||||
|
class="textarea textarea-bordered w-full"
|
||||||
|
placeholder="Descreva a sub-etapa..."
|
||||||
|
rows="2"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-action">
|
||||||
|
<button type="button" class="btn" onclick={closeSubEtapaModal} disabled={isCriandoSubEtapa}>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-secondary" disabled={isCriandoSubEtapa}>
|
||||||
|
{#if isCriandoSubEtapa}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
{/if}
|
||||||
|
Criar Sub-etapa
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="modal-backdrop" onclick={closeSubEtapaModal} aria-label="Fechar modal"></button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
managerId: managerId as Id<'usuarios'>
|
managerId: managerId as Id<'usuarios'>
|
||||||
});
|
});
|
||||||
closeCreateModal();
|
closeCreateModal();
|
||||||
goto(`/fluxos/instancias/${instanceId}`);
|
goto(`/licitacoes/fluxos/${instanceId}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
createError = e instanceof Error ? e.message : 'Erro ao criar instância';
|
createError = e instanceof Error ? e.message : 'Erro ao criar instância';
|
||||||
} finally {
|
} finally {
|
||||||
@@ -237,7 +237,7 @@
|
|||||||
<td class="text-base-content/60 text-sm">{formatDate(instance.startedAt)}</td>
|
<td class="text-base-content/60 text-sm">{formatDate(instance.startedAt)}</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<a
|
<a
|
||||||
href="/fluxos/instancias/{instance._id}"
|
href="/licitacoes/fluxos/{instance._id}"
|
||||||
class="btn btn-ghost btn-sm"
|
class="btn btn-ghost btn-sm"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { FileText, ClipboardCopy, Building2, Workflow } from 'lucide-svelte';
|
import { FileText, ClipboardCopy, Building2, Workflow, ChevronRight } from 'lucide-svelte';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
|
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
|
||||||
|
import type { Component } from 'svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
@@ -16,7 +17,16 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-4 md:grid-cols-3">
|
<!-- Cabeçalho -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h1 class="text-4xl font-bold text-primary mb-2">Licitações</h1>
|
||||||
|
<p class="text-lg text-base-content/70">
|
||||||
|
Gerencie empresas, contratos e processos licitatórios
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cards Principais -->
|
||||||
|
<div class="grid gap-4 md:grid-cols-3 mb-8">
|
||||||
<a
|
<a
|
||||||
href={resolve('/licitacoes/empresas')}
|
href={resolve('/licitacoes/empresas')}
|
||||||
class="card bg-base-100 border-base-200 hover:border-primary border shadow-md transition-shadow hover:shadow-lg"
|
class="card bg-base-100 border-base-200 hover:border-primary border shadow-md transition-shadow hover:shadow-lg"
|
||||||
@@ -74,23 +84,87 @@
|
|||||||
<p class="text-base-content/60 text-sm">Em breve: gestão de documentos e editais.</p>
|
<p class="text-base-content/60 text-sm">Em breve: gestão de documentos e editais.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a
|
<!-- Seção Fluxos -->
|
||||||
href={resolve('/fluxos')}
|
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300">
|
||||||
class="card bg-base-100 border-base-200 hover:border-secondary border shadow-md transition-shadow hover:shadow-lg"
|
<div class="card-body">
|
||||||
>
|
<!-- Cabeçalho da Categoria -->
|
||||||
<div class="card-body">
|
<div class="flex items-start gap-6 mb-6">
|
||||||
<div class="mb-2 flex items-center gap-3">
|
<div class="p-4 bg-secondary/20 rounded-2xl">
|
||||||
<div class="bg-secondary/10 rounded-lg p-2">
|
<Workflow class="h-12 w-12 text-secondary" strokeWidth={2} />
|
||||||
<Workflow class="text-secondary h-6 w-6" strokeWidth={2} />
|
</div>
|
||||||
</div>
|
<div class="flex-1">
|
||||||
<h4 class="font-semibold">Fluxos de Trabalho</h4>
|
<h2 class="card-title text-2xl mb-2 text-secondary">
|
||||||
|
Fluxos de Trabalho
|
||||||
|
</h2>
|
||||||
|
<p class="text-base-content/70">Gerencie templates e fluxos de trabalho para contratos e processos</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-base-content/70 text-sm">
|
|
||||||
Gerencie templates e instâncias de fluxos de trabalho para contratos e processos.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
|
||||||
|
<!-- Grid de Opções -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<a
|
||||||
|
href={resolve('/licitacoes/fluxos')}
|
||||||
|
class="group relative overflow-hidden rounded-xl border-2 border-base-300 bg-linear-to-br from-secondary/10 to-secondary/20 p-6 hover:border-secondary hover:shadow-lg transition-all duration-300 transform hover:-translate-y-1"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<div class="flex items-start justify-between mb-4">
|
||||||
|
<div
|
||||||
|
class="p-3 bg-base-100 rounded-lg group-hover:bg-secondary group-hover:text-white transition-colors duration-300"
|
||||||
|
>
|
||||||
|
<Workflow
|
||||||
|
class="h-5 w-5 text-secondary group-hover:text-white transition-colors duration-300"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ChevronRight
|
||||||
|
class="h-5 w-5 text-base-content/30 group-hover:text-secondary transition-colors duration-300"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h3
|
||||||
|
class="text-lg font-bold text-base-content mb-2 group-hover:text-secondary transition-colors duration-300"
|
||||||
|
>
|
||||||
|
Meus Fluxos
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-base-content/70 flex-1">
|
||||||
|
Visualize e gerencie os fluxos de trabalho em execução
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={resolve('/fluxos')}
|
||||||
|
class="group relative overflow-hidden rounded-xl border-2 border-base-300 bg-linear-to-br from-secondary/10 to-secondary/20 p-6 hover:border-secondary hover:shadow-lg transition-all duration-300 transform hover:-translate-y-1"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<div class="flex items-start justify-between mb-4">
|
||||||
|
<div
|
||||||
|
class="p-3 bg-base-100 rounded-lg group-hover:bg-secondary group-hover:text-white transition-colors duration-300"
|
||||||
|
>
|
||||||
|
<FileText
|
||||||
|
class="h-5 w-5 text-secondary group-hover:text-white transition-colors duration-300"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ChevronRight
|
||||||
|
class="h-5 w-5 text-base-content/30 group-hover:text-secondary transition-colors duration-300"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h3
|
||||||
|
class="text-lg font-bold text-base-content mb-2 group-hover:text-secondary transition-colors duration-300"
|
||||||
|
>
|
||||||
|
Templates
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-base-content/70 flex-1">
|
||||||
|
Crie e edite templates de fluxos de trabalho
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
|
|||||||
365
apps/web/src/routes/(dashboard)/licitacoes/fluxos/+page.svelte
Normal file
365
apps/web/src/routes/(dashboard)/licitacoes/fluxos/+page.svelte
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { useQuery, useConvexClient } from 'convex-svelte';
|
||||||
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
|
import ActionGuard from '$lib/components/ActionGuard.svelte';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
const client = useConvexClient();
|
||||||
|
|
||||||
|
// Estado dos filtros
|
||||||
|
let statusFilter = $state<'active' | 'completed' | 'cancelled' | undefined>(undefined);
|
||||||
|
|
||||||
|
// Query de instâncias
|
||||||
|
const instancesQuery = useQuery(
|
||||||
|
api.flows.listInstances,
|
||||||
|
() => (statusFilter ? { status: statusFilter } : {})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Query de templates publicados (para o modal de criação)
|
||||||
|
const publishedTemplatesQuery = useQuery(api.flows.listTemplates, { status: 'published' });
|
||||||
|
|
||||||
|
// Modal de criação
|
||||||
|
let showCreateModal = $state(false);
|
||||||
|
let selectedTemplateId = $state<Id<'flowTemplates'> | ''>('');
|
||||||
|
let contratoId = $state<Id<'contratos'> | ''>('');
|
||||||
|
let managerId = $state<Id<'usuarios'> | ''>('');
|
||||||
|
let isCreating = $state(false);
|
||||||
|
let createError = $state<string | null>(null);
|
||||||
|
|
||||||
|
// Query de usuários (para seleção de gerente)
|
||||||
|
const usuariosQuery = useQuery(api.usuarios.listar, {});
|
||||||
|
|
||||||
|
// Query de contratos (para seleção)
|
||||||
|
const contratosQuery = useQuery(api.contratos.listar, {});
|
||||||
|
|
||||||
|
function openCreateModal() {
|
||||||
|
selectedTemplateId = '';
|
||||||
|
contratoId = '';
|
||||||
|
managerId = '';
|
||||||
|
createError = null;
|
||||||
|
showCreateModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCreateModal() {
|
||||||
|
showCreateModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreate() {
|
||||||
|
if (!selectedTemplateId || !managerId) {
|
||||||
|
createError = 'Template e gerente são obrigatórios';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isCreating = true;
|
||||||
|
createError = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const instanceId = await client.mutation(api.flows.instantiateFlow, {
|
||||||
|
flowTemplateId: selectedTemplateId as Id<'flowTemplates'>,
|
||||||
|
contratoId: contratoId ? (contratoId as Id<'contratos'>) : undefined,
|
||||||
|
managerId: managerId as Id<'usuarios'>
|
||||||
|
});
|
||||||
|
closeCreateModal();
|
||||||
|
goto(`/licitacoes/fluxos/${instanceId}`);
|
||||||
|
} catch (e) {
|
||||||
|
createError = e instanceof Error ? e.message : 'Erro ao criar fluxo';
|
||||||
|
} finally {
|
||||||
|
isCreating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusBadge(status: 'active' | 'completed' | 'cancelled') {
|
||||||
|
switch (status) {
|
||||||
|
case 'active':
|
||||||
|
return { class: 'badge-info', label: 'Em Andamento' };
|
||||||
|
case 'completed':
|
||||||
|
return { class: 'badge-success', label: 'Concluído' };
|
||||||
|
case 'cancelled':
|
||||||
|
return { class: 'badge-error', label: 'Cancelado' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(timestamp: number): string {
|
||||||
|
return new Date(timestamp).toLocaleDateString('pt-BR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProgressPercentage(completed: number, total: number): number {
|
||||||
|
if (total === 0) return 0;
|
||||||
|
return Math.round((completed / total) * 100);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main class="mx-auto w-full max-w-7xl space-y-8 px-4 py-10">
|
||||||
|
<!-- Header -->
|
||||||
|
<section
|
||||||
|
class="border-info/25 from-info/10 via-base-100 to-secondary/20 relative overflow-hidden rounded-3xl border bg-linear-to-br p-8 shadow-2xl"
|
||||||
|
>
|
||||||
|
<div class="bg-info/20 absolute top-10 -left-10 h-40 w-40 rounded-full blur-3xl"></div>
|
||||||
|
<div class="bg-secondary/20 absolute right-0 -bottom-16 h-56 w-56 rounded-full blur-3xl"></div>
|
||||||
|
<div class="relative z-10 flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
|
||||||
|
<div class="max-w-3xl space-y-4">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<a href="/fluxos" class="btn btn-ghost btn-sm">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||||
|
</svg>
|
||||||
|
Templates
|
||||||
|
</a>
|
||||||
|
<span
|
||||||
|
class="border-info/40 bg-info/10 text-info inline-flex w-fit items-center gap-2 rounded-full border px-4 py-1 text-xs font-semibold tracking-[0.28em] uppercase"
|
||||||
|
>
|
||||||
|
Execução
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-base-content text-4xl leading-tight font-black sm:text-5xl">
|
||||||
|
Fluxos de Trabalho
|
||||||
|
</h1>
|
||||||
|
<p class="text-base-content/70 text-base leading-relaxed sm:text-lg">
|
||||||
|
Acompanhe e gerencie os fluxos de trabalho. Visualize o progresso,
|
||||||
|
documentos e responsáveis de cada etapa.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||||
|
<!-- Filtro de status -->
|
||||||
|
<select
|
||||||
|
class="select select-bordered"
|
||||||
|
bind:value={statusFilter}
|
||||||
|
>
|
||||||
|
<option value={undefined}>Todos os status</option>
|
||||||
|
<option value="active">Em Andamento</option>
|
||||||
|
<option value="completed">Concluído</option>
|
||||||
|
<option value="cancelled">Cancelado</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<ActionGuard recurso="fluxos_instancias" acao="criar">
|
||||||
|
<button class="btn btn-info shadow-lg" onclick={openCreateModal}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-5 w-5"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 4v16m8-8H4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Novo Fluxo
|
||||||
|
</button>
|
||||||
|
</ActionGuard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Lista de Instâncias -->
|
||||||
|
<section class="bg-base-100 rounded-2xl border p-6 shadow-lg">
|
||||||
|
{#if instancesQuery.isLoading}
|
||||||
|
<div class="flex items-center justify-center py-12">
|
||||||
|
<span class="loading loading-spinner loading-lg text-info"></span>
|
||||||
|
</div>
|
||||||
|
{:else if !instancesQuery.data || instancesQuery.data.length === 0}
|
||||||
|
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="text-base-content/30 h-16 w-16"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="1.5"
|
||||||
|
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-base-content/70 mt-4 text-lg font-semibold">Nenhum fluxo encontrado</h3>
|
||||||
|
<p class="text-base-content/50 mt-2">
|
||||||
|
{statusFilter ? 'Não há fluxos com este status.' : 'Clique em "Novo Fluxo" para iniciar um fluxo.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Template</th>
|
||||||
|
<th>Contrato</th>
|
||||||
|
<th>Gerente</th>
|
||||||
|
<th>Progresso</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Iniciado em</th>
|
||||||
|
<th class="text-right">Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each instancesQuery.data as instance (instance._id)}
|
||||||
|
{@const statusBadge = getStatusBadge(instance.status)}
|
||||||
|
{@const progressPercent = getProgressPercentage(instance.progress.completed, instance.progress.total)}
|
||||||
|
<tr class="hover">
|
||||||
|
<td>
|
||||||
|
<div class="font-medium">{instance.templateName ?? 'Template desconhecido'}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{#if instance.contratoId}
|
||||||
|
<span class="badge badge-outline badge-sm">{instance.contratoId}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="text-base-content/40 text-sm">-</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td class="text-base-content/70">{instance.managerName ?? '-'}</td>
|
||||||
|
<td>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<progress
|
||||||
|
class="progress progress-info w-20"
|
||||||
|
value={progressPercent}
|
||||||
|
max="100"
|
||||||
|
></progress>
|
||||||
|
<span class="text-xs text-base-content/60">
|
||||||
|
{instance.progress.completed}/{instance.progress.total}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {statusBadge.class}">{statusBadge.label}</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-base-content/60 text-sm">{formatDate(instance.startedAt)}</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<a
|
||||||
|
href="/licitacoes/fluxos/{instance._id}"
|
||||||
|
class="btn btn-ghost btn-sm"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
|
</svg>
|
||||||
|
Ver
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Modal de Criação -->
|
||||||
|
{#if showCreateModal}
|
||||||
|
<div class="modal modal-open">
|
||||||
|
<div class="modal-box max-w-2xl">
|
||||||
|
<h3 class="text-lg font-bold">Novo Fluxo de Trabalho</h3>
|
||||||
|
|
||||||
|
{#if createError}
|
||||||
|
<div class="alert alert-error mt-4">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6 shrink-0 stroke-current"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>{createError}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<form onsubmit={(e) => { e.preventDefault(); handleCreate(); }} class="mt-4 space-y-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="template-select">
|
||||||
|
<span class="label-text">Template de Fluxo</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="template-select"
|
||||||
|
bind:value={selectedTemplateId}
|
||||||
|
class="select select-bordered w-full"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Selecione um template</option>
|
||||||
|
{#if publishedTemplatesQuery.data}
|
||||||
|
{#each publishedTemplatesQuery.data as template (template._id)}
|
||||||
|
<option value={template._id}>{template.name}</option>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</select>
|
||||||
|
<p class="label">
|
||||||
|
<span class="label-text-alt text-base-content/60">Apenas templates publicados podem ser instanciados</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="contrato-select">
|
||||||
|
<span class="label-text">Contrato (Opcional)</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="contrato-select"
|
||||||
|
bind:value={contratoId}
|
||||||
|
class="select select-bordered w-full"
|
||||||
|
>
|
||||||
|
<option value="">Nenhum contrato</option>
|
||||||
|
{#if contratosQuery.data}
|
||||||
|
{#each contratosQuery.data as contrato (contrato._id)}
|
||||||
|
<option value={contrato._id}>{contrato.numero ?? contrato._id}</option>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</select>
|
||||||
|
<p class="label">
|
||||||
|
<span class="label-text-alt text-base-content/60">Opcional: vincule este fluxo a um contrato específico</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="manager-select">
|
||||||
|
<span class="label-text">Gerente Responsável</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="manager-select"
|
||||||
|
bind:value={managerId}
|
||||||
|
class="select select-bordered w-full"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Selecione um gerente</option>
|
||||||
|
{#if usuariosQuery.data}
|
||||||
|
{#each usuariosQuery.data as usuario (usuario._id)}
|
||||||
|
<option value={usuario._id}>{usuario.nome}</option>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-action">
|
||||||
|
<button type="button" class="btn" onclick={closeCreateModal} disabled={isCreating}>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-info" disabled={isCreating}>
|
||||||
|
{#if isCreating}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
{/if}
|
||||||
|
Iniciar Fluxo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="modal-backdrop" onclick={closeCreateModal} aria-label="Fechar modal"></button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
1251
apps/web/src/routes/(dashboard)/licitacoes/fluxos/[id]/+page.svelte
Normal file
1251
apps/web/src/routes/(dashboard)/licitacoes/fluxos/[id]/+page.svelte
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,42 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useConvexClient } 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 { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import type { SimboloTipo } from '@sgse-app/backend/convex/schema';
|
import type { SimboloTipo } from '@sgse-app/backend/convex/schema';
|
||||||
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import PrintModal from '$lib/components/PrintModal.svelte';
|
import PrintModal from '$lib/components/PrintModal.svelte';
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
let list: Array<any> = [];
|
// Estado reativo
|
||||||
let filtered: Array<any> = [];
|
let list = $state<Array<{
|
||||||
let selectedId: string | null = null;
|
_id: Id<'funcionarios'>;
|
||||||
let openMenuId: string | null = null;
|
nome: string;
|
||||||
let funcionarioParaImprimir: any = null;
|
matricula?: string;
|
||||||
|
cpf: string;
|
||||||
|
cidade?: string;
|
||||||
|
uf?: string;
|
||||||
|
simboloTipo?: SimboloTipo;
|
||||||
|
}>>([]);
|
||||||
|
let filtered = $state<typeof list>([]);
|
||||||
|
let openMenuId = $state<string | null>(null);
|
||||||
|
let funcionarioParaImprimir = $state<unknown>(null);
|
||||||
|
|
||||||
let filtroNome = '';
|
// Estado do modal de setores
|
||||||
let filtroCPF = '';
|
let showSetoresModal = $state(false);
|
||||||
let filtroMatricula = '';
|
let funcionarioParaSetores = $state<{ _id: Id<'funcionarios'>; nome: string } | null>(null);
|
||||||
let filtroTipo: SimboloTipo | '' = '';
|
let setoresSelecionados = $state<Id<'setores'>[]>([]);
|
||||||
|
let isSavingSetores = $state(false);
|
||||||
|
let setoresError = $state<string | null>(null);
|
||||||
|
|
||||||
|
// Queries
|
||||||
|
const todosSetoresQuery = useQuery(api.setores.list, {});
|
||||||
|
|
||||||
|
let filtroNome = $state('');
|
||||||
|
let filtroCPF = $state('');
|
||||||
|
let filtroMatricula = $state('');
|
||||||
|
let filtroTipo = $state<SimboloTipo | ''>('');
|
||||||
|
|
||||||
function applyFilters() {
|
function applyFilters() {
|
||||||
const nome = filtroNome.toLowerCase();
|
const nome = filtroNome.toLowerCase();
|
||||||
@@ -33,18 +52,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
list = await client.query(api.funcionarios.getAll, {} as any);
|
const data = await client.query(api.funcionarios.getAll, {});
|
||||||
|
list = data ?? [];
|
||||||
applyFilters();
|
applyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
function editSelected() {
|
|
||||||
if (selectedId) goto(resolve(`/recursos-humanos/funcionarios/${selectedId}/editar`));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openPrintModal(funcionarioId: string) {
|
async function openPrintModal(funcionarioId: string) {
|
||||||
try {
|
try {
|
||||||
const data = await client.query(api.funcionarios.getFichaCompleta, {
|
const data = await client.query(api.funcionarios.getFichaCompleta, {
|
||||||
id: funcionarioId as any
|
id: funcionarioId as Id<'funcionarios'>
|
||||||
});
|
});
|
||||||
funcionarioParaImprimir = data;
|
funcionarioParaImprimir = data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -62,12 +78,64 @@
|
|||||||
function toggleMenu(id: string) {
|
function toggleMenu(id: string) {
|
||||||
openMenuId = openMenuId === id ? null : id;
|
openMenuId = openMenuId === id ? null : id;
|
||||||
}
|
}
|
||||||
$: needsScroll = filtered.length > 8;
|
|
||||||
|
async function openSetoresModal(funcionarioId: Id<'funcionarios'>, nome: string) {
|
||||||
|
funcionarioParaSetores = { _id: funcionarioId, nome };
|
||||||
|
setoresSelecionados = [];
|
||||||
|
setoresError = null;
|
||||||
|
showSetoresModal = true;
|
||||||
|
openMenuId = null;
|
||||||
|
|
||||||
|
// Carregar setores do funcionário
|
||||||
|
try {
|
||||||
|
const setores = await client.query(api.setores.getSetoresByFuncionario, {
|
||||||
|
funcionarioId
|
||||||
|
});
|
||||||
|
setoresSelecionados = setores.map((s) => s._id);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erro ao carregar setores do funcionário:', err);
|
||||||
|
setoresError = 'Erro ao carregar setores do funcionário';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSetoresModal() {
|
||||||
|
showSetoresModal = false;
|
||||||
|
funcionarioParaSetores = null;
|
||||||
|
setoresSelecionados = [];
|
||||||
|
setoresError = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSetor(setorId: Id<'setores'>) {
|
||||||
|
if (setoresSelecionados.includes(setorId)) {
|
||||||
|
setoresSelecionados = setoresSelecionados.filter((id) => id !== setorId);
|
||||||
|
} else {
|
||||||
|
setoresSelecionados = [...setoresSelecionados, setorId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function salvarSetores() {
|
||||||
|
if (!funcionarioParaSetores) return;
|
||||||
|
|
||||||
|
isSavingSetores = true;
|
||||||
|
setoresError = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.mutation(api.setores.atualizarSetoresFuncionario, {
|
||||||
|
funcionarioId: funcionarioParaSetores._id,
|
||||||
|
setorIds: setoresSelecionados
|
||||||
|
});
|
||||||
|
closeSetoresModal();
|
||||||
|
} catch (err) {
|
||||||
|
setoresError = err instanceof Error ? err.message : 'Erro ao salvar setores';
|
||||||
|
} finally {
|
||||||
|
isSavingSetores = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="container mx-auto px-4 py-4 max-w-7xl flex flex-col" style="height: calc(100vh - 8rem); min-height: 600px;">
|
<main class="container mx-auto px-4 py-4 max-w-7xl flex flex-col" style="height: calc(100vh - 8rem); min-height: 600px;">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="breadcrumbs mb-4 text-sm flex-shrink-0">
|
<div class="breadcrumbs mb-4 text-sm shrink-0">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href={resolve('/recursos-humanos')} class="text-primary hover:underline">Recursos Humanos</a></li>
|
<li><a href={resolve('/recursos-humanos')} class="text-primary hover:underline">Recursos Humanos</a></li>
|
||||||
<li>Funcionários</li>
|
<li>Funcionários</li>
|
||||||
@@ -75,7 +143,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cabeçalho -->
|
<!-- Cabeçalho -->
|
||||||
<div class="mb-6 flex-shrink-0">
|
<div class="mb-6 shrink-0">
|
||||||
<div class="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
|
<div class="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div class="rounded-xl bg-blue-500/20 p-3">
|
<div class="rounded-xl bg-blue-500/20 p-3">
|
||||||
@@ -118,7 +186,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filtros -->
|
<!-- Filtros -->
|
||||||
<div class="card bg-base-100/90 backdrop-blur-sm border border-base-300 mb-4 shadow-xl flex-shrink-0">
|
<div class="card bg-base-100/90 backdrop-blur-sm border border-base-300 mb-4 shadow-xl shrink-0">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title mb-4 text-lg">
|
<h2 class="card-title mb-4 text-lg">
|
||||||
<svg
|
<svg
|
||||||
@@ -232,7 +300,7 @@
|
|||||||
<div class="flex-1 overflow-hidden flex flex-col">
|
<div class="flex-1 overflow-hidden flex flex-col">
|
||||||
<div class="overflow-x-auto flex-1 overflow-y-auto">
|
<div class="overflow-x-auto flex-1 overflow-y-auto">
|
||||||
<table class="table table-zebra w-full">
|
<table class="table table-zebra w-full">
|
||||||
<thead class="sticky top-0 z-10 shadow-md bg-gradient-to-r from-base-300 to-base-200">
|
<thead class="sticky top-0 z-10 shadow-md bg-linear-to-r from-base-300 to-base-200">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Nome</th>
|
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">Nome</th>
|
||||||
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">CPF</th>
|
<th class="whitespace-nowrap font-bold text-base-content border-b border-base-400">CPF</th>
|
||||||
@@ -277,7 +345,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{:else}
|
{:else}
|
||||||
{#each filtered as f}
|
{#each filtered as f (f._id)}
|
||||||
<tr class="hover:bg-base-200/50 transition-colors">
|
<tr class="hover:bg-base-200/50 transition-colors">
|
||||||
<td class="whitespace-nowrap font-medium">{f.nome}</td>
|
<td class="whitespace-nowrap font-medium">{f.nome}</td>
|
||||||
<td class="whitespace-nowrap">{f.cpf}</td>
|
<td class="whitespace-nowrap">{f.cpf}</td>
|
||||||
@@ -314,20 +382,28 @@
|
|||||||
class="dropdown-content menu bg-base-100 rounded-box border-base-300 z-20 w-52 border p-2 shadow-xl"
|
class="dropdown-content menu bg-base-100 rounded-box border-base-300 z-20 w-52 border p-2 shadow-xl"
|
||||||
>
|
>
|
||||||
<li>
|
<li>
|
||||||
<a href={`/recursos-humanos/funcionarios/${f._id}`} class="hover:bg-primary/10">
|
<a href={resolve(`/recursos-humanos/funcionarios/${f._id}`)} class="hover:bg-primary/10">
|
||||||
Ver Detalhes
|
Ver Detalhes
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href={`/recursos-humanos/funcionarios/${f._id}/editar`} class="hover:bg-primary/10">
|
<a href={resolve(`/recursos-humanos/funcionarios/${f._id}/editar`)} class="hover:bg-primary/10">
|
||||||
Editar
|
Editar
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href={`/recursos-humanos/funcionarios/${f._id}/documentos`} class="hover:bg-primary/10">
|
<a href={resolve(`/recursos-humanos/funcionarios/${f._id}/documentos`)} class="hover:bg-primary/10">
|
||||||
Ver Documentos
|
Ver Documentos
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onclick={() => openSetoresModal(f._id, f.nome)}
|
||||||
|
class="hover:bg-primary/10"
|
||||||
|
>
|
||||||
|
Atribuir Setores
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button onclick={() => openPrintModal(f._id)} class="hover:bg-primary/10">
|
<button onclick={() => openPrintModal(f._id)} class="hover:bg-primary/10">
|
||||||
Imprimir Ficha
|
Imprimir Ficha
|
||||||
@@ -347,7 +423,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Informação sobre resultados -->
|
<!-- Informação sobre resultados -->
|
||||||
<div class="text-base-content/70 mt-3 text-center text-sm flex-shrink-0 border-t border-base-300 pt-3 bg-base-100/50">
|
<div class="text-base-content/70 mt-3 text-center text-sm shrink-0 border-t border-base-300 pt-3 bg-base-100/50">
|
||||||
Exibindo <span class="font-semibold">{filtered.length}</span> de <span class="font-semibold">{list.length}</span> funcionário(s)
|
Exibindo <span class="font-semibold">{filtered.length}</span> de <span class="font-semibold">{list.length}</span> funcionário(s)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -359,4 +435,85 @@
|
|||||||
onClose={() => (funcionarioParaImprimir = null)}
|
onClose={() => (funcionarioParaImprimir = null)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Modal de Atribuição de Setores -->
|
||||||
|
{#if showSetoresModal && funcionarioParaSetores}
|
||||||
|
<div class="modal modal-open">
|
||||||
|
<div class="modal-box max-w-2xl">
|
||||||
|
<h3 class="text-lg font-bold">Atribuir Setores</h3>
|
||||||
|
<p class="text-base-content/60 mt-2">
|
||||||
|
Selecione os setores para <strong>{funcionarioParaSetores.nome}</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{#if setoresError}
|
||||||
|
<div class="alert alert-error mt-4">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6 shrink-0 stroke-current"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>{setoresError}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="mt-4 max-h-96 overflow-y-auto">
|
||||||
|
{#if todosSetoresQuery.isLoading}
|
||||||
|
<div class="flex items-center justify-center py-8">
|
||||||
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
{:else if todosSetoresQuery.data && todosSetoresQuery.data.length > 0}
|
||||||
|
<div class="space-y-2">
|
||||||
|
{#each todosSetoresQuery.data as setor (setor._id)}
|
||||||
|
{@const isSelected = setoresSelecionados.includes(setor._id)}
|
||||||
|
<label class="flex cursor-pointer items-center gap-3 rounded-lg border p-3 hover:bg-base-200 {isSelected ? 'border-primary bg-primary/5' : 'border-base-300'}">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-primary"
|
||||||
|
checked={isSelected}
|
||||||
|
onchange={() => toggleSetor(setor._id)}
|
||||||
|
aria-label="Selecionar setor {setor.nome}"
|
||||||
|
/>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="font-medium">{setor.nome}</div>
|
||||||
|
<div class="text-base-content/60 text-sm">Sigla: {setor.sigla}</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="text-base-content/60 py-8 text-center">
|
||||||
|
<p>Nenhum setor cadastrado</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-action">
|
||||||
|
<button class="btn" onclick={closeSetoresModal} disabled={isSavingSetores}>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" onclick={salvarSetores} disabled={isSavingSetores}>
|
||||||
|
{#if isSavingSetores}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
{/if}
|
||||||
|
Salvar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="modal-backdrop"
|
||||||
|
onclick={closeSetoresModal}
|
||||||
|
aria-label="Fechar modal"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -163,6 +163,16 @@ export default defineSchema({
|
|||||||
.index("by_nome", ["nome"])
|
.index("by_nome", ["nome"])
|
||||||
.index("by_sigla", ["sigla"]),
|
.index("by_sigla", ["sigla"]),
|
||||||
|
|
||||||
|
// Relação muitos-para-muitos entre funcionários e setores
|
||||||
|
funcionarioSetores: defineTable({
|
||||||
|
funcionarioId: v.id("funcionarios"),
|
||||||
|
setorId: v.id("setores"),
|
||||||
|
createdAt: v.number(),
|
||||||
|
})
|
||||||
|
.index("by_funcionarioId", ["funcionarioId"])
|
||||||
|
.index("by_setorId", ["setorId"])
|
||||||
|
.index("by_funcionarioId_and_setorId", ["funcionarioId", "setorId"]),
|
||||||
|
|
||||||
// Templates de fluxo
|
// Templates de fluxo
|
||||||
flowTemplates: defineTable({
|
flowTemplates: defineTable({
|
||||||
name: v.string(),
|
name: v.string(),
|
||||||
@@ -191,8 +201,7 @@ export default defineSchema({
|
|||||||
// Instâncias de fluxo
|
// Instâncias de fluxo
|
||||||
flowInstances: defineTable({
|
flowInstances: defineTable({
|
||||||
flowTemplateId: v.id("flowTemplates"),
|
flowTemplateId: v.id("flowTemplates"),
|
||||||
targetType: v.string(), // ex: 'contrato', 'projeto'
|
contratoId: v.optional(v.id("contratos")),
|
||||||
targetId: v.string(), // ID genérico do alvo
|
|
||||||
managerId: v.id("usuarios"),
|
managerId: v.id("usuarios"),
|
||||||
status: flowInstanceStatus,
|
status: flowInstanceStatus,
|
||||||
startedAt: v.number(),
|
startedAt: v.number(),
|
||||||
@@ -200,7 +209,7 @@ export default defineSchema({
|
|||||||
currentStepId: v.optional(v.id("flowInstanceSteps")),
|
currentStepId: v.optional(v.id("flowInstanceSteps")),
|
||||||
})
|
})
|
||||||
.index("by_flowTemplateId", ["flowTemplateId"])
|
.index("by_flowTemplateId", ["flowTemplateId"])
|
||||||
.index("by_targetType_and_targetId", ["targetType", "targetId"])
|
.index("by_contratoId", ["contratoId"])
|
||||||
.index("by_managerId", ["managerId"])
|
.index("by_managerId", ["managerId"])
|
||||||
.index("by_status", ["status"]),
|
.index("by_status", ["status"]),
|
||||||
|
|
||||||
@@ -214,6 +223,8 @@ export default defineSchema({
|
|||||||
startedAt: v.optional(v.number()),
|
startedAt: v.optional(v.number()),
|
||||||
finishedAt: v.optional(v.number()),
|
finishedAt: v.optional(v.number()),
|
||||||
notes: v.optional(v.string()),
|
notes: v.optional(v.string()),
|
||||||
|
notesUpdatedBy: v.optional(v.id("usuarios")),
|
||||||
|
notesUpdatedAt: v.optional(v.number()),
|
||||||
dueDate: v.optional(v.number()),
|
dueDate: v.optional(v.number()),
|
||||||
})
|
})
|
||||||
.index("by_flowInstanceId", ["flowInstanceId"])
|
.index("by_flowInstanceId", ["flowInstanceId"])
|
||||||
@@ -232,6 +243,39 @@ export default defineSchema({
|
|||||||
.index("by_flowInstanceStepId", ["flowInstanceStepId"])
|
.index("by_flowInstanceStepId", ["flowInstanceStepId"])
|
||||||
.index("by_uploadedById", ["uploadedById"]),
|
.index("by_uploadedById", ["uploadedById"]),
|
||||||
|
|
||||||
|
// Sub-etapas de fluxo (para templates e instâncias)
|
||||||
|
flowSubSteps: defineTable({
|
||||||
|
flowStepId: v.optional(v.id("flowSteps")), // Para templates
|
||||||
|
flowInstanceStepId: v.optional(v.id("flowInstanceSteps")), // Para instâncias
|
||||||
|
name: v.string(),
|
||||||
|
description: v.optional(v.string()),
|
||||||
|
status: v.union(
|
||||||
|
v.literal("pending"),
|
||||||
|
v.literal("in_progress"),
|
||||||
|
v.literal("completed"),
|
||||||
|
v.literal("blocked")
|
||||||
|
),
|
||||||
|
position: v.number(),
|
||||||
|
createdBy: v.id("usuarios"),
|
||||||
|
createdAt: v.number(),
|
||||||
|
})
|
||||||
|
.index("by_flowStepId", ["flowStepId"])
|
||||||
|
.index("by_flowInstanceStepId", ["flowInstanceStepId"]),
|
||||||
|
|
||||||
|
// Notas de steps e sub-etapas
|
||||||
|
flowStepNotes: defineTable({
|
||||||
|
flowStepId: v.optional(v.id("flowSteps")),
|
||||||
|
flowInstanceStepId: v.optional(v.id("flowInstanceSteps")),
|
||||||
|
flowSubStepId: v.optional(v.id("flowSubSteps")),
|
||||||
|
texto: v.string(),
|
||||||
|
criadoPor: v.id("usuarios"),
|
||||||
|
criadoEm: v.number(),
|
||||||
|
arquivos: v.array(v.id("_storage")),
|
||||||
|
})
|
||||||
|
.index("by_flowStepId", ["flowStepId"])
|
||||||
|
.index("by_flowInstanceStepId", ["flowInstanceStepId"])
|
||||||
|
.index("by_flowSubStepId", ["flowSubStepId"]),
|
||||||
|
|
||||||
contratos: defineTable({
|
contratos: defineTable({
|
||||||
contratadaId: v.id("empresas"),
|
contratadaId: v.id("empresas"),
|
||||||
objeto: v.string(),
|
objeto: v.string(),
|
||||||
@@ -314,7 +358,6 @@ export default defineSchema({
|
|||||||
simboloId: v.id("simbolos"),
|
simboloId: v.id("simbolos"),
|
||||||
simboloTipo: simboloTipo,
|
simboloTipo: simboloTipo,
|
||||||
gestorId: v.optional(v.id("usuarios")),
|
gestorId: v.optional(v.id("usuarios")),
|
||||||
setorId: v.optional(v.id("setores")), // Setor do funcionário
|
|
||||||
statusFerias: v.optional(
|
statusFerias: v.optional(
|
||||||
v.union(v.literal("ativo"), v.literal("em_ferias"))
|
v.union(v.literal("ativo"), v.literal("em_ferias"))
|
||||||
),
|
),
|
||||||
@@ -454,8 +497,7 @@ export default defineSchema({
|
|||||||
.index("by_simboloTipo", ["simboloTipo"])
|
.index("by_simboloTipo", ["simboloTipo"])
|
||||||
.index("by_cpf", ["cpf"])
|
.index("by_cpf", ["cpf"])
|
||||||
.index("by_rg", ["rg"])
|
.index("by_rg", ["rg"])
|
||||||
.index("by_gestor", ["gestorId"])
|
.index("by_gestor", ["gestorId"]),
|
||||||
.index("by_setor", ["setorId"]),
|
|
||||||
|
|
||||||
atestados: defineTable({
|
atestados: defineTable({
|
||||||
funcionarioId: v.id("funcionarios"),
|
funcionarioId: v.id("funcionarios"),
|
||||||
@@ -1003,7 +1045,8 @@ export default defineSchema({
|
|||||||
v.literal("mencao"),
|
v.literal("mencao"),
|
||||||
v.literal("grupo_criado"),
|
v.literal("grupo_criado"),
|
||||||
v.literal("adicionado_grupo"),
|
v.literal("adicionado_grupo"),
|
||||||
v.literal("alerta_seguranca")
|
v.literal("alerta_seguranca"),
|
||||||
|
v.literal("etapa_fluxo_concluida")
|
||||||
),
|
),
|
||||||
conversaId: v.optional(v.id("conversas")),
|
conversaId: v.optional(v.id("conversas")),
|
||||||
mensagemId: v.optional(v.id("mensagens")),
|
mensagemId: v.optional(v.id("mensagens")),
|
||||||
|
|||||||
@@ -136,6 +136,146 @@ export const update = mutation({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obter funcionários de um setor específico
|
||||||
|
*/
|
||||||
|
export const getFuncionariosBySetor = query({
|
||||||
|
args: { setorId: v.id('setores') },
|
||||||
|
returns: v.array(
|
||||||
|
v.object({
|
||||||
|
_id: v.id('funcionarios'),
|
||||||
|
_creationTime: v.number(),
|
||||||
|
nome: v.string(),
|
||||||
|
matricula: v.optional(v.string()),
|
||||||
|
email: v.string(),
|
||||||
|
cpf: v.string()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
// Buscar todas as relações funcionarioSetores para este setor
|
||||||
|
const funcionarioSetores = await ctx.db
|
||||||
|
.query('funcionarioSetores')
|
||||||
|
.withIndex('by_setorId', (q) => q.eq('setorId', args.setorId))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Buscar os funcionários correspondentes
|
||||||
|
const funcionarios = [];
|
||||||
|
for (const relacao of funcionarioSetores) {
|
||||||
|
const funcionario = await ctx.db.get(relacao.funcionarioId);
|
||||||
|
if (funcionario) {
|
||||||
|
funcionarios.push({
|
||||||
|
_id: funcionario._id,
|
||||||
|
_creationTime: funcionario._creationTime,
|
||||||
|
nome: funcionario.nome,
|
||||||
|
matricula: funcionario.matricula,
|
||||||
|
email: funcionario.email,
|
||||||
|
cpf: funcionario.cpf
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return funcionarios;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obter setores de um funcionário
|
||||||
|
*/
|
||||||
|
export const getSetoresByFuncionario = query({
|
||||||
|
args: { funcionarioId: v.id('funcionarios') },
|
||||||
|
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, args) => {
|
||||||
|
// Buscar todas as relações funcionarioSetores para este funcionário
|
||||||
|
const funcionarioSetores = await ctx.db
|
||||||
|
.query('funcionarioSetores')
|
||||||
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', args.funcionarioId))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Buscar os setores correspondentes
|
||||||
|
const setores = [];
|
||||||
|
for (const relacao of funcionarioSetores) {
|
||||||
|
const setor = await ctx.db.get(relacao.setorId);
|
||||||
|
if (setor) {
|
||||||
|
setores.push(setor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return setores;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atualizar setores de um funcionário
|
||||||
|
*/
|
||||||
|
export const atualizarSetoresFuncionario = mutation({
|
||||||
|
args: {
|
||||||
|
funcionarioId: v.id('funcionarios'),
|
||||||
|
setorIds: v.array(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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar se o funcionário existe
|
||||||
|
const funcionario = await ctx.db.get(args.funcionarioId);
|
||||||
|
if (!funcionario) {
|
||||||
|
throw new Error('Funcionário não encontrado');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar se todos os setores existem
|
||||||
|
for (const setorId of args.setorIds) {
|
||||||
|
const setor = await ctx.db.get(setorId);
|
||||||
|
if (!setor) {
|
||||||
|
throw new Error(`Setor ${setorId} não encontrado`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remover todas as relações existentes do funcionário
|
||||||
|
const funcionarioSetoresExistentes = await ctx.db
|
||||||
|
.query('funcionarioSetores')
|
||||||
|
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', args.funcionarioId))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (const relacao of funcionarioSetoresExistentes) {
|
||||||
|
await ctx.db.delete(relacao._id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Criar novas relações para os setores selecionados
|
||||||
|
const now = Date.now();
|
||||||
|
for (const setorId of args.setorIds) {
|
||||||
|
// Verificar se já existe relação (evitar duplicatas)
|
||||||
|
const existe = await ctx.db
|
||||||
|
.query('funcionarioSetores')
|
||||||
|
.withIndex('by_funcionarioId_and_setorId', (q) =>
|
||||||
|
q.eq('funcionarioId', args.funcionarioId).eq('setorId', setorId)
|
||||||
|
)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!existe) {
|
||||||
|
await ctx.db.insert('funcionarioSetores', {
|
||||||
|
funcionarioId: args.funcionarioId,
|
||||||
|
setorId,
|
||||||
|
createdAt: now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Excluir um setor
|
* Excluir um setor
|
||||||
*/
|
*/
|
||||||
@@ -155,8 +295,8 @@ export const remove = mutation({
|
|||||||
|
|
||||||
// Verificar se há funcionários vinculados
|
// Verificar se há funcionários vinculados
|
||||||
const funcionariosVinculados = await ctx.db
|
const funcionariosVinculados = await ctx.db
|
||||||
.query('funcionarios')
|
.query('funcionarioSetores')
|
||||||
.withIndex('by_setor', (q) => q.eq('setorId', args.id))
|
.withIndex('by_setorId', (q) => q.eq('setorId', args.id))
|
||||||
.first();
|
.first();
|
||||||
if (funcionariosVinculados) {
|
if (funcionariosVinculados) {
|
||||||
throw new Error('Não é possível excluir um setor com funcionários vinculados');
|
throw new Error('Não é possível excluir um setor com funcionários vinculados');
|
||||||
|
|||||||
Reference in New Issue
Block a user