Ajustes final etapa1 #69
@@ -280,9 +280,8 @@
|
||||
|
||||
// Sincronizar preview com value sempre que value mudar
|
||||
$effect(() => {
|
||||
if (value !== preview) {
|
||||
preview = value;
|
||||
}
|
||||
// Sempre sincronizar quando value mudar
|
||||
preview = value;
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { Package, Plus, Search, Edit, Eye, AlertTriangle } from 'lucide-svelte';
|
||||
import { Package, Plus, Search, Edit, Eye, AlertTriangle, Trash2, Info, X } from 'lucide-svelte';
|
||||
import BarcodeScanner from '$lib/components/almoxarifado/BarcodeScanner.svelte';
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
let materiais = $state<Array<Doc<'materiais'>>>([]);
|
||||
// Usar useQuery para atualização automática
|
||||
const materiaisQuery = useQuery(api.almoxarifado.listarMateriais, {});
|
||||
|
||||
let materiais = $derived(materiaisQuery?.data ?? []);
|
||||
let filtered = $state<Array<Doc<'materiais'>>>([]);
|
||||
let filtroBusca = $state('');
|
||||
let filtroCategoria = $state('');
|
||||
@@ -19,6 +22,11 @@
|
||||
let materialEncontrado = $state<Doc<'materiais'> | null>(null);
|
||||
let buscandoPorCodigoBarras = $state(false);
|
||||
let buscaTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let materialParaExcluir = $state<Doc<'materiais'> | null>(null);
|
||||
let excluindo = $state(false);
|
||||
let notice = $state<{ kind: 'success' | 'error'; text: string } | null>(null);
|
||||
let erroExclusao = $state<{ titulo: string; mensagem: string; tipo: 'movimentacoes' | 'requisicoes' | 'outro' } | null>(null);
|
||||
let desativando = $state(false);
|
||||
|
||||
const categorias = $derived(
|
||||
Array.from(new Set(materiais.map((m) => m.categoria).filter(Boolean))).sort()
|
||||
@@ -67,17 +75,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const data = await client.query(api.almoxarifado.listarMateriais, {});
|
||||
materiais = data ?? [];
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
load();
|
||||
});
|
||||
|
||||
// Aplicar filtros sempre que materiais ou filtros mudarem
|
||||
$effect(() => {
|
||||
// Acessar materiais para criar dependência reativa
|
||||
const _ = materiais;
|
||||
// Aplicar filtros
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
@@ -126,6 +128,114 @@
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
function abrirModalExclusao(material: Doc<'materiais'>) {
|
||||
materialParaExcluir = material;
|
||||
(document.getElementById('modal-excluir-material') as HTMLDialogElement)?.showModal();
|
||||
}
|
||||
|
||||
function fecharModalExclusao() {
|
||||
materialParaExcluir = null;
|
||||
(document.getElementById('modal-excluir-material') as HTMLDialogElement)?.close();
|
||||
}
|
||||
|
||||
async function confirmarExclusao() {
|
||||
if (!materialParaExcluir) return;
|
||||
|
||||
const materialId = materialParaExcluir._id;
|
||||
const materialNome = materialParaExcluir.nome;
|
||||
|
||||
try {
|
||||
excluindo = true;
|
||||
|
||||
// Remover item da lista localmente imediatamente (otimistic update)
|
||||
filtered = filtered.filter((m) => m._id !== materialId);
|
||||
|
||||
await client.mutation(api.almoxarifado.deletarMaterial, {
|
||||
id: materialId
|
||||
});
|
||||
|
||||
fecharModalExclusao();
|
||||
notice = {
|
||||
kind: 'success',
|
||||
text: `Material "${materialNome}" excluído com sucesso!`
|
||||
};
|
||||
|
||||
// Limpar notificação após 5 segundos
|
||||
setTimeout(() => {
|
||||
notice = null;
|
||||
}, 5000);
|
||||
} catch (error: unknown) {
|
||||
// Se houver erro, recarregar a lista para garantir consistência
|
||||
applyFilters();
|
||||
|
||||
const message = error instanceof Error ? error.message : 'Erro ao excluir material';
|
||||
|
||||
// Determinar tipo de erro e criar mensagem mais clara
|
||||
let tipo: 'movimentacoes' | 'requisicoes' | 'outro' = 'outro';
|
||||
let titulo = 'Não foi possível excluir o material';
|
||||
let mensagem = message;
|
||||
|
||||
const messageLower = message.toLowerCase();
|
||||
|
||||
if (messageLower.includes('movimenta') || messageLower.includes('movimentação')) {
|
||||
tipo = 'movimentacoes';
|
||||
titulo = 'Material possui movimentações de estoque';
|
||||
mensagem = 'Este material possui movimentações de estoque registradas e não pode ser excluído para manter o histórico. Você pode desativá-lo ao invés de excluir.';
|
||||
} else if (messageLower.includes('requisi') || messageLower.includes('requisição')) {
|
||||
tipo = 'requisicoes';
|
||||
titulo = 'Material possui requisições registradas';
|
||||
mensagem = 'Este material possui requisições registradas e não pode ser excluído para manter o histórico. Você pode desativá-lo ao invés de excluir.';
|
||||
}
|
||||
|
||||
// Exibir modal de erro
|
||||
erroExclusao = { titulo, mensagem, tipo };
|
||||
fecharModalExclusao();
|
||||
(document.getElementById('modal-erro-exclusao') as HTMLDialogElement)?.showModal();
|
||||
} finally {
|
||||
excluindo = false;
|
||||
}
|
||||
}
|
||||
|
||||
function fecharModalErro() {
|
||||
erroExclusao = null;
|
||||
(document.getElementById('modal-erro-exclusao') as HTMLDialogElement)?.close();
|
||||
}
|
||||
|
||||
async function desativarMaterial() {
|
||||
if (!materialParaExcluir || !erroExclusao) return;
|
||||
|
||||
const materialId = materialParaExcluir._id;
|
||||
const materialNome = materialParaExcluir.nome;
|
||||
|
||||
try {
|
||||
desativando = true;
|
||||
|
||||
await client.mutation(api.almoxarifado.editarMaterial, {
|
||||
id: materialId,
|
||||
ativo: false
|
||||
});
|
||||
|
||||
fecharModalErro();
|
||||
notice = {
|
||||
kind: 'success',
|
||||
text: `Material "${materialNome}" desativado com sucesso!`
|
||||
};
|
||||
|
||||
// Limpar notificação após 5 segundos
|
||||
setTimeout(() => {
|
||||
notice = null;
|
||||
}, 5000);
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : 'Erro ao desativar material';
|
||||
notice = { kind: 'error', text: message };
|
||||
setTimeout(() => {
|
||||
notice = null;
|
||||
}, 5000);
|
||||
} finally {
|
||||
desativando = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="container mx-auto px-4 py-6">
|
||||
@@ -158,6 +268,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notificações -->
|
||||
{#if notice}
|
||||
<div class="alert alert-{notice.kind} mb-6 shadow-lg">
|
||||
<span>{notice.text}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Filtros -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6 shadow-xl">
|
||||
<div class="card-body">
|
||||
@@ -314,6 +431,13 @@
|
||||
>
|
||||
<Edit class="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-ghost hover:btn-error"
|
||||
title="Excluir"
|
||||
onclick={() => abrirModalExclusao(material)}
|
||||
>
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -332,6 +456,148 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal de Confirmação de Exclusão -->
|
||||
<dialog id="modal-excluir-material" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold mb-4">Confirmar Exclusão</h3>
|
||||
{#if materialParaExcluir}
|
||||
<div class="space-y-4">
|
||||
<div class="alert alert-warning">
|
||||
<AlertTriangle class="h-5 w-5" />
|
||||
<div>
|
||||
<p class="font-semibold">Atenção!</p>
|
||||
<p class="text-sm">
|
||||
Esta ação não pode ser desfeita. O material será permanentemente excluído.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-base-200 rounded-lg p-4">
|
||||
<p class="text-sm text-base-content/70 mb-2">Material a ser excluído:</p>
|
||||
<p class="font-semibold text-base-content">{materialParaExcluir.nome}</p>
|
||||
<p class="text-sm text-base-content/60 mt-1">
|
||||
Código: <span class="font-mono">{materialParaExcluir.codigo}</span>
|
||||
</p>
|
||||
{#if materialParaExcluir.estoqueAtual > 0}
|
||||
<div class="mt-2 alert alert-info py-2">
|
||||
<p class="text-xs">
|
||||
⚠️ Este material possui <strong>{materialParaExcluir.estoqueAtual}</strong> unidades em estoque.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="modal-action">
|
||||
<button
|
||||
class="btn btn-ghost"
|
||||
onclick={fecharModalExclusao}
|
||||
disabled={excluindo}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-error"
|
||||
onclick={confirmarExclusao}
|
||||
disabled={excluindo}
|
||||
>
|
||||
{#if excluindo}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Excluindo...
|
||||
{:else}
|
||||
<Trash2 class="h-4 w-4" />
|
||||
Excluir
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button onclick={fecharModalExclusao}>fechar</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- Modal de Erro na Exclusão -->
|
||||
{#if erroExclusao}
|
||||
<dialog id="modal-erro-exclusao" class="modal modal-open">
|
||||
<div class="modal-box max-w-2xl">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="rounded-2xl bg-error/20 p-3">
|
||||
<AlertTriangle class="h-8 w-8 text-error" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-2xl font-bold text-base-content">{erroExclusao.titulo}</h3>
|
||||
<p class="text-base-content/70 mt-1">Não foi possível excluir o material</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 mb-6">
|
||||
<div class="alert alert-error border-error/30 bg-error/10">
|
||||
<AlertTriangle class="h-5 w-5 shrink-0 text-error" />
|
||||
<div class="flex-1">
|
||||
<p class="font-semibold text-base-content">Motivo do bloqueio</p>
|
||||
<p class="text-sm text-base-content/90 mt-1">{erroExclusao.mensagem}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if materialParaExcluir}
|
||||
<div class="bg-base-200 rounded-lg p-4 border border-base-300">
|
||||
<p class="text-sm text-base-content/70 mb-2 font-semibold">Material:</p>
|
||||
<p class="font-bold text-lg text-base-content">{materialParaExcluir.nome}</p>
|
||||
<p class="text-sm text-base-content/60 mt-1">
|
||||
Código: <span class="font-mono font-semibold">{materialParaExcluir.codigo}</span>
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="bg-info/10 border border-info/30 rounded-lg p-4">
|
||||
<div class="flex gap-3">
|
||||
<Info class="h-5 w-5 text-info shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p class="font-semibold text-base-content mb-1">Solução recomendada</p>
|
||||
<p class="text-sm text-base-content/80">
|
||||
{#if erroExclusao.tipo === 'movimentacoes'}
|
||||
O material possui histórico de movimentações de estoque. Para manter a integridade dos dados históricos, recomendamos <strong>desativar</strong> o material ao invés de excluí-lo. Um material desativado não aparecerá nas listagens ativas, mas seu histórico será preservado.
|
||||
{:else if erroExclusao.tipo === 'requisicoes'}
|
||||
O material possui requisições registradas. Para manter a integridade dos dados históricos, recomendamos <strong>desativar</strong> o material ao invés de excluí-lo. Um material desativado não aparecerá nas listagens ativas, mas seu histórico será preservado.
|
||||
{:else}
|
||||
Recomendamos verificar as dependências do material antes de tentar excluí-lo novamente.
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-action gap-3">
|
||||
<button
|
||||
class="btn btn-ghost"
|
||||
onclick={fecharModalErro}
|
||||
disabled={desativando}
|
||||
>
|
||||
Entendi
|
||||
</button>
|
||||
{#if erroExclusao.tipo === 'movimentacoes' || erroExclusao.tipo === 'requisicoes'}
|
||||
<button
|
||||
class="btn btn-warning gap-2"
|
||||
onclick={desativarMaterial}
|
||||
disabled={desativando}
|
||||
>
|
||||
{#if desativando}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Desativando...
|
||||
{:else}
|
||||
<X class="h-4 w-4" />
|
||||
Desativar Material
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button onclick={fecharModalErro}>fechar</button>
|
||||
</form>
|
||||
</dialog>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
|
||||
|
||||
@@ -103,9 +103,30 @@
|
||||
</div>
|
||||
|
||||
<!-- Informações Principais -->
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="grid grid-cols-1 gap-6 {material.imagemBase64 ? 'md:grid-cols-3' : 'md:grid-cols-2'}">
|
||||
<!-- Card: Imagem do Produto -->
|
||||
{#if material.imagemBase64}
|
||||
<div class="card bg-base-100 border border-base-300 shadow-xl md:col-span-1">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-6 text-xl">
|
||||
<div class="rounded-lg bg-primary/20 p-2">
|
||||
<Package class="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
Imagem do Produto
|
||||
</h2>
|
||||
<div class="flex justify-center">
|
||||
<img
|
||||
src={material.imagemBase64}
|
||||
alt={material.nome}
|
||||
class="max-h-96 w-full rounded-lg border border-base-300 object-contain bg-base-200 p-4 shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Card: Informações Básicas -->
|
||||
<div class="card bg-base-100 border border-base-300 shadow-xl">
|
||||
<div class="card bg-base-100 border border-base-300 shadow-xl {material.imagemBase64 ? 'md:col-span-1' : ''}">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-6 text-xl">
|
||||
<div class="rounded-lg bg-primary/20 p-2">
|
||||
@@ -118,6 +139,12 @@
|
||||
<label class="text-sm font-semibold text-base-content/60">Código</label>
|
||||
<p class="font-mono text-lg font-bold">{material.codigo}</p>
|
||||
</div>
|
||||
{#if material.codigoBarras}
|
||||
<div>
|
||||
<label class="text-sm font-semibold text-base-content/60">Código de Barras</label>
|
||||
<p class="font-mono text-base">{material.codigoBarras}</p>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<label class="text-sm font-semibold text-base-content/60">Nome</label>
|
||||
<p class="text-lg">{material.nome}</p>
|
||||
@@ -159,7 +186,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Card: Estoque -->
|
||||
<div class="card bg-base-100 border border-base-300 shadow-xl">
|
||||
<div class="card bg-base-100 border border-base-300 shadow-xl {material.imagemBase64 ? 'md:col-span-1' : ''}">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-6 text-xl">
|
||||
<div class="rounded-lg bg-info/20 p-2">
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Package, Save, ArrowLeft } from 'lucide-svelte';
|
||||
import ImageUpload from '$lib/components/almoxarifado/ImageUpload.svelte';
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useConvexClient } from 'convex-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { tick } from 'svelte';
|
||||
import { Package, Save, ArrowLeft, Check, X, ExternalLink, Loader2, AlertCircle, Info } from 'lucide-svelte';
|
||||
import BarcodeScanner from '$lib/components/almoxarifado/BarcodeScanner.svelte';
|
||||
import ImageUpload from '$lib/components/almoxarifado/ImageUpload.svelte';
|
||||
@@ -132,28 +133,47 @@
|
||||
// Carregar imagem se disponível (aguardar conversão para garantir que seja salva e exibida)
|
||||
if (dadosExternos.imagemUrl) {
|
||||
let imagemParaSalvar: string | null = null;
|
||||
const imagemUrlAtual = dadosExternos.imagemUrl;
|
||||
|
||||
if (dadosExternos.imagemUrl.startsWith('data:')) {
|
||||
// Já está em base64
|
||||
imagemParaSalvar = dadosExternos.imagemUrl;
|
||||
// Verificar se já está em base64 (carregada em background)
|
||||
if (imagemUrlAtual.startsWith('data:')) {
|
||||
// Já está em base64, usar diretamente
|
||||
imagemParaSalvar = imagemUrlAtual;
|
||||
console.log('Imagem já carregada em background, usando diretamente');
|
||||
} else {
|
||||
// Aguardar carregar a imagem da URL antes de continuar
|
||||
try {
|
||||
const imagemBase64Carregada = await carregarImagemDeUrl(dadosExternos.imagemUrl);
|
||||
if (imagemBase64Carregada) {
|
||||
imagemParaSalvar = imagemBase64Carregada;
|
||||
console.log('Imagem carregada e convertida para base64 com sucesso');
|
||||
} else {
|
||||
console.warn('Não foi possível carregar a imagem da URL:', dadosExternos.imagemUrl);
|
||||
// Ainda é uma URL, aguardar um pouco para ver se o carregamento em background terminou
|
||||
// Se após 500ms ainda for URL, carregar agora
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Verificar novamente se foi carregada em background
|
||||
if (dadosExternos.imagemUrl && dadosExternos.imagemUrl.startsWith('data:')) {
|
||||
imagemParaSalvar = dadosExternos.imagemUrl;
|
||||
console.log('Imagem foi carregada em background durante a espera');
|
||||
} else {
|
||||
// Ainda é URL, carregar agora
|
||||
console.log('Carregando imagem da URL:', imagemUrlAtual);
|
||||
try {
|
||||
const imagemBase64Carregada = await carregarImagemDeUrl(imagemUrlAtual);
|
||||
if (imagemBase64Carregada) {
|
||||
imagemParaSalvar = imagemBase64Carregada;
|
||||
console.log('Imagem carregada e convertida para base64 com sucesso');
|
||||
} else {
|
||||
console.warn('Não foi possível carregar a imagem da URL:', imagemUrlAtual);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Erro ao carregar imagem:', err);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Erro ao carregar imagem:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Atribuir a imagem após o carregamento completo
|
||||
if (imagemParaSalvar) {
|
||||
imagemBase64 = imagemParaSalvar;
|
||||
// Aguardar o tick para garantir que o componente ImageUpload detecte a mudança
|
||||
await tick();
|
||||
console.log('Imagem atribuída ao campo imagemBase64, tamanho:', imagemParaSalvar.length, 'caracteres');
|
||||
} else {
|
||||
console.warn('Nenhuma imagem disponível para salvar');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,14 +257,19 @@
|
||||
|
||||
// Tentar carregar imagem se disponível (em background, não bloquear)
|
||||
if (infoExterna.imagemUrl && !infoExterna.imagemUrl.startsWith('data:')) {
|
||||
// Carregar imagem em background
|
||||
carregarImagemDeUrl(infoExterna.imagemUrl)
|
||||
.then((imagemBase64Carregada) => {
|
||||
if (imagemBase64Carregada) {
|
||||
// Atualizar dadosExternos com a imagem em base64
|
||||
dadosExternos = { ...dadosExternos, imagemUrl: imagemBase64Carregada };
|
||||
console.log('Imagem carregada em background e atualizada em dadosExternos');
|
||||
} else {
|
||||
console.warn('Falha ao carregar imagem em background');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Erro ao carregar imagem:', err);
|
||||
console.error('Erro ao carregar imagem em background:', err);
|
||||
// Manter URL original se falhar
|
||||
});
|
||||
}
|
||||
@@ -373,6 +398,35 @@
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handler para tecla Escape
|
||||
function handleEscapeKey(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
if (modalDadosExternos && !carregandoImagemExterna) {
|
||||
descartarDadosExternos();
|
||||
} else if (modalProdutoNaoEncontrado) {
|
||||
fecharModalProdutoNaoEncontrado();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gerenciar scroll do body quando modais estão abertos
|
||||
$effect(() => {
|
||||
const temModalAberto = modalDadosExternos || modalBuscando || modalProdutoNaoEncontrado;
|
||||
|
||||
if (temModalAberto) {
|
||||
document.body.classList.add('modal-open');
|
||||
document.addEventListener('keydown', handleEscapeKey);
|
||||
} else {
|
||||
document.body.classList.remove('modal-open');
|
||||
document.removeEventListener('keydown', handleEscapeKey);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove('modal-open');
|
||||
document.removeEventListener('keydown', handleEscapeKey);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="container mx-auto px-4 py-6">
|
||||
@@ -646,60 +700,72 @@
|
||||
<!-- Modal de Dados Externos Encontrados -->
|
||||
{#if modalDadosExternos && dadosExternos}
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
||||
class="pointer-events-none fixed inset-0 z-50"
|
||||
style="animation: fadeIn 0.2s ease-out;"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-dados-externos-title"
|
||||
aria-describedby="modal-dados-externos-description"
|
||||
>
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="bg-base-100 mx-4 w-full max-w-2xl transform rounded-2xl shadow-2xl transition-all"
|
||||
class="pointer-events-auto absolute inset-0 bg-black/50 backdrop-blur-md transition-opacity duration-300"
|
||||
onclick={descartarDadosExternos}
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
|
||||
<!-- Modal Box -->
|
||||
<div
|
||||
class="pointer-events-auto absolute left-1/2 top-1/2 z-10 flex max-h-[92vh] w-full max-w-3xl transform -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-3xl bg-base-100 shadow-2xl transition-all duration-300"
|
||||
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="border-base-300 from-primary/10 to-secondary/10 border-b bg-linear-to-r p-6">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="rounded-2xl bg-primary/20 p-3">
|
||||
<ExternalLink class="h-6 w-6 text-primary" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 id="modal-dados-externos-title" class="text-xl font-bold">
|
||||
Produto encontrado externamente
|
||||
</h3>
|
||||
<p class="text-base-content/70 text-sm mt-1">
|
||||
{#if dadosExternos?.fonte}
|
||||
Dados encontrados em: <span class="font-semibold text-primary">{dadosExternos.fonte}</span>
|
||||
{:else}
|
||||
Dados encontrados na base externa. Deseja usar essas informações?
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="border-base-300 from-primary/10 via-primary/5 to-transparent flex flex-shrink-0 items-center justify-between border-b bg-gradient-to-r px-6 py-5"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="rounded-2xl bg-primary/20 p-3 shadow-lg">
|
||||
<ExternalLink class="h-6 w-6 text-primary" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 id="modal-dados-externos-title" class="text-base-content text-2xl font-bold">
|
||||
Produto encontrado externamente
|
||||
</h3>
|
||||
<p id="modal-dados-externos-description" class="text-base-content/70 mt-1 text-sm">
|
||||
{#if dadosExternos?.fonte}
|
||||
Dados encontrados em: <span class="font-semibold text-primary">{dadosExternos.fonte}</span>
|
||||
{:else}
|
||||
Dados encontrados na base externa. Deseja usar essas informações?
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle btn-ghost"
|
||||
onclick={descartarDadosExternos}
|
||||
aria-label="Fechar"
|
||||
>
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle btn-ghost hover:bg-base-300 transition-colors"
|
||||
onclick={descartarDadosExternos}
|
||||
aria-label="Fechar modal"
|
||||
>
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="max-h-[60vh] overflow-y-auto p-6">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="modal-scroll flex-1 overflow-y-auto px-6 py-6">
|
||||
<div class="space-y-6">
|
||||
<!-- Imagem do Produto -->
|
||||
{#if dadosExternos.imagemUrl}
|
||||
<div class="md:col-span-2">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Imagem do Produto</span>
|
||||
<div class="form-control">
|
||||
<label class="label pb-3">
|
||||
<span class="label-text text-base font-semibold">Imagem do Produto</span>
|
||||
</label>
|
||||
<div class="flex justify-center">
|
||||
<div class="flex justify-center rounded-xl border-2 border-base-300 bg-gradient-to-br from-base-200 to-base-300 p-6 shadow-inner">
|
||||
<img
|
||||
src={dadosExternos.imagemUrl}
|
||||
alt="Imagem do produto"
|
||||
class="max-h-64 rounded-lg border border-base-300"
|
||||
alt="Imagem do produto encontrado"
|
||||
class="max-h-72 max-w-full rounded-lg object-contain shadow-lg"
|
||||
loading="lazy"
|
||||
onerror={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
@@ -708,84 +774,97 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Nome -->
|
||||
{#if dadosExternos.nome}
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Nome</span>
|
||||
</label>
|
||||
<div class="input input-bordered bg-base-200">{dadosExternos.nome}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Categoria -->
|
||||
{#if dadosExternos.categoria}
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Categoria</span>
|
||||
</label>
|
||||
<div class="input input-bordered bg-base-200">{dadosExternos.categoria}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Descrição -->
|
||||
{#if dadosExternos.descricao}
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Descrição</span>
|
||||
</label>
|
||||
<div class="textarea textarea-bordered bg-base-200 min-h-[80px]">
|
||||
{dadosExternos.descricao}
|
||||
<!-- Grid de Informações -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<!-- Nome -->
|
||||
{#if dadosExternos.nome}
|
||||
<div class="form-control">
|
||||
<label class="label pb-2">
|
||||
<span class="label-text font-semibold">Nome</span>
|
||||
</label>
|
||||
<div class="input input-bordered bg-base-200/80 text-base-content/90 border-base-300">
|
||||
{dadosExternos.nome}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Marca -->
|
||||
{#if dadosExternos.marca}
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Marca</span>
|
||||
</label>
|
||||
<div class="input input-bordered bg-base-200">{dadosExternos.marca}</div>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Categoria -->
|
||||
{#if dadosExternos.categoria}
|
||||
<div class="form-control">
|
||||
<label class="label pb-2">
|
||||
<span class="label-text font-semibold">Categoria</span>
|
||||
</label>
|
||||
<div class="input input-bordered bg-base-200/80 text-base-content/90 border-base-300">
|
||||
{dadosExternos.categoria}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Quantidade -->
|
||||
{#if dadosExternos.quantidade}
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Quantidade</span>
|
||||
</label>
|
||||
<div class="input input-bordered bg-base-200">{dadosExternos.quantidade}</div>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Marca -->
|
||||
{#if dadosExternos.marca}
|
||||
<div class="form-control">
|
||||
<label class="label pb-2">
|
||||
<span class="label-text font-semibold">Marca</span>
|
||||
</label>
|
||||
<div class="input input-bordered bg-base-200/80 text-base-content/90 border-base-300">
|
||||
{dadosExternos.marca}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Embalagem -->
|
||||
{#if dadosExternos.embalagem}
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Embalagem</span>
|
||||
</label>
|
||||
<div class="input input-bordered bg-base-200">{dadosExternos.embalagem}</div>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Quantidade -->
|
||||
{#if dadosExternos.quantidade}
|
||||
<div class="form-control">
|
||||
<label class="label pb-2">
|
||||
<span class="label-text font-semibold">Quantidade</span>
|
||||
</label>
|
||||
<div class="input input-bordered bg-base-200/80 text-base-content/90 border-base-300">
|
||||
{dadosExternos.quantidade}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Embalagem -->
|
||||
{#if dadosExternos.embalagem}
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label pb-2">
|
||||
<span class="label-text font-semibold">Embalagem</span>
|
||||
</label>
|
||||
<div class="input input-bordered bg-base-200/80 text-base-content/90 border-base-300">
|
||||
{dadosExternos.embalagem}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Descrição -->
|
||||
{#if dadosExternos.descricao}
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label pb-2">
|
||||
<span class="label-text font-semibold">Descrição</span>
|
||||
</label>
|
||||
<div class="textarea textarea-bordered bg-base-200/80 min-h-[100px] text-base-content/90 whitespace-pre-wrap border-base-300">
|
||||
{dadosExternos.descricao}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="border-base-300 flex flex-shrink-0 justify-end gap-3 border-t px-6 py-4">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost"
|
||||
<div class="border-base-300 flex flex-shrink-0 justify-end gap-3 border-t bg-base-200/30 px-6 py-4">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost gap-2"
|
||||
onclick={descartarDadosExternos}
|
||||
disabled={carregandoImagemExterna}
|
||||
>
|
||||
<X class="h-5 w-5" />
|
||||
Descartar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary gap-2 shadow-lg"
|
||||
onclick={usarDadosExternos}
|
||||
disabled={carregandoImagemExterna}
|
||||
>
|
||||
@@ -805,26 +884,38 @@
|
||||
<!-- Modal de Busca em Andamento -->
|
||||
{#if modalBuscando}
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
||||
class="pointer-events-none fixed inset-0 z-50"
|
||||
style="animation: fadeIn 0.2s ease-out;"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-buscando-title"
|
||||
aria-live="polite"
|
||||
>
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="bg-base-100 mx-4 w-full max-w-md transform rounded-2xl shadow-2xl transition-all"
|
||||
class="pointer-events-auto absolute inset-0 bg-black/50 backdrop-blur-md transition-opacity duration-300"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
|
||||
<!-- Modal Box -->
|
||||
<div
|
||||
class="pointer-events-auto absolute left-1/2 top-1/2 z-10 flex max-h-[90vh] w-full max-w-md transform -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-3xl bg-base-100 shadow-2xl transition-all duration-300"
|
||||
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="border-base-300 from-info/10 to-info/5 border-b bg-linear-to-r p-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="rounded-2xl bg-info/20 p-3">
|
||||
<Loader2 class="h-6 w-6 text-info animate-spin" strokeWidth={2.5} />
|
||||
<div
|
||||
class="border-base-300 from-info/10 via-info/5 to-transparent flex flex-shrink-0 items-center justify-between border-b bg-gradient-to-r px-6 py-5"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="rounded-2xl bg-info/20 p-3 shadow-lg">
|
||||
<Loader2 class="h-6 w-6 animate-spin text-info" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 id="modal-buscando-title" class="text-xl font-bold">
|
||||
<h3 id="modal-buscando-title" class="text-base-content text-xl font-bold">
|
||||
Buscando produto...
|
||||
</h3>
|
||||
<p class="text-base-content/70 text-sm mt-1">
|
||||
<p class="text-base-content/70 mt-1 text-sm">
|
||||
Procurando informações do código de barras
|
||||
</p>
|
||||
</div>
|
||||
@@ -832,23 +923,28 @@
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-6">
|
||||
<div class="flex flex-col items-center justify-center gap-4">
|
||||
<div class="text-center">
|
||||
<p class="text-base-content/80 text-base mb-2">
|
||||
Código: <span class="font-mono font-semibold">{codigoBarrasBuscado}</span>
|
||||
</p>
|
||||
{#if !buscandoExterno}
|
||||
<div class="flex items-center justify-center gap-2 text-sm text-base-content/60">
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
<span>Buscando no banco de dados...</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center justify-center gap-2 text-sm text-base-content/60">
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
<span>Buscando em base externa...</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-1 px-6 py-8">
|
||||
<div class="flex flex-col items-center justify-center gap-6">
|
||||
<div class="text-center space-y-4">
|
||||
<div class="inline-flex items-center justify-center rounded-full bg-info/10 p-4">
|
||||
<Loader2 class="h-8 w-8 animate-spin text-info" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<p class="text-base-content/90 text-base font-medium">
|
||||
Código: <span class="font-mono font-semibold text-primary">{codigoBarrasBuscado}</span>
|
||||
</p>
|
||||
{#if !buscandoExterno}
|
||||
<div class="flex items-center justify-center gap-2 text-sm text-base-content/70">
|
||||
<span class="loading loading-spinner loading-sm text-info"></span>
|
||||
<span>Buscando no banco de dados...</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center justify-center gap-2 text-sm text-base-content/70">
|
||||
<span class="loading loading-spinner loading-sm text-info"></span>
|
||||
<span>Buscando em base externa...</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -859,65 +955,77 @@
|
||||
<!-- Modal de Produto Não Encontrado -->
|
||||
{#if modalProdutoNaoEncontrado}
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
||||
class="pointer-events-none fixed inset-0 z-50"
|
||||
style="animation: fadeIn 0.2s ease-out;"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-nao-encontrado-title"
|
||||
aria-describedby="modal-nao-encontrado-description"
|
||||
>
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="bg-base-100 mx-4 w-full max-w-md transform rounded-2xl shadow-2xl transition-all"
|
||||
class="pointer-events-auto absolute inset-0 bg-black/50 backdrop-blur-md transition-opacity duration-300"
|
||||
onclick={fecharModalProdutoNaoEncontrado}
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
|
||||
<!-- Modal Box -->
|
||||
<div
|
||||
class="pointer-events-auto absolute left-1/2 top-1/2 z-10 flex max-h-[90vh] w-full max-w-md transform -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-3xl bg-base-100 shadow-2xl transition-all duration-300"
|
||||
style="animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="border-base-300 from-warning/10 to-warning/5 border-b bg-linear-to-r p-6">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="rounded-2xl bg-warning/20 p-3">
|
||||
<AlertCircle class="h-6 w-6 text-warning" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 id="modal-nao-encontrado-title" class="text-xl font-bold">
|
||||
Produto não encontrado
|
||||
</h3>
|
||||
<p class="text-base-content/70 text-sm mt-1">
|
||||
O código de barras não foi encontrado
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="border-base-300 from-warning/10 via-warning/5 to-transparent flex flex-shrink-0 items-center justify-between border-b bg-gradient-to-r px-6 py-5"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="rounded-2xl bg-warning/20 p-3 shadow-lg">
|
||||
<AlertCircle class="h-6 w-6 text-warning" strokeWidth={2.5} />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 id="modal-nao-encontrado-title" class="text-base-content text-xl font-bold">
|
||||
Produto não encontrado
|
||||
</h3>
|
||||
<p id="modal-nao-encontrado-description" class="text-base-content/70 mt-1 text-sm">
|
||||
O código de barras não foi encontrado
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle btn-ghost"
|
||||
onclick={fecharModalProdutoNaoEncontrado}
|
||||
aria-label="Fechar"
|
||||
>
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle btn-ghost hover:bg-base-300 transition-colors"
|
||||
onclick={fecharModalProdutoNaoEncontrado}
|
||||
aria-label="Fechar modal"
|
||||
>
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="rounded-lg bg-base-200 p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<Info class="h-5 w-5 text-info mt-0.5 flex-shrink-0" strokeWidth={2} />
|
||||
<div class="flex-1">
|
||||
<p class="text-base-content/90 text-sm font-medium mb-1">
|
||||
Código de barras: <span class="font-mono font-semibold">{codigoBarrasBuscado}</span>
|
||||
</p>
|
||||
<p class="text-base-content/70 text-sm">
|
||||
Este produto não foi encontrado no banco de dados local nem em bases externas.
|
||||
Por favor, preencha as informações manualmente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-scroll flex-1 overflow-y-auto px-6 py-6">
|
||||
<div class="alert alert-info border-info/30 bg-info/5 shadow-md">
|
||||
<Info class="h-5 w-5 shrink-0 text-info" strokeWidth={2} />
|
||||
<div class="flex-1 space-y-2">
|
||||
<p class="text-base-content/90 text-sm font-semibold">
|
||||
Código de barras: <span class="font-mono font-bold text-primary">{codigoBarrasBuscado}</span>
|
||||
</p>
|
||||
<p class="text-base-content/80 text-sm leading-relaxed">
|
||||
Este produto não foi encontrado no banco de dados local nem em bases externas.
|
||||
Por favor, preencha as informações manualmente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="border-base-300 flex flex-shrink-0 justify-end gap-3 border-t px-6 py-4">
|
||||
<button type="button" class="btn btn-primary" onclick={fecharModalProdutoNaoEncontrado}>
|
||||
<div class="border-base-300 flex flex-shrink-0 justify-end border-t bg-base-200/30 px-6 py-4">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary gap-2 shadow-lg"
|
||||
onclick={fecharModalProdutoNaoEncontrado}
|
||||
>
|
||||
<Check class="h-5 w-5" />
|
||||
Entendi, vou preencher manualmente
|
||||
</button>
|
||||
</div>
|
||||
@@ -926,4 +1034,66 @@
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<style>
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -45%) scale(0.96);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar customizada para modais */
|
||||
:global(.modal-scroll) {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: hsl(var(--bc) / 0.3) transparent;
|
||||
scroll-behavior: smooth;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
:global(.modal-scroll::-webkit-scrollbar) {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
:global(.modal-scroll::-webkit-scrollbar-track) {
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:global(.modal-scroll::-webkit-scrollbar-thumb) {
|
||||
background-color: hsl(var(--bc) / 0.3);
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
:global(.modal-scroll::-webkit-scrollbar-thumb:hover) {
|
||||
background-color: hsl(var(--bc) / 0.5);
|
||||
}
|
||||
|
||||
/* Melhorias de acessibilidade e responsividade */
|
||||
@media (max-width: 640px) {
|
||||
:global([role="dialog"]) > div:last-child {
|
||||
max-width: calc(100vw - 2rem);
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Prevenção de scroll do body quando modal está aberto */
|
||||
:global(body.modal-open) {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -1258,6 +1258,65 @@ export const ignorarAlerta = mutation({
|
||||
}
|
||||
});
|
||||
|
||||
export const deletarMaterial = mutation({
|
||||
args: {
|
||||
id: v.id('materiais')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.runQuery(internal.permissoesAcoes.assertPermissaoAcaoAtual, {
|
||||
recurso: 'almoxarifado',
|
||||
acao: 'deletar_material'
|
||||
});
|
||||
|
||||
const material = await ctx.db.get(args.id);
|
||||
if (!material) throw new Error('Material não encontrado');
|
||||
|
||||
// Verificar se há movimentações relacionadas
|
||||
const movimentacoes = await ctx.db
|
||||
.query('movimentacoesEstoque')
|
||||
.withIndex('by_materialId', (q) => q.eq('materialId', args.id))
|
||||
.first();
|
||||
|
||||
if (movimentacoes) {
|
||||
throw new Error('Não é possível excluir material com movimentações de estoque registradas. O material possui histórico de movimentações e deve ser desativado ao invés de excluído.');
|
||||
}
|
||||
|
||||
// Verificar se há requisições relacionadas
|
||||
const requisicoes = await ctx.db
|
||||
.query('requisicaoItens')
|
||||
.withIndex('by_materialId', (q) => q.eq('materialId', args.id))
|
||||
.first();
|
||||
|
||||
if (requisicoes) {
|
||||
throw new Error('Não é possível excluir material com requisições registradas. O material possui requisições associadas e deve ser desativado ao invés de excluído.');
|
||||
}
|
||||
|
||||
// Verificar se há alertas relacionados
|
||||
const alertas = await ctx.db
|
||||
.query('alertasEstoque')
|
||||
.withIndex('by_materialId', (q) => q.eq('materialId', args.id))
|
||||
.first();
|
||||
|
||||
if (alertas) {
|
||||
// Deletar alertas relacionados
|
||||
const todosAlertas = await ctx.db
|
||||
.query('alertasEstoque')
|
||||
.withIndex('by_materialId', (q) => q.eq('materialId', args.id))
|
||||
.collect();
|
||||
|
||||
for (const alerta of todosAlertas) {
|
||||
await ctx.db.delete(alerta._id);
|
||||
}
|
||||
}
|
||||
|
||||
// Registrar histórico antes de deletar
|
||||
await registrarHistorico(ctx, 'material', args.id.toString(), 'exclusao', material, undefined);
|
||||
|
||||
// Deletar o material
|
||||
await ctx.db.delete(args.id);
|
||||
}
|
||||
});
|
||||
|
||||
// ========== INTERNAL ACTIONS ==========
|
||||
|
||||
export const registrarHistoricoAlteracao = internalMutation({
|
||||
|
||||
Reference in New Issue
Block a user