diff --git a/apps/web/src/lib/components/ConfirmationModal.svelte b/apps/web/src/lib/components/ConfirmationModal.svelte new file mode 100644 index 0000000..3d968ce --- /dev/null +++ b/apps/web/src/lib/components/ConfirmationModal.svelte @@ -0,0 +1,133 @@ + + +{#if open} + +{/if} + + diff --git a/apps/web/src/routes/(dashboard)/pedidos/[id]/+page.svelte b/apps/web/src/routes/(dashboard)/pedidos/[id]/+page.svelte index b2b665b..9998754 100644 --- a/apps/web/src/routes/(dashboard)/pedidos/[id]/+page.svelte +++ b/apps/web/src/routes/(dashboard)/pedidos/[id]/+page.svelte @@ -3,6 +3,8 @@ import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel'; import { useConvexClient, useQuery } from 'convex-svelte'; import { SvelteSet } from 'svelte/reactivity'; + import { toast } from 'svelte-sonner'; + import ConfirmationModal from '$lib/components/ConfirmationModal.svelte'; import { AlertTriangle, CheckCircle, @@ -156,6 +158,43 @@ let showDetailsModal = $state(false); let selectedObjeto = $state | 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, + 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) { const obj = objetos.find((o) => o._id === objetoId); if (obj) { @@ -232,11 +271,13 @@ ataId: '' }; showAddItem = false; - if (pedido.status === 'em_analise') { - alert('Solicitação de adição enviada para análise.'); + if (pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite') { + toast.success('Solicitação de adição enviada para análise.'); + } else { + toast.success('Item adicionado com sucesso!'); } } catch (e) { - alert('Erro ao adicionar item: ' + (e as Error).message); + toast.error('Erro ao adicionar item: ' + (e as Error).message); } finally { addingItem = false; } @@ -246,7 +287,7 @@ if (!pedido) return; if (novaQuantidade < 1) { - alert('Quantidade deve ser pelo menos 1.'); + toast.error('Quantidade deve ser pelo menos 1.'); return; } try { @@ -254,61 +295,79 @@ itemId, novaQuantidade }); - if (pedido.status === 'em_analise') { - alert('Solicitação de alteração de quantidade enviada para análise.'); + if (pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite') { + toast.success('Solicitação de alteração de quantidade enviada para análise.'); + } else { + toast.success('Quantidade atualizada com sucesso!'); } } 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 (!confirm('Remover este item?')) return; - try { - await client.mutation(api.pedidos.removeItem, { itemId }); - if (pedido.status === 'em_analise') { - alert('Solicitação de remoção enviada para análise.'); - } - } catch (e) { - alert('Erro ao remover item: ' + (e as Error).message); - } + openConfirm( + 'Remover Item', + 'Tem certeza que deseja remover este item do pedido?', + async () => { + await client.mutation(api.pedidos.removeItem, { itemId }); + if (pedido.status === 'em_analise' || pedido.status === 'aguardando_aceite') { + toast.success('Solicitação de remoção enviada para análise.'); + } else { + toast.success('Item removido com sucesso!'); + } + }, + { isDestructive: true, confirmText: 'Remover' } + ); } - async function handleEnviarParaAceite() { - if (!confirm('Enviar para aceite?')) return; - try { - await client.mutation(api.pedidos.enviarParaAceite, { pedidoId }); - } catch (e) { - alert('Erro: ' + (e as Error).message); - } + function handleEnviarParaAceite() { + openConfirm( + 'Enviar para Aceite', + 'Tem certeza que deseja enviar este pedido para aceite? Ele ficará aguardando análise do setor responsável.', + async () => { + await client.mutation(api.pedidos.enviarParaAceite, { pedidoId }); + toast.success('Pedido enviado para aceite com sucesso!'); + }, + { confirmText: 'Enviar' } + ); } - async function handleIniciarAnalise() { - if (!confirm('Iniciar análise?')) return; - try { - await client.mutation(api.pedidos.iniciarAnalise, { pedidoId }); - } catch (e) { - alert('Erro: ' + (e as Error).message); - } + function handleIniciarAnalise() { + openConfirm( + 'Iniciar Análise', + 'Tem certeza que deseja iniciar a análise deste pedido? O status será alterado para "Em Análise".', + async () => { + await client.mutation(api.pedidos.iniciarAnalise, { pedidoId }); + toast.success('Análise iniciada com sucesso!'); + }, + { confirmText: 'Iniciar Análise' } + ); } - async function handleConcluir() { - if (!confirm('Concluir pedido?')) return; - try { - await client.mutation(api.pedidos.concluirPedido, { pedidoId }); - } catch (e) { - alert('Erro: ' + (e as Error).message); - } + function handleConcluir() { + openConfirm( + 'Concluir Pedido', + 'Tem certeza que deseja concluir este pedido? Esta ação não pode ser desfeita.', + async () => { + await client.mutation(api.pedidos.concluirPedido, { pedidoId }); + toast.success('Pedido concluído com sucesso!'); + }, + { confirmText: 'Concluir Pedido' } + ); } - async function handleCancelar() { - if (!confirm('Cancelar pedido?')) return; - try { - await client.mutation(api.pedidos.cancelarPedido, { pedidoId }); - } catch (e) { - alert('Erro: ' + (e as Error).message); - } + function handleCancelar() { + openConfirm( + 'Cancelar Pedido', + 'Tem certeza que deseja cancelar este pedido? Esta ação não pode ser desfeita e o pedido ficará inutilizável.', + async () => { + await client.mutation(api.pedidos.cancelarPedido, { pedidoId }); + toast.success('Pedido cancelado com sucesso!'); + }, + { isDestructive: true, confirmText: 'Cancelar Pedido' } + ); } function getObjetoName(id: string) { @@ -411,8 +470,14 @@ acaoId: current.acaoId ? (current.acaoId as Id<'acoes'>) : 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) { - 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() { 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; } updatingSei = true; try { await client.mutation(api.pedidos.updateSeiNumber, { pedidoId, - numeroSei: seiValue.trim() + numeroSei: seiValue }); editingSei = false; + toast.success('Número SEI atualizado com sucesso!'); } 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 { updatingSei = false; } @@ -553,26 +619,22 @@ } } - async function handleRemoveSelectedItems() { + function handleRemoveSelectedItems() { if (!hasSelection) return; - if ( - !confirm( - selectedCount === 1 - ? 'Remover o item selecionado deste pedido?' - : `Remover os ${selectedCount} itens selecionados deste pedido?` - ) - ) - return; - - try { - const itemIds = Array.from(selectedItemIds) as Id<'objetoItems'>[]; - await client.mutation(api.pedidos.removeItemsBatch, { - itemIds - }); - clearSelection(); - } catch (e) { - alert('Erro ao remover itens selecionados: ' + (e as Error).message); - } + openConfirm( + 'Remover Itens Selecionados', + selectedCount === 1 + ? 'Tem certeza que deseja remover o item selecionado deste pedido?' + : `Tem certeza que deseja remover os ${selectedCount} itens selecionados deste pedido?`, + async () => { + await client.mutation(api.pedidos.removeItemsBatch, { + itemIds: Array.from(selectedItemIds) as Id<'objetoItems'>[] + }); + clearSelection(); + toast.success('Itens removidos com sucesso!'); + }, + { isDestructive: true, confirmText: 'Remover Selecionados' } + ); } let showSplitResultModal = $state(false); @@ -603,8 +665,9 @@ showSplitConfirmationModal = false; showSplitResultModal = true; clearSelection(); + toast.success('Pedido dividido com sucesso!'); } 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() { 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; } try { @@ -628,36 +691,98 @@ descricao: adjustmentDescription }); showRequestAdjustmentsModal = false; + toast.success('Solicitação de ajustes enviada com sucesso!'); } catch (e) { - alert('Erro: ' + (e as Error).message); + toast.error('Erro ao solicitar ajustes: ' + (e as Error).message); } } - async function handleConcluirAjustes() { - if (!confirm('Concluir ajustes e retornar para análise?')) return; - try { - await client.mutation(api.pedidos.concluirAjustes, { pedidoId }); - } catch (e) { - alert('Erro: ' + (e as Error).message); - } + function handleConcluirAjustes() { + openConfirm( + 'Concluir Ajustes', + 'Tem certeza que deseja concluir os ajustes e retornar o pedido para análise?', + async () => { + await client.mutation(api.pedidos.concluirAjustes, { pedidoId }); + toast.success('Ajustes concluídos com sucesso!'); + }, + { confirmText: 'Concluir Ajustes' } + ); } - async function handleApproveRequest(requestId: Id<'solicitacoesItens'>) { - if (!confirm('Aprovar esta solicitação?')) return; - try { - await client.mutation(api.pedidos.approveItemRequest, { requestId }); - } catch (e) { - alert('Erro ao aprovar: ' + (e as Error).message); - } + function handleApproveRequest(requestId: Id<'solicitacoesItens'>) { + openConfirm( + 'Aprovar Solicitação', + 'Tem certeza que deseja aprovar esta solicitação?', + async () => { + await client.mutation(api.pedidos.approveItemRequest, { requestId }); + toast.success('Solicitação aprovada com sucesso!'); + }, + { confirmText: 'Aprovar' } + ); } - async function handleRejectRequest(requestId: Id<'solicitacoesItens'>) { - if (!confirm('Rejeitar esta solicitação?')) return; - try { - await client.mutation(api.pedidos.rejectItemRequest, { requestId }); - } catch (e) { - alert('Erro ao rejeitar: ' + (e as Error).message); + function handleRejectRequest(requestId: Id<'solicitacoesItens'>) { + openConfirm( + 'Rejeitar Solicitação', + 'Tem certeza que deseja rejeitar esta solicitação?', + async () => { + 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) { @@ -824,6 +949,8 @@ Alteração Qtd {:else if req.tipo === 'exclusao'} Exclusão + {:else if req.tipo === 'alteracao_detalhes'} + {describeChangedDetails(data)} {/if} {req.solicitadoPorNome} @@ -844,6 +971,14 @@ {:else} Remover Item {/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} @@ -879,7 +1014,7 @@

Itens do Pedido

- {#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'}