feat: Add confirmation modal for item actions and enhance user feedback with toast notifications in pedidos management.
This commit is contained in:
133
apps/web/src/lib/components/ConfirmationModal.svelte
Normal file
133
apps/web/src/lib/components/ConfirmationModal.svelte
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertTriangle, X } from 'lucide-svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
title?: string;
|
||||||
|
message: string;
|
||||||
|
confirmText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
isDestructive?: boolean;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
open = $bindable(false),
|
||||||
|
title = 'Confirmar Ação',
|
||||||
|
message,
|
||||||
|
confirmText = 'Confirmar',
|
||||||
|
cancelText = 'Cancelar',
|
||||||
|
isDestructive = false,
|
||||||
|
onConfirm,
|
||||||
|
onClose
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
// Tenta centralizar, mas se tiver um contexto específico pode ser ajustado
|
||||||
|
// Por padrão, centralizado.
|
||||||
|
function getModalStyle() {
|
||||||
|
return 'position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100%; max-width: 500px;';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
open = false;
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConfirm() {
|
||||||
|
open = false;
|
||||||
|
onConfirm();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if open}
|
||||||
|
<div
|
||||||
|
class="pointer-events-none fixed inset-0 z-50"
|
||||||
|
style="animation: fadeIn 0.2s ease-out;"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="modal-confirm-title"
|
||||||
|
>
|
||||||
|
<!-- Backdrop leve -->
|
||||||
|
<div
|
||||||
|
class="pointer-events-auto absolute inset-0 bg-black/20 transition-opacity duration-200"
|
||||||
|
onclick={handleClose}
|
||||||
|
aria-hidden="true"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Modal Box -->
|
||||||
|
<div
|
||||||
|
class="pointer-events-auto absolute z-10 flex w-full max-w-lg flex-col overflow-hidden rounded-2xl bg-white shadow-2xl transition-all duration-300"
|
||||||
|
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); {getModalStyle()}"
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex shrink-0 items-center justify-between border-b border-gray-100 px-6 py-4">
|
||||||
|
<h2
|
||||||
|
id="modal-confirm-title"
|
||||||
|
class="flex items-center gap-2 text-xl font-bold {isDestructive
|
||||||
|
? 'text-red-600'
|
||||||
|
: 'text-gray-900'}"
|
||||||
|
>
|
||||||
|
{#if isDestructive}
|
||||||
|
<AlertTriangle class="h-6 w-6" strokeWidth={2.5} />
|
||||||
|
{/if}
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
||||||
|
onclick={handleClose}
|
||||||
|
aria-label="Fechar"
|
||||||
|
>
|
||||||
|
<X class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="flex-1 overflow-y-auto px-6 py-6">
|
||||||
|
<p class="text-base leading-relaxed font-medium text-gray-700">{message}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="flex shrink-0 justify-end gap-3 border-t border-gray-100 bg-gray-50 px-6 py-4">
|
||||||
|
<button
|
||||||
|
class="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-200"
|
||||||
|
onclick={handleClose}
|
||||||
|
>
|
||||||
|
{cancelText}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="rounded-lg px-4 py-2 text-sm font-medium text-white shadow-sm {isDestructive
|
||||||
|
? 'bg-red-600 hover:bg-red-700 focus:ring-red-500'
|
||||||
|
: 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-500'}"
|
||||||
|
onclick={handleConfirm}
|
||||||
|
>
|
||||||
|
{confirmText}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
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 { SvelteSet } from 'svelte/reactivity';
|
import { SvelteSet } from 'svelte/reactivity';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import ConfirmationModal from '$lib/components/ConfirmationModal.svelte';
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
@@ -156,6 +158,43 @@
|
|||||||
let showDetailsModal = $state(false);
|
let showDetailsModal = $state(false);
|
||||||
let selectedObjeto = $state<Doc<'objetos'> | null>(null);
|
let selectedObjeto = $state<Doc<'objetos'> | null>(null);
|
||||||
|
|
||||||
|
// Confirmation Modal State
|
||||||
|
let confirmModal = $state({
|
||||||
|
open: false,
|
||||||
|
title: '',
|
||||||
|
message: '',
|
||||||
|
confirmText: 'Confirmar',
|
||||||
|
cancelText: 'Cancelar',
|
||||||
|
isDestructive: false,
|
||||||
|
onConfirm: async () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
function openConfirm(
|
||||||
|
title: string,
|
||||||
|
message: string,
|
||||||
|
onConfirm: () => Promise<void> | void,
|
||||||
|
options: {
|
||||||
|
confirmText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
isDestructive?: boolean;
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
confirmModal.title = title;
|
||||||
|
confirmModal.message = message;
|
||||||
|
confirmModal.confirmText = options.confirmText || 'Confirmar';
|
||||||
|
confirmModal.cancelText = options.cancelText || 'Cancelar';
|
||||||
|
confirmModal.isDestructive = options.isDestructive || false;
|
||||||
|
confirmModal.onConfirm = async () => {
|
||||||
|
try {
|
||||||
|
await onConfirm();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error in confirmation action:', e);
|
||||||
|
toast.error('Erro ao executar ação: ' + (e as Error).message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
confirmModal.open = true;
|
||||||
|
}
|
||||||
|
|
||||||
function openDetails(objetoId: string) {
|
function openDetails(objetoId: string) {
|
||||||
const obj = objetos.find((o) => o._id === objetoId);
|
const obj = objetos.find((o) => o._id === objetoId);
|
||||||
if (obj) {
|
if (obj) {
|
||||||
@@ -232,11 +271,13 @@
|
|||||||
ataId: ''
|
ataId: ''
|
||||||
};
|
};
|
||||||
showAddItem = false;
|
showAddItem = false;
|
||||||
if (pedido.status === 'em_analise') {
|
if (pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite') {
|
||||||
alert('Solicitação de adição enviada para análise.');
|
toast.success('Solicitação de adição enviada para análise.');
|
||||||
|
} else {
|
||||||
|
toast.success('Item adicionado com sucesso!');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Erro ao adicionar item: ' + (e as Error).message);
|
toast.error('Erro ao adicionar item: ' + (e as Error).message);
|
||||||
} finally {
|
} finally {
|
||||||
addingItem = false;
|
addingItem = false;
|
||||||
}
|
}
|
||||||
@@ -246,7 +287,7 @@
|
|||||||
if (!pedido) return;
|
if (!pedido) return;
|
||||||
|
|
||||||
if (novaQuantidade < 1) {
|
if (novaQuantidade < 1) {
|
||||||
alert('Quantidade deve ser pelo menos 1.');
|
toast.error('Quantidade deve ser pelo menos 1.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -254,61 +295,79 @@
|
|||||||
itemId,
|
itemId,
|
||||||
novaQuantidade
|
novaQuantidade
|
||||||
});
|
});
|
||||||
if (pedido.status === 'em_analise') {
|
if (pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite') {
|
||||||
alert('Solicitação de alteração de quantidade enviada para análise.');
|
toast.success('Solicitação de alteração de quantidade enviada para análise.');
|
||||||
|
} else {
|
||||||
|
toast.success('Quantidade atualizada com sucesso!');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Erro ao atualizar quantidade: ' + (e as Error).message);
|
toast.error('Erro ao atualizar quantidade: ' + (e as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemoveItem(itemId: Id<'objetoItems'>) {
|
function handleRemoveItem(itemId: Id<'objetoItems'>) {
|
||||||
if (!pedido) return;
|
if (!pedido) return;
|
||||||
if (!confirm('Remover este item?')) return;
|
openConfirm(
|
||||||
try {
|
'Remover Item',
|
||||||
await client.mutation(api.pedidos.removeItem, { itemId });
|
'Tem certeza que deseja remover este item do pedido?',
|
||||||
if (pedido.status === 'em_analise') {
|
async () => {
|
||||||
alert('Solicitação de remoção enviada para análise.');
|
await client.mutation(api.pedidos.removeItem, { itemId });
|
||||||
}
|
if (pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite') {
|
||||||
} catch (e) {
|
toast.success('Solicitação de remoção enviada para análise.');
|
||||||
alert('Erro ao remover item: ' + (e as Error).message);
|
} else {
|
||||||
}
|
toast.success('Item removido com sucesso!');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ isDestructive: true, confirmText: 'Remover' }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleEnviarParaAceite() {
|
function handleEnviarParaAceite() {
|
||||||
if (!confirm('Enviar para aceite?')) return;
|
openConfirm(
|
||||||
try {
|
'Enviar para Aceite',
|
||||||
await client.mutation(api.pedidos.enviarParaAceite, { pedidoId });
|
'Tem certeza que deseja enviar este pedido para aceite? Ele ficará aguardando análise do setor responsável.',
|
||||||
} catch (e) {
|
async () => {
|
||||||
alert('Erro: ' + (e as Error).message);
|
await client.mutation(api.pedidos.enviarParaAceite, { pedidoId });
|
||||||
}
|
toast.success('Pedido enviado para aceite com sucesso!');
|
||||||
|
},
|
||||||
|
{ confirmText: 'Enviar' }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleIniciarAnalise() {
|
function handleIniciarAnalise() {
|
||||||
if (!confirm('Iniciar análise?')) return;
|
openConfirm(
|
||||||
try {
|
'Iniciar Análise',
|
||||||
await client.mutation(api.pedidos.iniciarAnalise, { pedidoId });
|
'Tem certeza que deseja iniciar a análise deste pedido? O status será alterado para "Em Análise".',
|
||||||
} catch (e) {
|
async () => {
|
||||||
alert('Erro: ' + (e as Error).message);
|
await client.mutation(api.pedidos.iniciarAnalise, { pedidoId });
|
||||||
}
|
toast.success('Análise iniciada com sucesso!');
|
||||||
|
},
|
||||||
|
{ confirmText: 'Iniciar Análise' }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleConcluir() {
|
function handleConcluir() {
|
||||||
if (!confirm('Concluir pedido?')) return;
|
openConfirm(
|
||||||
try {
|
'Concluir Pedido',
|
||||||
await client.mutation(api.pedidos.concluirPedido, { pedidoId });
|
'Tem certeza que deseja concluir este pedido? Esta ação não pode ser desfeita.',
|
||||||
} catch (e) {
|
async () => {
|
||||||
alert('Erro: ' + (e as Error).message);
|
await client.mutation(api.pedidos.concluirPedido, { pedidoId });
|
||||||
}
|
toast.success('Pedido concluído com sucesso!');
|
||||||
|
},
|
||||||
|
{ confirmText: 'Concluir Pedido' }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCancelar() {
|
function handleCancelar() {
|
||||||
if (!confirm('Cancelar pedido?')) return;
|
openConfirm(
|
||||||
try {
|
'Cancelar Pedido',
|
||||||
await client.mutation(api.pedidos.cancelarPedido, { pedidoId });
|
'Tem certeza que deseja cancelar este pedido? Esta ação não pode ser desfeita e o pedido ficará inutilizável.',
|
||||||
} catch (e) {
|
async () => {
|
||||||
alert('Erro: ' + (e as Error).message);
|
await client.mutation(api.pedidos.cancelarPedido, { pedidoId });
|
||||||
}
|
toast.success('Pedido cancelado com sucesso!');
|
||||||
|
},
|
||||||
|
{ isDestructive: true, confirmText: 'Cancelar Pedido' }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getObjetoName(id: string) {
|
function getObjetoName(id: string) {
|
||||||
@@ -411,8 +470,14 @@
|
|||||||
acaoId: current.acaoId ? (current.acaoId as Id<'acoes'>) : undefined,
|
acaoId: current.acaoId ? (current.acaoId as Id<'acoes'>) : undefined,
|
||||||
ataId: current.ataId ? (current.ataId as Id<'atas'>) : undefined
|
ataId: current.ataId ? (current.ataId as Id<'atas'>) : undefined
|
||||||
});
|
});
|
||||||
|
delete editingItems[item._id]; // Clear editing state
|
||||||
|
if (pedido?.status === 'em_analise' || pedido?.status === 'aguardando_aceite') {
|
||||||
|
toast.success('Solicitação de alteração enviada para análise.');
|
||||||
|
} else {
|
||||||
|
toast.success('Item atualizado com sucesso!');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Erro ao atualizar item: ' + (e as Error).message);
|
toast.error('Erro ao atualizar item: ' + (e as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,18 +502,19 @@
|
|||||||
|
|
||||||
async function handleUpdateSei() {
|
async function handleUpdateSei() {
|
||||||
if (!seiValue.trim()) {
|
if (!seiValue.trim()) {
|
||||||
alert('O número SEI não pode estar vazio.');
|
toast.error('O número SEI não pode estar vazio.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updatingSei = true;
|
updatingSei = true;
|
||||||
try {
|
try {
|
||||||
await client.mutation(api.pedidos.updateSeiNumber, {
|
await client.mutation(api.pedidos.updateSeiNumber, {
|
||||||
pedidoId,
|
pedidoId,
|
||||||
numeroSei: seiValue.trim()
|
numeroSei: seiValue
|
||||||
});
|
});
|
||||||
editingSei = false;
|
editingSei = false;
|
||||||
|
toast.success('Número SEI atualizado com sucesso!');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Erro ao atualizar número SEI: ' + (e as Error).message);
|
toast.error('Erro ao atualizar número SEI: ' + (e as Error).message);
|
||||||
} finally {
|
} finally {
|
||||||
updatingSei = false;
|
updatingSei = false;
|
||||||
}
|
}
|
||||||
@@ -553,26 +619,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemoveSelectedItems() {
|
function handleRemoveSelectedItems() {
|
||||||
if (!hasSelection) return;
|
if (!hasSelection) return;
|
||||||
if (
|
openConfirm(
|
||||||
!confirm(
|
'Remover Itens Selecionados',
|
||||||
selectedCount === 1
|
selectedCount === 1
|
||||||
? 'Remover o item selecionado deste pedido?'
|
? 'Tem certeza que deseja remover o item selecionado deste pedido?'
|
||||||
: `Remover os ${selectedCount} itens selecionados deste pedido?`
|
: `Tem certeza que deseja remover os ${selectedCount} itens selecionados deste pedido?`,
|
||||||
)
|
async () => {
|
||||||
)
|
await client.mutation(api.pedidos.removeItemsBatch, {
|
||||||
return;
|
itemIds: Array.from(selectedItemIds) as Id<'objetoItems'>[]
|
||||||
|
});
|
||||||
try {
|
clearSelection();
|
||||||
const itemIds = Array.from(selectedItemIds) as Id<'objetoItems'>[];
|
toast.success('Itens removidos com sucesso!');
|
||||||
await client.mutation(api.pedidos.removeItemsBatch, {
|
},
|
||||||
itemIds
|
{ isDestructive: true, confirmText: 'Remover Selecionados' }
|
||||||
});
|
);
|
||||||
clearSelection();
|
|
||||||
} catch (e) {
|
|
||||||
alert('Erro ao remover itens selecionados: ' + (e as Error).message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let showSplitResultModal = $state(false);
|
let showSplitResultModal = $state(false);
|
||||||
@@ -603,8 +665,9 @@
|
|||||||
showSplitConfirmationModal = false;
|
showSplitConfirmationModal = false;
|
||||||
showSplitResultModal = true;
|
showSplitResultModal = true;
|
||||||
clearSelection();
|
clearSelection();
|
||||||
|
toast.success('Pedido dividido com sucesso!');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Erro ao dividir pedido: ' + (e as Error).message);
|
toast.error('Erro ao dividir pedido: ' + (e as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -619,7 +682,7 @@
|
|||||||
|
|
||||||
async function confirmRequestAdjustments() {
|
async function confirmRequestAdjustments() {
|
||||||
if (!adjustmentDescription.trim()) {
|
if (!adjustmentDescription.trim()) {
|
||||||
alert('Por favor, informe a descrição dos ajustes necessários.');
|
toast.error('Por favor, informe a descrição dos ajustes necessários.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -628,36 +691,98 @@
|
|||||||
descricao: adjustmentDescription
|
descricao: adjustmentDescription
|
||||||
});
|
});
|
||||||
showRequestAdjustmentsModal = false;
|
showRequestAdjustmentsModal = false;
|
||||||
|
toast.success('Solicitação de ajustes enviada com sucesso!');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Erro: ' + (e as Error).message);
|
toast.error('Erro ao solicitar ajustes: ' + (e as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleConcluirAjustes() {
|
function handleConcluirAjustes() {
|
||||||
if (!confirm('Concluir ajustes e retornar para análise?')) return;
|
openConfirm(
|
||||||
try {
|
'Concluir Ajustes',
|
||||||
await client.mutation(api.pedidos.concluirAjustes, { pedidoId });
|
'Tem certeza que deseja concluir os ajustes e retornar o pedido para análise?',
|
||||||
} catch (e) {
|
async () => {
|
||||||
alert('Erro: ' + (e as Error).message);
|
await client.mutation(api.pedidos.concluirAjustes, { pedidoId });
|
||||||
}
|
toast.success('Ajustes concluídos com sucesso!');
|
||||||
|
},
|
||||||
|
{ confirmText: 'Concluir Ajustes' }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleApproveRequest(requestId: Id<'solicitacoesItens'>) {
|
function handleApproveRequest(requestId: Id<'solicitacoesItens'>) {
|
||||||
if (!confirm('Aprovar esta solicitação?')) return;
|
openConfirm(
|
||||||
try {
|
'Aprovar Solicitação',
|
||||||
await client.mutation(api.pedidos.approveItemRequest, { requestId });
|
'Tem certeza que deseja aprovar esta solicitação?',
|
||||||
} catch (e) {
|
async () => {
|
||||||
alert('Erro ao aprovar: ' + (e as Error).message);
|
await client.mutation(api.pedidos.approveItemRequest, { requestId });
|
||||||
}
|
toast.success('Solicitação aprovada com sucesso!');
|
||||||
|
},
|
||||||
|
{ confirmText: 'Aprovar' }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRejectRequest(requestId: Id<'solicitacoesItens'>) {
|
function handleRejectRequest(requestId: Id<'solicitacoesItens'>) {
|
||||||
if (!confirm('Rejeitar esta solicitação?')) return;
|
openConfirm(
|
||||||
try {
|
'Rejeitar Solicitação',
|
||||||
await client.mutation(api.pedidos.rejectItemRequest, { requestId });
|
'Tem certeza que deseja rejeitar esta solicitação?',
|
||||||
} catch (e) {
|
async () => {
|
||||||
alert('Erro ao rejeitar: ' + (e as Error).message);
|
await client.mutation(api.pedidos.rejectItemRequest, { requestId });
|
||||||
|
toast.success('Solicitação rejeitada com sucesso!');
|
||||||
|
},
|
||||||
|
{ isDestructive: true, confirmText: 'Rejeitar' }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeChangedDetails(data: unknown): string {
|
||||||
|
if (!data || typeof data !== 'object') {
|
||||||
|
return 'Alteração de detalhes do item';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { de, para } = data as {
|
||||||
|
de?: {
|
||||||
|
valorEstimado?: string;
|
||||||
|
modalidade?: Modalidade;
|
||||||
|
acaoId?: string | null;
|
||||||
|
ataId?: string | null;
|
||||||
|
};
|
||||||
|
para?: {
|
||||||
|
valorEstimado?: string;
|
||||||
|
modalidade?: Modalidade;
|
||||||
|
acaoId?: string | null;
|
||||||
|
ataId?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!de || !para) {
|
||||||
|
return 'Alteração de detalhes do item';
|
||||||
|
}
|
||||||
|
|
||||||
|
const changed: string[] = [];
|
||||||
|
|
||||||
|
if (de.valorEstimado !== para.valorEstimado) {
|
||||||
|
changed.push('valor estimado');
|
||||||
|
}
|
||||||
|
if (de.modalidade !== para.modalidade) {
|
||||||
|
changed.push('modalidade');
|
||||||
|
}
|
||||||
|
if ((de.acaoId ?? null) !== (para.acaoId ?? null)) {
|
||||||
|
changed.push('ação');
|
||||||
|
}
|
||||||
|
if ((de.ataId ?? null) !== (para.ataId ?? null)) {
|
||||||
|
changed.push('ata');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed.length === 0) {
|
||||||
|
return 'Alteração de detalhes do item';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed.length === 1) {
|
||||||
|
return `Alteração de ${changed[0]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = changed.pop()!;
|
||||||
|
const prefix = changed.join(', ');
|
||||||
|
return `Alteração de ${prefix} e ${last}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseRequestData(json: string) {
|
function parseRequestData(json: string) {
|
||||||
@@ -824,6 +949,8 @@
|
|||||||
<span class="text-blue-600">Alteração Qtd</span>
|
<span class="text-blue-600">Alteração Qtd</span>
|
||||||
{:else if req.tipo === 'exclusao'}
|
{:else if req.tipo === 'exclusao'}
|
||||||
<span class="text-red-600">Exclusão</span>
|
<span class="text-red-600">Exclusão</span>
|
||||||
|
{:else if req.tipo === 'alteracao_detalhes'}
|
||||||
|
<span class="text-purple-600">{describeChangedDetails(data)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2">{req.solicitadoPorNome}</td>
|
<td class="px-4 py-2">{req.solicitadoPorNome}</td>
|
||||||
@@ -844,6 +971,14 @@
|
|||||||
{:else}
|
{:else}
|
||||||
Remover Item
|
Remover Item
|
||||||
{/if}
|
{/if}
|
||||||
|
{:else if req.tipo === 'alteracao_detalhes'}
|
||||||
|
{#if data.itemId}
|
||||||
|
{@const item = items.find((i) => i._id === data.itemId)}
|
||||||
|
Alterar detalhes de:
|
||||||
|
{item ? getObjetoName(item.objetoId) : 'Item desconhecido'}
|
||||||
|
{:else}
|
||||||
|
Alteração de detalhes do item
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2 text-right">
|
<td class="px-4 py-2 text-right">
|
||||||
@@ -879,7 +1014,7 @@
|
|||||||
<div class="mb-6 overflow-hidden rounded-lg bg-white shadow-md">
|
<div class="mb-6 overflow-hidden rounded-lg bg-white shadow-md">
|
||||||
<div class="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
<div class="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
||||||
<h2 class="text-lg font-semibold">Itens do Pedido</h2>
|
<h2 class="text-lg font-semibold">Itens do Pedido</h2>
|
||||||
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes' || pedido.status === 'em_analise'}
|
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes' || pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite'}
|
||||||
<button
|
<button
|
||||||
onclick={() => (showAddItem = true)}
|
onclick={() => (showAddItem = true)}
|
||||||
class="flex items-center gap-1 text-sm font-medium text-blue-600 hover:text-blue-800"
|
class="flex items-center gap-1 text-sm font-medium text-blue-600 hover:text-blue-800"
|
||||||
@@ -1121,7 +1256,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<td class="px-6 py-4 whitespace-nowrap">{getObjetoName(item.objetoId)}</td>
|
<td class="px-6 py-4 whitespace-nowrap">{getObjetoName(item.objetoId)}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes' || pedido.status === 'em_analise'}
|
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes' || pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite'}
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
@@ -1135,7 +1270,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes' || pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite'}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="w-28 rounded border px-2 py-1 text-sm"
|
class="w-28 rounded border px-2 py-1 text-sm"
|
||||||
@@ -1154,7 +1289,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-600">
|
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-600">
|
||||||
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes' || pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite'}
|
||||||
<select
|
<select
|
||||||
class="rounded border px-2 py-1 text-xs"
|
class="rounded border px-2 py-1 text-xs"
|
||||||
value={ensureEditingItem(item).modalidade}
|
value={ensureEditingItem(item).modalidade}
|
||||||
@@ -1177,7 +1312,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-600">
|
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-600">
|
||||||
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes' || pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite'}
|
||||||
<select
|
<select
|
||||||
class="rounded border px-2 py-1 text-xs"
|
class="rounded border px-2 py-1 text-xs"
|
||||||
value={ensureEditingItem(item).acaoId}
|
value={ensureEditingItem(item).acaoId}
|
||||||
@@ -1196,7 +1331,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-600">
|
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-600">
|
||||||
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes'}
|
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes' || pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite'}
|
||||||
<select
|
<select
|
||||||
class="rounded border px-2 py-1 text-xs"
|
class="rounded border px-2 py-1 text-xs"
|
||||||
value={ensureEditingItem(item).ataId}
|
value={ensureEditingItem(item).ataId}
|
||||||
@@ -1233,7 +1368,7 @@
|
|||||||
>
|
>
|
||||||
<Eye size={18} />
|
<Eye size={18} />
|
||||||
</button>
|
</button>
|
||||||
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes' || pedido.status === 'em_analise'}
|
{#if pedido.status === 'em_rascunho' || pedido.status === 'precisa_ajustes' || pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite'}
|
||||||
<button
|
<button
|
||||||
onclick={() => handleRemoveItem(item._id)}
|
onclick={() => handleRemoveItem(item._id)}
|
||||||
class="text-red-600 hover:text-red-900"
|
class="text-red-600 hover:text-red-900"
|
||||||
@@ -1512,4 +1647,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
bind:open={confirmModal.open}
|
||||||
|
title={confirmModal.title}
|
||||||
|
message={confirmModal.message}
|
||||||
|
confirmText={confirmModal.confirmText}
|
||||||
|
isDestructive={confirmModal.isDestructive}
|
||||||
|
onConfirm={confirmModal.onConfirm}
|
||||||
|
onClose={() => (confirmModal.open = false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -619,8 +619,8 @@ export const addItem = mutation({
|
|||||||
const pedido = await ctx.db.get(args.pedidoId);
|
const pedido = await ctx.db.get(args.pedidoId);
|
||||||
if (!pedido) throw new Error('Pedido não encontrado.');
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
||||||
|
|
||||||
// --- CHECK ANALYSIS MODE ---
|
// --- CHECK ANALYSIS / ACCEPTANCE MODE ---
|
||||||
if (pedido.status === 'em_analise') {
|
if (pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite') {
|
||||||
await ctx.db.insert('solicitacoesItens', {
|
await ctx.db.insert('solicitacoesItens', {
|
||||||
pedidoId: args.pedidoId,
|
pedidoId: args.pedidoId,
|
||||||
tipo: 'adicao',
|
tipo: 'adicao',
|
||||||
@@ -729,8 +729,8 @@ export const updateItemQuantity = mutation({
|
|||||||
const pedido = await ctx.db.get(item.pedidoId);
|
const pedido = await ctx.db.get(item.pedidoId);
|
||||||
if (!pedido) throw new Error('Pedido não encontrado.');
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
||||||
|
|
||||||
// --- CHECK ANALYSIS MODE ---
|
// --- CHECK ANALYSIS / ACCEPTANCE MODE ---
|
||||||
if (pedido.status === 'em_analise') {
|
if (pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite') {
|
||||||
await ctx.db.insert('solicitacoesItens', {
|
await ctx.db.insert('solicitacoesItens', {
|
||||||
pedidoId: item.pedidoId,
|
pedidoId: item.pedidoId,
|
||||||
tipo: 'alteracao_quantidade',
|
tipo: 'alteracao_quantidade',
|
||||||
@@ -784,8 +784,8 @@ export const removeItem = mutation({
|
|||||||
const pedido = await ctx.db.get(item.pedidoId);
|
const pedido = await ctx.db.get(item.pedidoId);
|
||||||
if (!pedido) throw new Error('Pedido não encontrado.');
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
||||||
|
|
||||||
// --- CHECK ANALYSIS MODE ---
|
// --- CHECK ANALYSIS / ACCEPTANCE MODE ---
|
||||||
if (pedido.status === 'em_analise') {
|
if (pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite') {
|
||||||
if (!user.funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
if (!user.funcionarioId) throw new Error('Usuário sem funcionário vinculado.');
|
||||||
await ctx.db.insert('solicitacoesItens', {
|
await ctx.db.insert('solicitacoesItens', {
|
||||||
pedidoId: item.pedidoId,
|
pedidoId: item.pedidoId,
|
||||||
@@ -995,6 +995,9 @@ export const updateItem = mutation({
|
|||||||
const item = await ctx.db.get(args.itemId);
|
const item = await ctx.db.get(args.itemId);
|
||||||
if (!item) throw new Error('Item não encontrado.');
|
if (!item) throw new Error('Item não encontrado.');
|
||||||
|
|
||||||
|
const pedido = await ctx.db.get(item.pedidoId);
|
||||||
|
if (!pedido) throw new Error('Pedido não encontrado.');
|
||||||
|
|
||||||
// Apenas quem adicionou o item pode editá-lo
|
// Apenas quem adicionou o item pode editá-lo
|
||||||
const isOwner = item.adicionadoPor === user.funcionarioId;
|
const isOwner = item.adicionadoPor === user.funcionarioId;
|
||||||
if (!isOwner) {
|
if (!isOwner) {
|
||||||
@@ -1008,6 +1011,29 @@ export const updateItem = mutation({
|
|||||||
ataId: 'ataId' in item ? item.ataId : undefined
|
ataId: 'ataId' in item ? item.ataId : undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Em pedidos em análise ou aguardando aceite, geramos uma solicitação em vez de alterar diretamente
|
||||||
|
if (pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite') {
|
||||||
|
await ctx.db.insert('solicitacoesItens', {
|
||||||
|
pedidoId: item.pedidoId,
|
||||||
|
tipo: 'alteracao_detalhes',
|
||||||
|
dados: JSON.stringify({
|
||||||
|
itemId: args.itemId,
|
||||||
|
de: oldValues,
|
||||||
|
para: {
|
||||||
|
valorEstimado: args.valorEstimado,
|
||||||
|
modalidade: args.modalidade,
|
||||||
|
acaoId: args.acaoId,
|
||||||
|
ataId: args.ataId
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
status: 'pendente',
|
||||||
|
solicitadoPor: user.funcionarioId,
|
||||||
|
criadoEm: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await ctx.db.patch(args.itemId, {
|
await ctx.db.patch(args.itemId, {
|
||||||
valorEstimado: args.valorEstimado,
|
valorEstimado: args.valorEstimado,
|
||||||
modalidade: args.modalidade,
|
modalidade: args.modalidade,
|
||||||
@@ -1510,18 +1536,6 @@ export const notifyStatusChange = internalMutation({
|
|||||||
|
|
||||||
export const getItemRequests = query({
|
export const getItemRequests = query({
|
||||||
args: { pedidoId: v.id('pedidos') },
|
args: { pedidoId: v.id('pedidos') },
|
||||||
returns: v.array(
|
|
||||||
v.object({
|
|
||||||
_id: v.id('solicitacoesItens'),
|
|
||||||
pedidoId: v.id('pedidos'),
|
|
||||||
tipo: v.union(v.literal('adicao'), v.literal('alteracao_quantidade'), v.literal('exclusao')),
|
|
||||||
dados: v.string(),
|
|
||||||
status: v.union(v.literal('pendente'), v.literal('aprovado'), v.literal('rejeitado')),
|
|
||||||
solicitadoPor: v.id('funcionarios'),
|
|
||||||
solicitadoPorNome: v.string(),
|
|
||||||
criadoEm: v.number()
|
|
||||||
})
|
|
||||||
),
|
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const requests = await ctx.db
|
const requests = await ctx.db
|
||||||
.query('solicitacoesItens')
|
.query('solicitacoesItens')
|
||||||
@@ -1648,6 +1662,26 @@ export const approveItemRequest = mutation({
|
|||||||
if (item) {
|
if (item) {
|
||||||
await ctx.db.delete(itemId);
|
await ctx.db.delete(itemId);
|
||||||
}
|
}
|
||||||
|
} else if (request.tipo === 'alteracao_detalhes') {
|
||||||
|
const { itemId, para } = data as {
|
||||||
|
itemId: Id<'objetoItems'>;
|
||||||
|
para: {
|
||||||
|
valorEstimado: string;
|
||||||
|
modalidade: Doc<'objetoItems'>['modalidade'];
|
||||||
|
acaoId?: Id<'acoes'>;
|
||||||
|
ataId?: Id<'atas'>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const item = await ctx.db.get(itemId);
|
||||||
|
if (item) {
|
||||||
|
await ctx.db.patch(itemId, {
|
||||||
|
valorEstimado: para.valorEstimado,
|
||||||
|
modalidade: para.modalidade,
|
||||||
|
acaoId: para.acaoId,
|
||||||
|
ataId: para.ataId
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update request status
|
// Update request status
|
||||||
|
|||||||
@@ -47,7 +47,12 @@ export const pedidosTables = {
|
|||||||
|
|
||||||
solicitacoesItens: defineTable({
|
solicitacoesItens: defineTable({
|
||||||
pedidoId: v.id('pedidos'),
|
pedidoId: v.id('pedidos'),
|
||||||
tipo: v.union(v.literal('adicao'), v.literal('alteracao_quantidade'), v.literal('exclusao')),
|
tipo: v.union(
|
||||||
|
v.literal('adicao'),
|
||||||
|
v.literal('alteracao_quantidade'),
|
||||||
|
v.literal('exclusao'),
|
||||||
|
v.literal('alteracao_detalhes')
|
||||||
|
),
|
||||||
dados: v.string(), // JSON string with details
|
dados: v.string(), // JSON string with details
|
||||||
status: v.union(v.literal('pendente'), v.literal('aprovado'), v.literal('rejeitado')),
|
status: v.union(v.literal('pendente'), v.literal('aprovado'), v.literal('rejeitado')),
|
||||||
solicitadoPor: v.id('funcionarios'),
|
solicitadoPor: v.id('funcionarios'),
|
||||||
|
|||||||
Reference in New Issue
Block a user