feat: implement requisition approval and rejection functionality in 'Almoxarifado', including stock verification, modal confirmations, and improved error handling for better inventory management
This commit is contained in:
@@ -72,6 +72,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resizeImage(dataUrl: string, maxWidth: number, maxHeight: number): Promise<string> {
|
function resizeImage(dataUrl: string, maxWidth: number, maxHeight: number): Promise<string> {
|
||||||
|
// Verificar se estamos no browser (não durante SSR)
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return Promise.reject(new Error('resizeImage não pode ser executada durante SSR'));
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const img = new window.Image();
|
const img = new window.Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
|
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
import { Check, Zap, Clock, Info, AlertTriangle, Calendar, X, Plus, ChevronLeft, ChevronRight, Trash2, CheckCircle } from 'lucide-svelte';
|
import { Check, Zap, Clock, Info, AlertTriangle, Calendar, X, Plus, ChevronLeft, ChevronRight, Trash2, CheckCircle } from 'lucide-svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -7,11 +7,22 @@ export const load = async ({ locals, url }) => {
|
|||||||
throw redirect(302, '/login?redirect=' + url.pathname);
|
throw redirect(302, '/login?redirect=' + url.pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = createConvexHttpClient({ token: locals.token });
|
try {
|
||||||
const currentUser = await client.query(api.auth.getCurrentUser);
|
const client = createConvexHttpClient({ token: locals.token });
|
||||||
|
const currentUser = await client.query(api.auth.getCurrentUser);
|
||||||
|
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
throw redirect(302, '/login?redirect=' + url.pathname);
|
throw redirect(302, '/login?redirect=' + url.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { currentUser };
|
||||||
|
} catch (error) {
|
||||||
|
// Se for um redirect, re-lançar
|
||||||
|
if (error && typeof error === 'object' && 'status' in error && 'location' in error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para outros erros, lançar novamente para ser capturado pelo handleError
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
return { currentUser };
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -99,6 +99,12 @@
|
|||||||
* para evitar problemas de CORS.
|
* para evitar problemas de CORS.
|
||||||
*/
|
*/
|
||||||
async function carregarImagemDeUrl(url: string): Promise<string | null> {
|
async function carregarImagemDeUrl(url: string): Promise<string | null> {
|
||||||
|
// Verificar se estamos no browser (não durante SSR)
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
console.warn('carregarImagemDeUrl chamada durante SSR, retornando null');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const appOrigin = window.location.origin;
|
const appOrigin = window.location.origin;
|
||||||
let urlOrigin: string;
|
let urlOrigin: string;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import { ClipboardList, Plus, CheckCircle, XCircle, Package, Filter, User, Building2, Calendar, FileText, Search, Hash } from 'lucide-svelte';
|
import { ClipboardList, Plus, CheckCircle, XCircle, Package, Filter, User, Building2, Calendar, FileText, Search, Hash, Eye, AlertTriangle } from 'lucide-svelte';
|
||||||
import ErrorModal from '$lib/components/ErrorModal.svelte';
|
import ErrorModal from '$lib/components/ErrorModal.svelte';
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
@@ -172,27 +172,177 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function aprovarRequisicao(id: Id<'requisicoesMaterial'>) {
|
let requisicaoParaAprovar = $state<Id<'requisicoesMaterial'> | null>(null);
|
||||||
|
let showModalAprovar = $state(false);
|
||||||
|
let problemasEstoqueAprovar = $state<Array<{
|
||||||
|
materialNome: string;
|
||||||
|
quantidadeSolicitada: number;
|
||||||
|
estoqueAtual: number;
|
||||||
|
falta: number;
|
||||||
|
}>>([]);
|
||||||
|
let processandoAprovar = $state(false);
|
||||||
|
|
||||||
|
function abrirModalAprovar(id: Id<'requisicoesMaterial'>) {
|
||||||
|
requisicaoParaAprovar = id;
|
||||||
|
problemasEstoqueAprovar = [];
|
||||||
|
showModalAprovar = true;
|
||||||
|
processandoAprovar = false;
|
||||||
|
|
||||||
|
// Verificar estoque quando abrir o modal
|
||||||
|
client.query(api.almoxarifado.verificarEstoqueRequisicao, { id }).then((verificacao) => {
|
||||||
|
if (!verificacao.temEstoqueSuficiente) {
|
||||||
|
problemasEstoqueAprovar = verificacao.problemasEstoque;
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// Ignorar erros na verificação prévia
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fecharModalAprovar() {
|
||||||
|
showModalAprovar = false;
|
||||||
|
requisicaoParaAprovar = null;
|
||||||
|
problemasEstoqueAprovar = [];
|
||||||
|
processandoAprovar = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmarAprovar() {
|
||||||
|
if (!requisicaoParaAprovar) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.mutation(api.almoxarifado.aprovarRequisicao, { id });
|
processandoAprovar = true;
|
||||||
|
await client.mutation(api.almoxarifado.aprovarRequisicao, { id: requisicaoParaAprovar });
|
||||||
mostrarMensagem('success', 'Requisição aprovada com sucesso!');
|
mostrarMensagem('success', 'Requisição aprovada com sucesso!');
|
||||||
|
fecharModalAprovar();
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Erro ao aprovar requisição';
|
const message = error instanceof Error ? error.message : 'Erro ao aprovar requisição';
|
||||||
mostrarMensagem('error', message);
|
mostrarMensagem('error', message);
|
||||||
|
} finally {
|
||||||
|
processandoAprovar = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function atenderRequisicao(id: Id<'requisicoesMaterial'>) {
|
let requisicaoParaVisualizar = $state<Id<'requisicoesMaterial'> | null>(null);
|
||||||
if (!confirm('Tem certeza que deseja atender esta requisição? Isso registrará as saídas de material.')) {
|
let showModalVisualizar = $state(false);
|
||||||
|
let requisicaoDetalhes = $state<any>(null);
|
||||||
|
let carregandoDetalhes = $state(false);
|
||||||
|
let erroDetalhes = $state<string | null>(null);
|
||||||
|
|
||||||
|
// Usar $effect para buscar dados quando requisicaoParaVisualizar muda
|
||||||
|
$effect(() => {
|
||||||
|
const id = requisicaoParaVisualizar;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
requisicaoDetalhes = null;
|
||||||
|
carregandoDetalhes = false;
|
||||||
|
erroDetalhes = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
carregandoDetalhes = true;
|
||||||
|
erroDetalhes = null;
|
||||||
|
requisicaoDetalhes = null;
|
||||||
|
|
||||||
|
client.query(api.almoxarifado.obterRequisicao, { id })
|
||||||
|
.then((data) => {
|
||||||
|
// Verificar se o ID ainda é o mesmo (evitar race conditions)
|
||||||
|
if (requisicaoParaVisualizar === id) {
|
||||||
|
requisicaoDetalhes = data;
|
||||||
|
carregandoDetalhes = false;
|
||||||
|
erroDetalhes = null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
// Verificar se o ID ainda é o mesmo (evitar race conditions)
|
||||||
|
if (requisicaoParaVisualizar === id) {
|
||||||
|
erroDetalhes = error?.message || String(error);
|
||||||
|
carregandoDetalhes = false;
|
||||||
|
requisicaoDetalhes = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function abrirModalVisualizar(id: Id<'requisicoesMaterial'>) {
|
||||||
|
requisicaoParaVisualizar = id;
|
||||||
|
showModalVisualizar = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fecharModalVisualizar() {
|
||||||
|
showModalVisualizar = false;
|
||||||
|
requisicaoParaVisualizar = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let requisicaoParaReprovar = $state<Id<'requisicoesMaterial'> | null>(null);
|
||||||
|
let motivoReprovacao = $state('');
|
||||||
|
let showModalReprovar = $state(false);
|
||||||
|
let processandoReprovar = $state(false);
|
||||||
|
|
||||||
|
function abrirModalReprovar(id: Id<'requisicoesMaterial'>) {
|
||||||
|
requisicaoParaReprovar = id;
|
||||||
|
motivoReprovacao = '';
|
||||||
|
showModalReprovar = true;
|
||||||
|
processandoReprovar = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fecharModalReprovar() {
|
||||||
|
showModalReprovar = false;
|
||||||
|
requisicaoParaReprovar = null;
|
||||||
|
motivoReprovacao = '';
|
||||||
|
processandoReprovar = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmarReprovar() {
|
||||||
|
if (!requisicaoParaReprovar) return;
|
||||||
|
|
||||||
|
if (!motivoReprovacao.trim()) {
|
||||||
|
mostrarMensagem('error', 'É necessário informar o motivo da reprovação');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.mutation(api.almoxarifado.atenderRequisicao, { id });
|
processandoReprovar = true;
|
||||||
|
await client.mutation(api.almoxarifado.reprovarRequisicao, {
|
||||||
|
id: requisicaoParaReprovar,
|
||||||
|
motivoReprovacao: motivoReprovacao.trim()
|
||||||
|
});
|
||||||
|
mostrarMensagem('success', 'Requisição reprovada com sucesso!');
|
||||||
|
fecharModalReprovar();
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Erro ao reprovar requisição';
|
||||||
|
mostrarMensagem('error', message);
|
||||||
|
} finally {
|
||||||
|
processandoReprovar = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let requisicaoParaAtender = $state<Id<'requisicoesMaterial'> | null>(null);
|
||||||
|
let showModalAtender = $state(false);
|
||||||
|
let processandoAtender = $state(false);
|
||||||
|
|
||||||
|
function abrirModalAtender(id: Id<'requisicoesMaterial'>) {
|
||||||
|
requisicaoParaAtender = id;
|
||||||
|
showModalAtender = true;
|
||||||
|
processandoAtender = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fecharModalAtender() {
|
||||||
|
showModalAtender = false;
|
||||||
|
requisicaoParaAtender = null;
|
||||||
|
processandoAtender = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmarAtender() {
|
||||||
|
if (!requisicaoParaAtender) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
processandoAtender = true;
|
||||||
|
await client.mutation(api.almoxarifado.atenderRequisicao, { id: requisicaoParaAtender });
|
||||||
mostrarMensagem('success', 'Requisição atendida com sucesso!');
|
mostrarMensagem('success', 'Requisição atendida com sucesso!');
|
||||||
|
fecharModalAtender();
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Erro ao atender requisição';
|
const message = error instanceof Error ? error.message : 'Erro ao atender requisição';
|
||||||
mostrarMensagem('error', message);
|
mostrarMensagem('error', message);
|
||||||
|
} finally {
|
||||||
|
processandoAtender = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,28 +576,50 @@
|
|||||||
<span class="badge badge-ghost">{setor?.nome || 'Carregando...'}</span>
|
<span class="badge badge-ghost">{setor?.nome || 'Carregando...'}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge {getStatusBadge(requisicao.status)} badge-lg">
|
<div class="flex flex-col gap-1">
|
||||||
{getStatusLabel(requisicao.status)}
|
<span class="badge {getStatusBadge(requisicao.status)} badge-lg">
|
||||||
</span>
|
{getStatusLabel(requisicao.status)}
|
||||||
|
</span>
|
||||||
|
{#if requisicao.status === 'rejeitada' && requisicao.motivoReprovacao}
|
||||||
|
<div class="text-xs text-error mt-1 max-w-xs truncate" title={requisicao.motivoReprovacao}>
|
||||||
|
<strong>Motivo:</strong> {requisicao.motivoReprovacao}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="text-sm">{new Date(requisicao.criadoEm).toLocaleDateString('pt-BR')}</span>
|
<span class="text-sm">{new Date(requisicao.criadoEm).toLocaleDateString('pt-BR')}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-ghost transition-all"
|
||||||
|
onclick={() => abrirModalVisualizar(requisicao._id)}
|
||||||
|
title="Visualizar requisição"
|
||||||
|
>
|
||||||
|
<Eye class="h-4 w-4" />
|
||||||
|
</button>
|
||||||
{#if requisicao.status === 'pendente'}
|
{#if requisicao.status === 'pendente'}
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-success transition-all"
|
class="btn btn-sm btn-success transition-all"
|
||||||
onclick={() => aprovarRequisicao(requisicao._id)}
|
onclick={() => abrirModalAprovar(requisicao._id)}
|
||||||
title="Aprovar requisição"
|
title="Aprovar requisição"
|
||||||
>
|
>
|
||||||
<CheckCircle class="h-4 w-4" />
|
<CheckCircle class="h-4 w-4" />
|
||||||
Aprovar
|
Aprovar
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-error transition-all"
|
||||||
|
onclick={() => abrirModalReprovar(requisicao._id)}
|
||||||
|
title="Reprovar requisição"
|
||||||
|
>
|
||||||
|
<XCircle class="h-4 w-4" />
|
||||||
|
Reprovar
|
||||||
|
</button>
|
||||||
{:else if requisicao.status === 'aprovada'}
|
{:else if requisicao.status === 'aprovada'}
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-primary transition-all"
|
class="btn btn-sm btn-primary transition-all"
|
||||||
onclick={() => atenderRequisicao(requisicao._id)}
|
onclick={() => abrirModalAtender(requisicao._id)}
|
||||||
title="Atender requisição"
|
title="Atender requisição"
|
||||||
>
|
>
|
||||||
<Package class="h-4 w-4" />
|
<Package class="h-4 w-4" />
|
||||||
@@ -695,6 +867,423 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Modal Aprovar Requisição -->
|
||||||
|
{#if showModalAprovar}
|
||||||
|
<div class="modal modal-open backdrop-blur-sm">
|
||||||
|
<div class="modal-box max-w-2xl border border-base-300 shadow-2xl">
|
||||||
|
<div class="mb-6 flex items-center gap-4 border-b-2 border-success/20 pb-4">
|
||||||
|
<div class="rounded-2xl bg-success/20 p-3">
|
||||||
|
<CheckCircle class="h-8 w-8 text-success" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="text-2xl font-bold text-base-content">Aprovar Requisição</h3>
|
||||||
|
<p class="text-base-content/70 mt-1">Confirmar aprovação da requisição</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-ghost btn-sm" onclick={fecharModalAprovar} disabled={processandoAprovar}>
|
||||||
|
<XCircle class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if problemasEstoqueAprovar.length > 0}
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="alert alert-warning border-warning/30 bg-warning/10">
|
||||||
|
<AlertTriangle class="h-5 w-5 text-warning" />
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold text-base-content">Estoque Insuficiente</h4>
|
||||||
|
<p class="text-sm text-base-content/80 mt-1">
|
||||||
|
A requisição contém materiais com estoque insuficiente:
|
||||||
|
</p>
|
||||||
|
<ul class="list-disc list-inside mt-2 text-sm text-base-content/80">
|
||||||
|
{#each problemasEstoqueAprovar as problema}
|
||||||
|
<li>
|
||||||
|
{problema.materialNome}: solicitado {problema.quantidadeSolicitada}, disponível {problema.estoqueAtual} (faltam {problema.falta})
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
<p class="text-sm text-base-content/80 mt-2">
|
||||||
|
A aprovação permitirá atender apenas a quantidade disponível em estoque.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="alert alert-info border-info/30 bg-info/10">
|
||||||
|
<CheckCircle class="h-5 w-5 text-info" />
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold text-base-content">Confirmar Aprovação</h4>
|
||||||
|
<p class="text-sm text-base-content/80 mt-1">
|
||||||
|
A requisição será aprovada e ficará disponível para atendimento.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="modal-action gap-3 border-t-2 border-base-300 pt-6">
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-lg min-w-[140px]"
|
||||||
|
onclick={fecharModalAprovar}
|
||||||
|
disabled={processandoAprovar}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-success btn-lg min-w-[200px] shadow-lg hover:shadow-xl"
|
||||||
|
onclick={confirmarAprovar}
|
||||||
|
disabled={processandoAprovar}
|
||||||
|
>
|
||||||
|
{#if processandoAprovar}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
Aprovando...
|
||||||
|
{:else}
|
||||||
|
<CheckCircle class="h-5 w-5" />
|
||||||
|
Confirmar Aprovação
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Modal Reprovar Requisição -->
|
||||||
|
{#if showModalReprovar}
|
||||||
|
<div class="modal modal-open backdrop-blur-sm">
|
||||||
|
<div class="modal-box max-w-2xl border border-base-300 shadow-2xl">
|
||||||
|
<div class="mb-6 flex items-center gap-4 border-b-2 border-error/20 pb-4">
|
||||||
|
<div class="rounded-2xl bg-error/20 p-3">
|
||||||
|
<XCircle class="h-8 w-8 text-error" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="text-2xl font-bold text-base-content">Reprovar Requisição</h3>
|
||||||
|
<p class="text-base-content/70 mt-1">Informe o motivo da reprovação</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-ghost btn-sm" onclick={fecharModalReprovar} disabled={processandoReprovar}>
|
||||||
|
<XCircle class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="alert alert-warning border-warning/30 bg-warning/10 mb-4">
|
||||||
|
<AlertTriangle class="h-5 w-5 text-warning" />
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold text-base-content">Confirmar Reprovação</h4>
|
||||||
|
<p class="text-sm text-base-content/80 mt-1">
|
||||||
|
O motivo informado será registrado e poderá ser visualizado pelo solicitante da requisição.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label pb-2">
|
||||||
|
<span class="label-text font-semibold flex items-center gap-2">
|
||||||
|
<FileText class="h-4 w-4" />
|
||||||
|
Motivo da Reprovação <span class="text-error">*</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
class="textarea textarea-bordered w-full focus:textarea-error transition-colors h-32"
|
||||||
|
placeholder="Descreva o motivo da reprovação desta requisição..."
|
||||||
|
bind:value={motivoReprovacao}
|
||||||
|
rows="5"
|
||||||
|
disabled={processandoReprovar}
|
||||||
|
></textarea>
|
||||||
|
<label class="label pt-1">
|
||||||
|
<span class="label-text-alt text-base-content/60">Este motivo será registrado e poderá ser visualizado pelo solicitante</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-action gap-3 border-t-2 border-base-300 pt-6">
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-lg min-w-[140px]"
|
||||||
|
onclick={fecharModalReprovar}
|
||||||
|
disabled={processandoReprovar}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-error btn-lg min-w-[200px] shadow-lg hover:shadow-xl"
|
||||||
|
onclick={confirmarReprovar}
|
||||||
|
disabled={!motivoReprovacao.trim() || processandoReprovar}
|
||||||
|
>
|
||||||
|
{#if processandoReprovar}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
Reprovando...
|
||||||
|
{:else}
|
||||||
|
<XCircle class="h-5 w-5" />
|
||||||
|
Confirmar Reprovação
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Modal Visualizar Requisição -->
|
||||||
|
{#if showModalVisualizar}
|
||||||
|
<div class="modal modal-open backdrop-blur-sm">
|
||||||
|
<div class="modal-box max-w-4xl border border-base-300 shadow-2xl">
|
||||||
|
<div class="mb-6 flex items-center gap-4 border-b-2 border-primary/20 pb-4">
|
||||||
|
<div class="rounded-2xl bg-primary/20 p-3">
|
||||||
|
<Eye class="h-8 w-8 text-primary" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="text-2xl font-bold text-base-content">Detalhes da Requisição</h3>
|
||||||
|
{#if carregandoDetalhes}
|
||||||
|
<p class="text-base-content/70 mt-1">Carregando...</p>
|
||||||
|
{:else if erroDetalhes}
|
||||||
|
<p class="text-base-content/70 mt-1 text-error">Erro: {erroDetalhes}</p>
|
||||||
|
{:else if requisicaoDetalhes}
|
||||||
|
<p class="text-base-content/70 mt-1">Número: <span class="font-mono font-bold text-primary">{requisicaoDetalhes.numero}</span></p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-base-content/70 mt-1">Carregando...</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-ghost btn-sm" onclick={fecharModalVisualizar}>
|
||||||
|
<XCircle class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if carregandoDetalhes}
|
||||||
|
<div class="flex items-center justify-center py-16">
|
||||||
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
{:else if erroDetalhes}
|
||||||
|
<div class="alert alert-error">
|
||||||
|
<AlertTriangle class="h-5 w-5" />
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold">Erro ao carregar detalhes</h4>
|
||||||
|
<p class="text-sm">{erroDetalhes}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if requisicaoDetalhes}
|
||||||
|
{@const solicitanteDetalhes = funcionariosQuery?.data?.find(f => f._id === requisicaoDetalhes.solicitanteId)}
|
||||||
|
{@const setorDetalhes = setoresQuery?.data?.find(s => s._id === requisicaoDetalhes.setorId)}
|
||||||
|
|
||||||
|
<!-- Informações Básicas -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h4 class="mb-4 text-lg font-bold flex items-center gap-2">
|
||||||
|
<FileText class="h-5 w-5" />
|
||||||
|
Informações Básicas
|
||||||
|
</h4>
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="text-sm text-base-content/70">Solicitante</div>
|
||||||
|
<div class="font-semibold">{solicitanteDetalhes?.nome || 'Carregando...'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="text-sm text-base-content/70">Setor</div>
|
||||||
|
<div class="font-semibold">{setorDetalhes?.nome || 'Carregando...'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="text-sm text-base-content/70">Status</div>
|
||||||
|
<span class="badge {getStatusBadge(requisicaoDetalhes.status)} badge-lg">
|
||||||
|
{getStatusLabel(requisicaoDetalhes.status)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="text-sm text-base-content/70">Data de Criação</div>
|
||||||
|
<div class="font-semibold">{new Date(requisicaoDetalhes.criadoEm).toLocaleString('pt-BR')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if requisicaoDetalhes.aprovadoPor}
|
||||||
|
{@const aprovador = funcionariosQuery.data?.find(f => f._id === requisicaoDetalhes.aprovadoPor)}
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="text-sm text-base-content/70">Aprovado por</div>
|
||||||
|
<div class="font-semibold">{aprovador?.nome || 'Carregando...'}</div>
|
||||||
|
{#if requisicaoDetalhes.dataAprovacao}
|
||||||
|
<div class="text-xs text-base-content/60 mt-1">
|
||||||
|
{new Date(requisicaoDetalhes.dataAprovacao).toLocaleString('pt-BR')}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if requisicaoDetalhes.reprovadoPor}
|
||||||
|
{@const reprovador = funcionariosQuery.data?.find(f => f._id === requisicaoDetalhes.reprovadoPor)}
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="text-sm text-base-content/70">Reprovado por</div>
|
||||||
|
<div class="font-semibold">{reprovador?.nome || 'Carregando...'}</div>
|
||||||
|
{#if requisicaoDetalhes.dataReprovacao}
|
||||||
|
<div class="text-xs text-base-content/60 mt-1">
|
||||||
|
{new Date(requisicaoDetalhes.dataReprovacao).toLocaleString('pt-BR')}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if requisicaoDetalhes.motivoReprovacao}
|
||||||
|
<div class="mt-2 text-sm">
|
||||||
|
<div class="text-xs text-base-content/70">Motivo:</div>
|
||||||
|
<div class="text-error">{requisicaoDetalhes.motivoReprovacao}</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Itens da Requisição -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h4 class="mb-4 text-lg font-bold flex items-center gap-2">
|
||||||
|
<Package class="h-5 w-5" />
|
||||||
|
Itens da Requisição
|
||||||
|
</h4>
|
||||||
|
{#if requisicaoDetalhes.itens && requisicaoDetalhes.itens.length > 0}
|
||||||
|
<div class="overflow-x-auto rounded-lg border border-base-300">
|
||||||
|
<table class="table table-zebra">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-base-200">
|
||||||
|
<th class="font-bold text-base-content">Material</th>
|
||||||
|
<th class="font-bold text-base-content">Quantidade Solicitada</th>
|
||||||
|
<th class="font-bold text-base-content">Quantidade Atendida</th>
|
||||||
|
<th class="font-bold text-base-content">Observações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each requisicaoDetalhes.itens as item}
|
||||||
|
{@const material = materiaisQuery.data?.find(m => m._id === item.materialId)}
|
||||||
|
<tr class="hover:bg-base-200/50 transition-colors">
|
||||||
|
<td>
|
||||||
|
<div class="font-medium">{material?.nome || 'Carregando...'}</div>
|
||||||
|
{#if material?.codigo}
|
||||||
|
<div class="text-xs text-base-content/50 font-mono">{material.codigo}</div>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="font-semibold">{item.quantidadeSolicitada}</span>
|
||||||
|
<span class="text-sm text-base-content/60 ml-1">{material?.unidadeMedida || ''}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{#if item.quantidadeAtendida !== undefined}
|
||||||
|
<span class="font-semibold">{item.quantidadeAtendida}</span>
|
||||||
|
<span class="text-sm text-base-content/60 ml-1">{material?.unidadeMedida || ''}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="text-base-content/50">-</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{#if item.observacoes}
|
||||||
|
<div class="text-sm">{item.observacoes}</div>
|
||||||
|
{:else}
|
||||||
|
<span class="text-base-content/50">-</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<Package class="h-5 w-5" />
|
||||||
|
<span>Nenhum item encontrado nesta requisição</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Observações Gerais -->
|
||||||
|
{#if requisicaoDetalhes.observacoes}
|
||||||
|
<div class="mb-6">
|
||||||
|
<h4 class="mb-4 text-lg font-bold flex items-center gap-2">
|
||||||
|
<FileText class="h-5 w-5" />
|
||||||
|
Observações Gerais
|
||||||
|
</h4>
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<p class="text-sm whitespace-pre-wrap">{requisicaoDetalhes.observacoes}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="modal-action">
|
||||||
|
<button class="btn btn-ghost" onclick={fecharModalVisualizar}>
|
||||||
|
Fechar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex items-center justify-center py-16">
|
||||||
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-action">
|
||||||
|
<button class="btn btn-ghost" onclick={fecharModalVisualizar}>
|
||||||
|
Fechar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Modal Atender Requisição -->
|
||||||
|
{#if showModalAtender}
|
||||||
|
<div class="modal modal-open backdrop-blur-sm">
|
||||||
|
<div class="modal-box max-w-2xl border border-base-300 shadow-2xl">
|
||||||
|
<div class="mb-6 flex items-center gap-4 border-b-2 border-primary/20 pb-4">
|
||||||
|
<div class="rounded-2xl bg-primary/20 p-3">
|
||||||
|
<Package class="h-8 w-8 text-primary" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="text-2xl font-bold text-base-content">Atender Requisição</h3>
|
||||||
|
<p class="text-base-content/70 mt-1">Registrar saídas de material</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-ghost btn-sm" onclick={fecharModalAtender} disabled={processandoAtender}>
|
||||||
|
<XCircle class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="alert alert-info border-info/30 bg-info/10">
|
||||||
|
<FileText class="h-5 w-5 text-info" />
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold text-base-content">Confirmar Atendimento</h4>
|
||||||
|
<p class="text-sm text-base-content/80 mt-1">
|
||||||
|
Ao confirmar, serão registradas as saídas de material do estoque conforme os itens da requisição.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<p class="text-base-content">
|
||||||
|
Tem certeza que deseja atender esta requisição? Isso registrará as saídas de material no estoque.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-action gap-3 border-t-2 border-base-300 pt-6">
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-lg min-w-[140px]"
|
||||||
|
onclick={fecharModalAtender}
|
||||||
|
disabled={processandoAtender}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-lg min-w-[200px] shadow-lg hover:shadow-xl"
|
||||||
|
onclick={confirmarAtender}
|
||||||
|
disabled={processandoAtender}
|
||||||
|
>
|
||||||
|
{#if processandoAtender}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
Atendendo...
|
||||||
|
{:else}
|
||||||
|
<Package class="h-5 w-5" />
|
||||||
|
Confirmar Atendimento
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1132,6 +1132,53 @@ export const criarRequisicao = mutation({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const verificarEstoqueRequisicao = query({
|
||||||
|
args: { id: v.id('requisicoesMaterial') },
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||||
|
recurso: 'almoxarifado',
|
||||||
|
acao: 'aprovar_requisicao'
|
||||||
|
});
|
||||||
|
|
||||||
|
const requisicao = await ctx.db.get(args.id);
|
||||||
|
if (!requisicao) throw new Error('Requisição não encontrada');
|
||||||
|
|
||||||
|
// Buscar itens da requisição
|
||||||
|
const itens = await ctx.db
|
||||||
|
.query('requisicaoItens')
|
||||||
|
.withIndex('by_requisicaoId', (q) => q.eq('requisicaoId', args.id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
const problemasEstoque: Array<{
|
||||||
|
materialId: Id<'materiais'>;
|
||||||
|
materialNome: string;
|
||||||
|
quantidadeSolicitada: number;
|
||||||
|
estoqueAtual: number;
|
||||||
|
falta: number;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (const item of itens) {
|
||||||
|
const material = await ctx.db.get(item.materialId);
|
||||||
|
if (!material) continue;
|
||||||
|
|
||||||
|
if (material.estoqueAtual < item.quantidadeSolicitada) {
|
||||||
|
problemasEstoque.push({
|
||||||
|
materialId: material._id,
|
||||||
|
materialNome: material.nome,
|
||||||
|
quantidadeSolicitada: item.quantidadeSolicitada,
|
||||||
|
estoqueAtual: material.estoqueAtual,
|
||||||
|
falta: item.quantidadeSolicitada - material.estoqueAtual
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
temEstoqueSuficiente: problemasEstoque.length === 0,
|
||||||
|
problemasEstoque
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const aprovarRequisicao = mutation({
|
export const aprovarRequisicao = mutation({
|
||||||
args: {
|
args: {
|
||||||
id: v.id('requisicoesMaterial'),
|
id: v.id('requisicoesMaterial'),
|
||||||
@@ -1149,16 +1196,55 @@ export const aprovarRequisicao = mutation({
|
|||||||
throw new Error('Apenas requisições pendentes podem ser aprovadas');
|
throw new Error('Apenas requisições pendentes podem ser aprovadas');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verificar estoque antes de aprovar
|
||||||
|
const itens = await ctx.db
|
||||||
|
.query('requisicaoItens')
|
||||||
|
.withIndex('by_requisicaoId', (q) => q.eq('requisicaoId', args.id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
const problemasEstoque: Array<{
|
||||||
|
materialNome: string;
|
||||||
|
quantidadeSolicitada: number;
|
||||||
|
estoqueAtual: number;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (const item of itens) {
|
||||||
|
const material = await ctx.db.get(item.materialId);
|
||||||
|
if (!material) continue;
|
||||||
|
|
||||||
|
if (material.estoqueAtual < item.quantidadeSolicitada) {
|
||||||
|
problemasEstoque.push({
|
||||||
|
materialNome: material.nome,
|
||||||
|
quantidadeSolicitada: item.quantidadeSolicitada,
|
||||||
|
estoqueAtual: material.estoqueAtual
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (problemasEstoque.length > 0) {
|
||||||
|
const mensagem = `Estoque insuficiente para os seguintes materiais:\n${problemasEstoque.map(p => `- ${p.materialNome}: solicitado ${p.quantidadeSolicitada}, disponível ${p.estoqueAtual}`).join('\n')}`;
|
||||||
|
throw new Error(mensagem);
|
||||||
|
}
|
||||||
|
|
||||||
const usuario = await getCurrentUserFunction(ctx);
|
const usuario = await getCurrentUserFunction(ctx);
|
||||||
if (!usuario) throw new Error('Usuário não autenticado');
|
if (!usuario) throw new Error('Usuário não autenticado');
|
||||||
|
|
||||||
// Buscar funcionário do usuário
|
// Buscar funcionário do usuário - primeiro tenta por funcionarioId, depois por email
|
||||||
const funcionario = await ctx.db
|
let funcionario;
|
||||||
.query('funcionarios')
|
if (usuario.funcionarioId) {
|
||||||
.filter((q) => q.eq(q.field('email'), usuario.email))
|
funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||||
.first();
|
}
|
||||||
|
|
||||||
if (!funcionario) throw new Error('Funcionário não encontrado para o usuário');
|
if (!funcionario) {
|
||||||
|
funcionario = await ctx.db
|
||||||
|
.query('funcionarios')
|
||||||
|
.filter((q) => q.eq(q.field('email'), usuario.email))
|
||||||
|
.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!funcionario) {
|
||||||
|
throw new Error('Funcionário não encontrado para o usuário');
|
||||||
|
}
|
||||||
|
|
||||||
await ctx.db.patch(args.id, {
|
await ctx.db.patch(args.id, {
|
||||||
status: 'aprovada',
|
status: 'aprovada',
|
||||||
@@ -1176,6 +1262,64 @@ export const aprovarRequisicao = mutation({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const reprovarRequisicao = mutation({
|
||||||
|
args: {
|
||||||
|
id: v.id('requisicoesMaterial'),
|
||||||
|
motivoReprovacao: v.string()
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||||
|
recurso: 'almoxarifado',
|
||||||
|
acao: 'aprovar_requisicao'
|
||||||
|
});
|
||||||
|
|
||||||
|
const requisicao = await ctx.db.get(args.id);
|
||||||
|
if (!requisicao) throw new Error('Requisição não encontrada');
|
||||||
|
if (requisicao.status !== 'pendente') {
|
||||||
|
throw new Error('Apenas requisições pendentes podem ser reprovadas');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!args.motivoReprovacao.trim()) {
|
||||||
|
throw new Error('É necessário informar o motivo da reprovação');
|
||||||
|
}
|
||||||
|
|
||||||
|
const usuario = await getCurrentUserFunction(ctx);
|
||||||
|
if (!usuario) throw new Error('Usuário não autenticado');
|
||||||
|
|
||||||
|
// Buscar funcionário do usuário - primeiro tenta por funcionarioId, depois por email
|
||||||
|
let funcionario;
|
||||||
|
if (usuario.funcionarioId) {
|
||||||
|
funcionario = await ctx.db.get(usuario.funcionarioId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!funcionario) {
|
||||||
|
funcionario = await ctx.db
|
||||||
|
.query('funcionarios')
|
||||||
|
.filter((q) => q.eq(q.field('email'), usuario.email))
|
||||||
|
.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!funcionario) {
|
||||||
|
throw new Error('Funcionário não encontrado para o usuário');
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.db.patch(args.id, {
|
||||||
|
status: 'rejeitada',
|
||||||
|
reprovadoPor: funcionario._id,
|
||||||
|
dataReprovacao: Date.now(),
|
||||||
|
motivoReprovacao: args.motivoReprovacao.trim(),
|
||||||
|
atualizadoEm: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Registrar histórico
|
||||||
|
await registrarHistorico(ctx, 'requisicao', args.id.toString(), 'edicao', requisicao, {
|
||||||
|
status: 'rejeitada',
|
||||||
|
reprovadoPor: funcionario._id,
|
||||||
|
motivoReprovacao: args.motivoReprovacao.trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const atenderRequisicao = mutation({
|
export const atenderRequisicao = mutation({
|
||||||
args: {
|
args: {
|
||||||
id: v.id('requisicoesMaterial')
|
id: v.id('requisicoesMaterial')
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const createAuth = (
|
|||||||
logger: {
|
logger: {
|
||||||
disabled: optionsOnly
|
disabled: optionsOnly
|
||||||
},
|
},
|
||||||
trustedOrigins: ['https://vite.kilder.dev'],
|
trustedOrigins: ['https://vite.kilder.dev', 'http://localhost:5173', 'http://127.0.0.1:5173'],
|
||||||
baseURL: siteUrl,
|
baseURL: siteUrl,
|
||||||
database: authComponent.adapter(ctx),
|
database: authComponent.adapter(ctx),
|
||||||
// Configure simple, non-verified email/password to get started
|
// Configure simple, non-verified email/password to get started
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ export const almoxarifadoTables = {
|
|||||||
status: requisicaoStatus,
|
status: requisicaoStatus,
|
||||||
aprovadoPor: v.optional(v.id('funcionarios')),
|
aprovadoPor: v.optional(v.id('funcionarios')),
|
||||||
dataAprovacao: v.optional(v.number()),
|
dataAprovacao: v.optional(v.number()),
|
||||||
|
reprovadoPor: v.optional(v.id('funcionarios')),
|
||||||
|
dataReprovacao: v.optional(v.number()),
|
||||||
|
motivoReprovacao: v.optional(v.string()),
|
||||||
observacoes: v.optional(v.string()),
|
observacoes: v.optional(v.string()),
|
||||||
criadoEm: v.number(),
|
criadoEm: v.number(),
|
||||||
atualizadoEm: v.number()
|
atualizadoEm: v.number()
|
||||||
|
|||||||
Reference in New Issue
Block a user