feat: integrate barcode scanning functionality in 'Almoxarifado' for improved product search and registration, along with image upload support for enhanced inventory management
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { Package, Plus, Search, Edit, Eye, AlertTriangle } from 'lucide-svelte';
|
||||
import BarcodeScanner from '$lib/components/almoxarifado/BarcodeScanner.svelte';
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
@@ -14,6 +15,10 @@
|
||||
let filtroCategoria = $state('');
|
||||
let filtroAtivo = $state<boolean | ''>('');
|
||||
let filtroEstoqueBaixo = $state(false);
|
||||
let scannerEnabled = $state(false);
|
||||
let materialEncontrado = $state<Doc<'materiais'> | null>(null);
|
||||
let buscandoPorCodigoBarras = $state(false);
|
||||
let buscaTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const categorias = $derived(
|
||||
Array.from(new Set(materiais.map((m) => m.categoria).filter(Boolean))).sort()
|
||||
@@ -25,7 +30,8 @@
|
||||
const okBusca =
|
||||
!busca ||
|
||||
m.codigo.toLowerCase().includes(busca) ||
|
||||
m.nome.toLowerCase().includes(busca);
|
||||
m.nome.toLowerCase().includes(busca) ||
|
||||
(m.codigoBarras && m.codigoBarras.toLowerCase().includes(busca));
|
||||
const okCategoria = !filtroCategoria || m.categoria === filtroCategoria;
|
||||
const okAtivo = filtroAtivo === '' || m.ativo === filtroAtivo;
|
||||
const okEstoqueBaixo = !filtroEstoqueBaixo || m.estoqueAtual <= m.estoqueMinimo;
|
||||
@@ -33,6 +39,34 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function buscarPorCodigoBarras(codigo: string) {
|
||||
if (!codigo.trim() || codigo.trim().length < 8) {
|
||||
return;
|
||||
}
|
||||
|
||||
buscandoPorCodigoBarras = true;
|
||||
|
||||
try {
|
||||
const material = await client.query(api.almoxarifado.buscarMaterialPorCodigoBarras, {
|
||||
codigoBarras: codigo.trim()
|
||||
});
|
||||
|
||||
if (material) {
|
||||
materialEncontrado = material;
|
||||
// Filtrar para mostrar apenas o produto encontrado
|
||||
filtroBusca = material.codigoBarras || material.codigo;
|
||||
// Navegar para detalhes do produto após um pequeno delay
|
||||
setTimeout(() => {
|
||||
goto(resolve(`/almoxarifado/materiais/${material._id}`));
|
||||
}, 500);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Erro ao buscar produto:', err);
|
||||
} finally {
|
||||
buscandoPorCodigoBarras = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const data = await client.query(api.almoxarifado.listarMateriais, {});
|
||||
materiais = data ?? [];
|
||||
@@ -50,6 +84,48 @@
|
||||
function navCadastro() {
|
||||
goto(resolve('/almoxarifado/materiais/cadastro'));
|
||||
}
|
||||
|
||||
async function handleBarcodeScanned(barcode: string) {
|
||||
await buscarPorCodigoBarras(barcode);
|
||||
if (!materialEncontrado) {
|
||||
// Produto não encontrado, oferecer cadastro
|
||||
if (confirm('Produto não encontrado. Deseja cadastrar um novo produto com este código de barras?')) {
|
||||
goto(resolve('/almoxarifado/materiais/cadastro'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Busca automática quando código de barras é digitado no campo de busca
|
||||
$effect(() => {
|
||||
const busca = filtroBusca.trim();
|
||||
|
||||
// Limpar timeout anterior
|
||||
if (buscaTimeout) {
|
||||
clearTimeout(buscaTimeout);
|
||||
}
|
||||
|
||||
// Se a busca foi limpa, resetar material encontrado
|
||||
if (!busca) {
|
||||
materialEncontrado = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar se parece ser um código de barras (apenas números, 8-14 caracteres)
|
||||
const pareceCodigoBarras = /^\d{8,14}$/.test(busca);
|
||||
|
||||
if (pareceCodigoBarras) {
|
||||
// Aguardar 1 segundo após parar de digitar antes de buscar
|
||||
buscaTimeout = setTimeout(() => {
|
||||
buscarPorCodigoBarras(busca);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (buscaTimeout) {
|
||||
clearTimeout(buscaTimeout);
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="container mx-auto px-4 py-6">
|
||||
@@ -85,7 +161,14 @@
|
||||
<!-- Filtros -->
|
||||
<div class="card bg-base-100 border border-base-300 mb-6 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold mb-4">Filtros de Busca</h3>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold">Filtros de Busca</h3>
|
||||
<BarcodeScanner
|
||||
enabled={scannerEnabled}
|
||||
onScan={handleBarcodeScanned}
|
||||
onError={(error) => console.error('Erro no scanner:', error)}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
@@ -95,10 +178,13 @@
|
||||
<Search class="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-base-content/40" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Código ou nome..."
|
||||
class="input input-bordered w-full pl-10"
|
||||
placeholder="Código, nome ou código de barras..."
|
||||
class="input input-bordered w-full pl-10 {buscandoPorCodigoBarras ? 'input-info' : ''}"
|
||||
bind:value={filtroBusca}
|
||||
/>
|
||||
{#if buscandoPorCodigoBarras}
|
||||
<span class="loading loading-spinner loading-xs absolute right-3 top-1/2 -translate-y-1/2"></span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { Package, Save, ArrowLeft } from 'lucide-svelte';
|
||||
import BarcodeScanner from '$lib/components/almoxarifado/BarcodeScanner.svelte';
|
||||
import ImageUpload from '$lib/components/almoxarifado/ImageUpload.svelte';
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
@@ -17,8 +19,14 @@
|
||||
let estoqueAtual = $state(0);
|
||||
let localizacao = $state('');
|
||||
let fornecedor = $state('');
|
||||
let codigoBarras = $state('');
|
||||
let imagemBase64 = $state<string | null>(null);
|
||||
let scannerEnabled = $state(false);
|
||||
let loading = $state(false);
|
||||
let buscandoProduto = $state(false);
|
||||
let notice = $state<{ kind: 'success' | 'error'; text: string } | null>(null);
|
||||
let buscaTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let ultimoCodigoBuscado = $state('');
|
||||
|
||||
const unidadesMedida = ['UN', 'CX', 'KG', 'L', 'M', 'M²', 'M³', 'PC', 'DZ'];
|
||||
const categoriasComuns = [
|
||||
@@ -38,6 +46,94 @@
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
async function buscarProdutoPorCodigoBarras(barcode: string, mostrarMensagemSucesso = true) {
|
||||
if (!barcode.trim() || barcode.trim().length < 8) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Evitar busca duplicada do mesmo código
|
||||
if (ultimoCodigoBuscado === barcode.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
buscandoProduto = true;
|
||||
ultimoCodigoBuscado = barcode.trim();
|
||||
|
||||
try {
|
||||
// Buscar produto existente pelo código de barras
|
||||
const materialExistente = await client.query(api.almoxarifado.buscarMaterialPorCodigoBarras, {
|
||||
codigoBarras: barcode.trim()
|
||||
});
|
||||
|
||||
if (materialExistente) {
|
||||
// Preencher campos automaticamente
|
||||
codigo = materialExistente.codigo;
|
||||
nome = materialExistente.nome;
|
||||
descricao = materialExistente.descricao || '';
|
||||
categoria = materialExistente.categoria;
|
||||
unidadeMedida = materialExistente.unidadeMedida;
|
||||
estoqueMinimo = materialExistente.estoqueMinimo;
|
||||
estoqueMaximo = materialExistente.estoqueMaximo;
|
||||
estoqueAtual = materialExistente.estoqueAtual;
|
||||
localizacao = materialExistente.localizacao || '';
|
||||
fornecedor = materialExistente.fornecedor || '';
|
||||
imagemBase64 = materialExistente.imagemBase64 || null;
|
||||
|
||||
if (mostrarMensagemSucesso) {
|
||||
mostrarMensagem('success', 'Produto encontrado! Campos preenchidos automaticamente.');
|
||||
}
|
||||
} else {
|
||||
// Produto não encontrado
|
||||
if (mostrarMensagemSucesso) {
|
||||
mostrarMensagem('success', 'Código de barras lido. Complete as informações do produto.');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Erro ao buscar produto';
|
||||
if (mostrarMensagemSucesso) {
|
||||
mostrarMensagem('error', message);
|
||||
}
|
||||
} finally {
|
||||
buscandoProduto = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBarcodeScanned(barcode: string) {
|
||||
codigoBarras = barcode;
|
||||
await buscarProdutoPorCodigoBarras(barcode, true);
|
||||
}
|
||||
|
||||
// Busca automática quando código de barras é digitado manualmente
|
||||
$effect(() => {
|
||||
const codigo = codigoBarras.trim();
|
||||
|
||||
// Limpar timeout anterior
|
||||
if (buscaTimeout) {
|
||||
clearTimeout(buscaTimeout);
|
||||
}
|
||||
|
||||
// Se o código foi limpo, resetar último código buscado
|
||||
if (!codigo) {
|
||||
ultimoCodigoBuscado = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Aguardar 800ms após parar de digitar antes de buscar
|
||||
// Isso evita muitas buscas enquanto o usuário está digitando
|
||||
buscaTimeout = setTimeout(() => {
|
||||
// Só buscar se o código tiver pelo menos 8 caracteres (tamanho mínimo de código de barras)
|
||||
if (codigo.length >= 8) {
|
||||
buscarProdutoPorCodigoBarras(codigo, false);
|
||||
}
|
||||
}, 800);
|
||||
|
||||
return () => {
|
||||
if (buscaTimeout) {
|
||||
clearTimeout(buscaTimeout);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
// Validação
|
||||
if (!codigo.trim() || !nome.trim() || !categoria.trim()) {
|
||||
@@ -73,7 +169,9 @@
|
||||
estoqueMaximo,
|
||||
estoqueAtual,
|
||||
localizacao: localizacao.trim() || undefined,
|
||||
fornecedor: fornecedor.trim() || undefined
|
||||
fornecedor: fornecedor.trim() || undefined,
|
||||
codigoBarras: codigoBarras.trim() || undefined,
|
||||
imagemBase64: imagemBase64 || undefined
|
||||
});
|
||||
|
||||
mostrarMensagem('success', 'Material cadastrado com sucesso!');
|
||||
@@ -135,6 +233,15 @@
|
||||
<div class="card bg-base-100 border border-base-300 shadow-xl">
|
||||
<div class="card-body">
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
|
||||
<!-- Leitor de Código de Barras -->
|
||||
<div class="mb-6">
|
||||
<BarcodeScanner
|
||||
enabled={scannerEnabled}
|
||||
onScan={handleBarcodeScanned}
|
||||
onError={(error) => mostrarMensagem('error', error)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<!-- Código -->
|
||||
<div class="form-control md:col-span-1">
|
||||
@@ -153,6 +260,31 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Código de Barras -->
|
||||
<div class="form-control md:col-span-1">
|
||||
<label class="label">
|
||||
<span class="label-text">Código de Barras</span>
|
||||
{#if buscandoProduto}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{/if}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered {buscandoProduto ? 'input-info' : ''}"
|
||||
placeholder="EAN-13, UPC, etc."
|
||||
bind:value={codigoBarras}
|
||||
/>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">
|
||||
{#if buscandoProduto}
|
||||
Buscando produto...
|
||||
{:else}
|
||||
Digite ou use o leitor acima para escanear
|
||||
{/if}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Nome -->
|
||||
<div class="form-control md:col-span-1">
|
||||
<label class="label">
|
||||
@@ -283,6 +415,17 @@
|
||||
bind:value={fornecedor}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Imagem do Produto -->
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label">
|
||||
<span class="label-text">Imagem do Produto</span>
|
||||
</label>
|
||||
<ImageUpload bind:value={imagemBase64} />
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Upload opcional da imagem do produto</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Botões -->
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import { useQuery } from 'convex-svelte';
|
||||
import { resolve } from '$app/paths';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import jsPDF from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
import ExcelJS from 'exceljs';
|
||||
import logoGovPE from '$lib/assets/logo_governo_PE.png';
|
||||
import {
|
||||
BarChart3,
|
||||
Package,
|
||||
@@ -11,7 +17,9 @@
|
||||
CheckCircle,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Settings
|
||||
Settings,
|
||||
FileText,
|
||||
FileSpreadsheet
|
||||
} from 'lucide-svelte';
|
||||
|
||||
const statsQuery = useQuery(api.almoxarifado.obterEstatisticas, {});
|
||||
@@ -19,6 +27,9 @@
|
||||
const movimentacoesQuery = useQuery(api.almoxarifado.listarMovimentacoes, {});
|
||||
const alertasQuery = useQuery(api.almoxarifado.listarAlertas, { status: 'ativo' });
|
||||
|
||||
let gerandoRelatorio = $state(false);
|
||||
let tipoRelatorioGerando = $state<string | null>(null);
|
||||
|
||||
// Agrupar materiais por categoria
|
||||
const materiaisPorCategoria = $derived(() => {
|
||||
if (!materiaisQuery.data) return {};
|
||||
@@ -45,9 +56,693 @@
|
||||
};
|
||||
});
|
||||
|
||||
function exportarRelatorio(tipo: string) {
|
||||
// Implementar exportação (CSV/Excel)
|
||||
alert(`Exportação de ${tipo} será implementada em breve`);
|
||||
// Função auxiliar para carregar logo
|
||||
async function carregarLogo(): Promise<HTMLImageElement | null> {
|
||||
try {
|
||||
const logoImg = new Image();
|
||||
logoImg.crossOrigin = 'anonymous';
|
||||
logoImg.src = logoGovPE;
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
logoImg.onload = () => resolve();
|
||||
logoImg.onerror = () => reject();
|
||||
setTimeout(() => reject(), 3000);
|
||||
});
|
||||
return logoImg;
|
||||
} catch (err) {
|
||||
console.warn('Não foi possível carregar a logo:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Função auxiliar para adicionar logo ao PDF
|
||||
async function adicionarLogoPDF(doc: jsPDF): Promise<number> {
|
||||
try {
|
||||
const logoImg = await carregarLogo();
|
||||
if (logoImg) {
|
||||
const logoWidth = 25;
|
||||
const aspectRatio = logoImg.height / logoImg.width;
|
||||
const logoHeight = logoWidth * aspectRatio;
|
||||
doc.addImage(logoImg, 'PNG', 15, 10, logoWidth, logoHeight);
|
||||
return 10 + logoHeight + 5;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Erro ao adicionar logo:', err);
|
||||
}
|
||||
return 20;
|
||||
}
|
||||
|
||||
// Função auxiliar para adicionar rodapé ao PDF
|
||||
function adicionarRodapePDF(doc: jsPDF) {
|
||||
const pageCount = doc.getNumberOfPages();
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
doc.setPage(i);
|
||||
doc.setFontSize(8);
|
||||
doc.setTextColor(128, 128, 128);
|
||||
doc.text(
|
||||
`SGSE - Sistema de Gerenciamento de Secretaria | Página ${i} de ${pageCount}`,
|
||||
doc.internal.pageSize.getWidth() / 2,
|
||||
doc.internal.pageSize.getHeight() - 10,
|
||||
{ align: 'center' }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Função auxiliar para carregar logo para Excel
|
||||
async function carregarLogoExcel(): Promise<ArrayBuffer | null> {
|
||||
try {
|
||||
const response = await fetch(logoGovPE);
|
||||
if (response.ok) return await response.arrayBuffer();
|
||||
} catch {
|
||||
// Fallback via canvas
|
||||
try {
|
||||
const logoImg = await carregarLogo();
|
||||
if (logoImg) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = logoImg.width;
|
||||
canvas.height = logoImg.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.drawImage(logoImg, 0, 0);
|
||||
const blob = await new Promise<Blob>((resolve, reject) => {
|
||||
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('Falha ao converter'))), 'image/png');
|
||||
});
|
||||
return await blob.arrayBuffer();
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Função auxiliar para adicionar título e logo ao Excel
|
||||
async function adicionarTituloExcel(
|
||||
worksheet: ExcelJS.Worksheet,
|
||||
titulo: string,
|
||||
colunas: number,
|
||||
workbook: ExcelJS.Workbook
|
||||
) {
|
||||
worksheet.getRow(1).height = 60;
|
||||
|
||||
// Mesclar células para logo (A1:B1)
|
||||
if (colunas >= 3) {
|
||||
worksheet.mergeCells(1, 1, 1, 2);
|
||||
}
|
||||
|
||||
const logoCell = worksheet.getCell(1, 1);
|
||||
logoCell.alignment = { vertical: 'middle', horizontal: 'left' };
|
||||
if (colunas >= 3) {
|
||||
logoCell.border = { right: { style: 'thin', color: { argb: 'FFE0E0E0' } } };
|
||||
}
|
||||
|
||||
const logoBuffer = await carregarLogoExcel();
|
||||
if (logoBuffer) {
|
||||
const bytes = new Uint8Array(logoBuffer);
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
const logoBase64 = btoa(binary);
|
||||
const logoId = workbook.addImage({
|
||||
base64: `data:image/png;base64,${logoBase64}`,
|
||||
extension: 'png'
|
||||
});
|
||||
worksheet.addImage(logoId, {
|
||||
tl: { col: 0, row: 0 },
|
||||
ext: { width: 140, height: 55 }
|
||||
});
|
||||
}
|
||||
|
||||
// Título
|
||||
if (colunas >= 3) {
|
||||
worksheet.mergeCells(1, 3, 1, colunas);
|
||||
const titleCell = worksheet.getCell(1, 3);
|
||||
titleCell.value = titulo;
|
||||
titleCell.font = { bold: true, size: 18, color: { argb: 'FF2980B9' } };
|
||||
titleCell.alignment = { vertical: 'middle', horizontal: 'center' };
|
||||
} else {
|
||||
const titleCell = worksheet.getCell(1, colunas);
|
||||
titleCell.value = titulo;
|
||||
titleCell.font = { bold: true, size: 18, color: { argb: 'FF2980B9' } };
|
||||
titleCell.alignment = { vertical: 'middle', horizontal: 'center' };
|
||||
}
|
||||
}
|
||||
|
||||
// Função auxiliar para estilizar cabeçalho Excel
|
||||
function estilizarCabecalhoExcel(row: ExcelJS.Row) {
|
||||
row.height = 22;
|
||||
row.eachCell((cell) => {
|
||||
cell.font = { bold: true, size: 11, color: { argb: 'FFFFFFFF' } };
|
||||
cell.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: 'FF2980B9' }
|
||||
};
|
||||
cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
|
||||
cell.border = {
|
||||
top: { style: 'thin', color: { argb: 'FF000000' } },
|
||||
bottom: { style: 'thin', color: { argb: 'FF000000' } },
|
||||
left: { style: 'thin', color: { argb: 'FF000000' } },
|
||||
right: { style: 'thin', color: { argb: 'FF000000' } }
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Função auxiliar para estilizar linhas Excel (zebra)
|
||||
function estilizarLinhaExcel(row: ExcelJS.Row, isEven: boolean) {
|
||||
row.eachCell((cell) => {
|
||||
cell.font = { size: 10, color: { argb: 'FF000000' } };
|
||||
cell.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: isEven ? 'FFF8F9FA' : 'FFFFFFFF' }
|
||||
};
|
||||
cell.border = {
|
||||
top: { style: 'thin', color: { argb: 'FFE0E0E0' } },
|
||||
bottom: { style: 'thin', color: { argb: 'FFE0E0E0' } },
|
||||
left: { style: 'thin', color: { argb: 'FFE0E0E0' } },
|
||||
right: { style: 'thin', color: { argb: 'FFE0E0E0' } }
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ========== RELATÓRIO: MATERIAIS POR CATEGORIA ==========
|
||||
|
||||
async function gerarPDFMateriaisCategoria() {
|
||||
if (gerandoRelatorio) return;
|
||||
gerandoRelatorio = true;
|
||||
tipoRelatorioGerando = 'materiais-categoria-pdf';
|
||||
|
||||
try {
|
||||
const doc = new jsPDF();
|
||||
let yPos = await adicionarLogoPDF(doc);
|
||||
|
||||
// Título
|
||||
doc.setFontSize(20);
|
||||
doc.setTextColor(41, 128, 185);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('RELATÓRIO DE MATERIAIS POR CATEGORIA', 105, yPos, { align: 'center' });
|
||||
yPos += 10;
|
||||
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(
|
||||
`Gerado em: ${format(new Date(), "dd 'de' MMMM 'de' yyyy 'às' HH:mm", { locale: ptBR })}`,
|
||||
105,
|
||||
yPos,
|
||||
{ align: 'center' }
|
||||
);
|
||||
yPos += 15;
|
||||
|
||||
const dados = materiaisPorCategoria;
|
||||
const dadosArray = Object.entries(dados).map(([categoria, quantidade]) => [categoria, String(quantidade)]);
|
||||
|
||||
if (dadosArray.length === 0) {
|
||||
doc.text('Nenhum dado disponível', 14, yPos);
|
||||
} else {
|
||||
autoTable(doc, {
|
||||
startY: yPos,
|
||||
head: [['Categoria', 'Quantidade']],
|
||||
body: dadosArray,
|
||||
theme: 'striped',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold', textColor: [255, 255, 255] },
|
||||
styles: { fontSize: 10 }
|
||||
});
|
||||
}
|
||||
|
||||
adicionarRodapePDF(doc);
|
||||
doc.save(`relatorio-materiais-categoria-${format(new Date(), 'yyyy-MM-dd-HHmm')}.pdf`);
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar PDF:', error);
|
||||
alert('Erro ao gerar relatório PDF. Tente novamente.');
|
||||
} finally {
|
||||
gerandoRelatorio = false;
|
||||
tipoRelatorioGerando = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function gerarExcelMateriaisCategoria() {
|
||||
if (gerandoRelatorio) return;
|
||||
gerandoRelatorio = true;
|
||||
tipoRelatorioGerando = 'materiais-categoria-excel';
|
||||
|
||||
try {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet('Materiais por Categoria');
|
||||
worksheet.columns = [
|
||||
{ header: 'Categoria', key: 'categoria', width: 40 },
|
||||
{ header: 'Quantidade', key: 'quantidade', width: 15 }
|
||||
];
|
||||
|
||||
await adicionarTituloExcel(worksheet, 'RELATÓRIO DE MATERIAIS POR CATEGORIA', 2, workbook);
|
||||
|
||||
const headerRow = worksheet.getRow(2);
|
||||
headerRow.values = ['Categoria', 'Quantidade'];
|
||||
estilizarCabecalhoExcel(headerRow);
|
||||
|
||||
const dados = materiaisPorCategoria;
|
||||
Object.entries(dados).forEach(([categoria, quantidade], idx) => {
|
||||
const row = worksheet.addRow({ categoria, quantidade });
|
||||
estilizarLinhaExcel(row, idx % 2 === 1);
|
||||
row.getCell(2).alignment = { vertical: 'middle', horizontal: 'center' };
|
||||
});
|
||||
|
||||
worksheet.getColumn(2).numFmt = '#,##0';
|
||||
worksheet.views = [{ state: 'frozen', ySplit: 2, topLeftCell: 'A3', activeCell: 'A3' }];
|
||||
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
const blob = new Blob([buffer], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `relatorio-materiais-categoria-${format(new Date(), 'yyyy-MM-dd-HHmm')}.xlsx`;
|
||||
link.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar Excel:', error);
|
||||
alert('Erro ao gerar relatório Excel. Tente novamente.');
|
||||
} finally {
|
||||
gerandoRelatorio = false;
|
||||
tipoRelatorioGerando = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== RELATÓRIO: MOVIMENTAÇÕES DO MÊS ==========
|
||||
|
||||
async function gerarPDFMovimentacoesMes() {
|
||||
if (gerandoRelatorio) return;
|
||||
gerandoRelatorio = true;
|
||||
tipoRelatorioGerando = 'movimentacoes-mes-pdf';
|
||||
|
||||
try {
|
||||
const doc = new jsPDF();
|
||||
let yPos = await adicionarLogoPDF(doc);
|
||||
|
||||
// Título
|
||||
doc.setFontSize(20);
|
||||
doc.setTextColor(41, 128, 185);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('RELATÓRIO DE MOVIMENTAÇÕES DO MÊS', 105, yPos, { align: 'center' });
|
||||
yPos += 10;
|
||||
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(
|
||||
`Gerado em: ${format(new Date(), "dd 'de' MMMM 'de' yyyy 'às' HH:mm", { locale: ptBR })}`,
|
||||
105,
|
||||
yPos,
|
||||
{ align: 'center' }
|
||||
);
|
||||
yPos += 5;
|
||||
doc.text(
|
||||
`Mês de referência: ${format(new Date(), "MMMM 'de' yyyy", { locale: ptBR })}`,
|
||||
105,
|
||||
yPos,
|
||||
{ align: 'center' }
|
||||
);
|
||||
yPos += 15;
|
||||
|
||||
const dados = movimentacoesMes;
|
||||
const dadosArray = [
|
||||
['Entradas', String(dados.entrada)],
|
||||
['Saídas', String(dados.saida)],
|
||||
['Ajustes', String(dados.ajuste)]
|
||||
];
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPos,
|
||||
head: [['Tipo de Movimentação', 'Quantidade']],
|
||||
body: dadosArray,
|
||||
theme: 'striped',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold', textColor: [255, 255, 255] },
|
||||
styles: { fontSize: 10 }
|
||||
});
|
||||
|
||||
adicionarRodapePDF(doc);
|
||||
doc.save(`relatorio-movimentacoes-mes-${format(new Date(), 'yyyy-MM-dd-HHmm')}.pdf`);
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar PDF:', error);
|
||||
alert('Erro ao gerar relatório PDF. Tente novamente.');
|
||||
} finally {
|
||||
gerandoRelatorio = false;
|
||||
tipoRelatorioGerando = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function gerarExcelMovimentacoesMes() {
|
||||
if (gerandoRelatorio) return;
|
||||
gerandoRelatorio = true;
|
||||
tipoRelatorioGerando = 'movimentacoes-mes-excel';
|
||||
|
||||
try {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet('Movimentações do Mês');
|
||||
worksheet.columns = [
|
||||
{ header: 'Tipo de Movimentação', key: 'tipo', width: 30 },
|
||||
{ header: 'Quantidade', key: 'quantidade', width: 15 }
|
||||
];
|
||||
|
||||
await adicionarTituloExcel(worksheet, 'RELATÓRIO DE MOVIMENTAÇÕES DO MÊS', 2, workbook);
|
||||
|
||||
const headerRow = worksheet.getRow(2);
|
||||
headerRow.values = ['Tipo de Movimentação', 'Quantidade'];
|
||||
estilizarCabecalhoExcel(headerRow);
|
||||
|
||||
const dados = movimentacoesMes;
|
||||
const tipos = [
|
||||
{ tipo: 'Entradas', quantidade: dados.entrada },
|
||||
{ tipo: 'Saídas', quantidade: dados.saida },
|
||||
{ tipo: 'Ajustes', quantidade: dados.ajuste }
|
||||
];
|
||||
|
||||
tipos.forEach((item, idx) => {
|
||||
const row = worksheet.addRow(item);
|
||||
estilizarLinhaExcel(row, idx % 2 === 1);
|
||||
row.getCell(2).alignment = { vertical: 'middle', horizontal: 'center' };
|
||||
});
|
||||
|
||||
worksheet.getColumn(2).numFmt = '#,##0';
|
||||
worksheet.views = [{ state: 'frozen', ySplit: 2, topLeftCell: 'A3', activeCell: 'A3' }];
|
||||
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
const blob = new Blob([buffer], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `relatorio-movimentacoes-mes-${format(new Date(), 'yyyy-MM-dd-HHmm')}.xlsx`;
|
||||
link.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar Excel:', error);
|
||||
alert('Erro ao gerar relatório Excel. Tente novamente.');
|
||||
} finally {
|
||||
gerandoRelatorio = false;
|
||||
tipoRelatorioGerando = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== RELATÓRIO: ESTOQUE BAIXO ==========
|
||||
|
||||
async function gerarPDFEstoqueBaixo() {
|
||||
if (gerandoRelatorio) return;
|
||||
gerandoRelatorio = true;
|
||||
tipoRelatorioGerando = 'estoque-baixo-pdf';
|
||||
|
||||
try {
|
||||
const doc = new jsPDF();
|
||||
let yPos = await adicionarLogoPDF(doc);
|
||||
|
||||
// Título
|
||||
doc.setFontSize(20);
|
||||
doc.setTextColor(41, 128, 185);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('RELATÓRIO DE MATERIAIS COM ESTOQUE BAIXO', 105, yPos, { align: 'center' });
|
||||
yPos += 10;
|
||||
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(
|
||||
`Gerado em: ${format(new Date(), "dd 'de' MMMM 'de' yyyy 'às' HH:mm", { locale: ptBR })}`,
|
||||
105,
|
||||
yPos,
|
||||
{ align: 'center' }
|
||||
);
|
||||
yPos += 15;
|
||||
|
||||
const materiais = materiaisQuery.data || [];
|
||||
const estoqueBaixo = materiais.filter((m) => m.estoqueAtual <= m.estoqueMinimo);
|
||||
|
||||
if (estoqueBaixo.length === 0) {
|
||||
doc.text('Nenhum material com estoque baixo', 14, yPos);
|
||||
} else {
|
||||
const dadosArray = estoqueBaixo.map((m) => [
|
||||
m.codigo,
|
||||
m.nome,
|
||||
String(m.estoqueAtual),
|
||||
String(m.estoqueMinimo)
|
||||
]);
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPos,
|
||||
head: [['Código', 'Material', 'Estoque Atual', 'Estoque Mínimo']],
|
||||
body: dadosArray,
|
||||
theme: 'striped',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold', textColor: [255, 255, 255] },
|
||||
styles: { fontSize: 9 },
|
||||
columnStyles: {
|
||||
0: { cellWidth: 30 },
|
||||
1: { cellWidth: 80 },
|
||||
2: { cellWidth: 30, halign: 'center' },
|
||||
3: { cellWidth: 30, halign: 'center' }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
adicionarRodapePDF(doc);
|
||||
doc.save(`relatorio-estoque-baixo-${format(new Date(), 'yyyy-MM-dd-HHmm')}.pdf`);
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar PDF:', error);
|
||||
alert('Erro ao gerar relatório PDF. Tente novamente.');
|
||||
} finally {
|
||||
gerandoRelatorio = false;
|
||||
tipoRelatorioGerando = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function gerarExcelEstoqueBaixo() {
|
||||
if (gerandoRelatorio) return;
|
||||
gerandoRelatorio = true;
|
||||
tipoRelatorioGerando = 'estoque-baixo-excel';
|
||||
|
||||
try {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet('Estoque Baixo');
|
||||
worksheet.columns = [
|
||||
{ header: 'Código', key: 'codigo', width: 20 },
|
||||
{ header: 'Material', key: 'nome', width: 50 },
|
||||
{ header: 'Estoque Atual', key: 'estoqueAtual', width: 18 },
|
||||
{ header: 'Estoque Mínimo', key: 'estoqueMinimo', width: 18 }
|
||||
];
|
||||
|
||||
await adicionarTituloExcel(worksheet, 'RELATÓRIO DE MATERIAIS COM ESTOQUE BAIXO', 4, workbook);
|
||||
|
||||
const headerRow = worksheet.getRow(2);
|
||||
headerRow.values = ['Código', 'Material', 'Estoque Atual', 'Estoque Mínimo'];
|
||||
estilizarCabecalhoExcel(headerRow);
|
||||
|
||||
const materiais = materiaisQuery.data || [];
|
||||
const estoqueBaixo = materiais.filter((m) => m.estoqueAtual <= m.estoqueMinimo);
|
||||
|
||||
estoqueBaixo.forEach((material, idx) => {
|
||||
const row = worksheet.addRow({
|
||||
codigo: material.codigo,
|
||||
nome: material.nome,
|
||||
estoqueAtual: material.estoqueAtual,
|
||||
estoqueMinimo: material.estoqueMinimo
|
||||
});
|
||||
estilizarLinhaExcel(row, idx % 2 === 1);
|
||||
row.getCell(3).alignment = { vertical: 'middle', horizontal: 'center' };
|
||||
row.getCell(4).alignment = { vertical: 'middle', horizontal: 'center' };
|
||||
// Destaque para estoque crítico
|
||||
if (material.estoqueAtual < material.estoqueMinimo) {
|
||||
row.getCell(3).fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: 'FFFFE0E0' }
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
worksheet.getColumn(3).numFmt = '#,##0';
|
||||
worksheet.getColumn(4).numFmt = '#,##0';
|
||||
worksheet.views = [{ state: 'frozen', ySplit: 2, topLeftCell: 'A3', activeCell: 'A3' }];
|
||||
worksheet.autoFilter = {
|
||||
from: { row: 2, column: 1 },
|
||||
to: { row: 2, column: 4 }
|
||||
};
|
||||
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
const blob = new Blob([buffer], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `relatorio-estoque-baixo-${format(new Date(), 'yyyy-MM-dd-HHmm')}.xlsx`;
|
||||
link.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar Excel:', error);
|
||||
alert('Erro ao gerar relatório Excel. Tente novamente.');
|
||||
} finally {
|
||||
gerandoRelatorio = false;
|
||||
tipoRelatorioGerando = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== RELATÓRIO: ALERTAS ==========
|
||||
|
||||
async function gerarPDFAlertas() {
|
||||
if (gerandoRelatorio) return;
|
||||
gerandoRelatorio = true;
|
||||
tipoRelatorioGerando = 'alertas-pdf';
|
||||
|
||||
try {
|
||||
const doc = new jsPDF();
|
||||
let yPos = await adicionarLogoPDF(doc);
|
||||
|
||||
// Título
|
||||
doc.setFontSize(20);
|
||||
doc.setTextColor(41, 128, 185);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('RELATÓRIO DE ALERTAS DE ESTOQUE', 105, yPos, { align: 'center' });
|
||||
yPos += 10;
|
||||
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(
|
||||
`Gerado em: ${format(new Date(), "dd 'de' MMMM 'de' yyyy 'às' HH:mm", { locale: ptBR })}`,
|
||||
105,
|
||||
yPos,
|
||||
{ align: 'center' }
|
||||
);
|
||||
yPos += 15;
|
||||
|
||||
const alertas = alertasQuery.data || [];
|
||||
const materiais = materiaisQuery.data || [];
|
||||
|
||||
if (alertas.length === 0) {
|
||||
doc.text('Nenhum alerta ativo', 14, yPos);
|
||||
} else {
|
||||
const dadosArray = alertas.map((alerta) => {
|
||||
const material = materiais.find((m) => m._id === alerta.materialId);
|
||||
return [
|
||||
material?.codigo || '-',
|
||||
material?.nome || 'Material não encontrado',
|
||||
String(alerta.quantidadeAtual),
|
||||
String(alerta.quantidadeMinima),
|
||||
alerta.tipo === 'estoque_zerado' ? 'Zerado' : 'Mínimo'
|
||||
];
|
||||
});
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPos,
|
||||
head: [['Código', 'Material', 'Quantidade Atual', 'Quantidade Mínima', 'Tipo']],
|
||||
body: dadosArray,
|
||||
theme: 'striped',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold', textColor: [255, 255, 255] },
|
||||
styles: { fontSize: 9 },
|
||||
columnStyles: {
|
||||
0: { cellWidth: 25 },
|
||||
1: { cellWidth: 70 },
|
||||
2: { cellWidth: 25, halign: 'center' },
|
||||
3: { cellWidth: 25, halign: 'center' },
|
||||
4: { cellWidth: 25, halign: 'center' }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
adicionarRodapePDF(doc);
|
||||
doc.save(`relatorio-alertas-${format(new Date(), 'yyyy-MM-dd-HHmm')}.pdf`);
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar PDF:', error);
|
||||
alert('Erro ao gerar relatório PDF. Tente novamente.');
|
||||
} finally {
|
||||
gerandoRelatorio = false;
|
||||
tipoRelatorioGerando = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function gerarExcelAlertas() {
|
||||
if (gerandoRelatorio) return;
|
||||
gerandoRelatorio = true;
|
||||
tipoRelatorioGerando = 'alertas-excel';
|
||||
|
||||
try {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet('Alertas');
|
||||
worksheet.columns = [
|
||||
{ header: 'Código', key: 'codigo', width: 20 },
|
||||
{ header: 'Material', key: 'nome', width: 50 },
|
||||
{ header: 'Quantidade Atual', key: 'quantidadeAtual', width: 20 },
|
||||
{ header: 'Quantidade Mínima', key: 'quantidadeMinima', width: 20 },
|
||||
{ header: 'Tipo', key: 'tipo', width: 15 }
|
||||
];
|
||||
|
||||
await adicionarTituloExcel(worksheet, 'RELATÓRIO DE ALERTAS DE ESTOQUE', 5, workbook);
|
||||
|
||||
const headerRow = worksheet.getRow(2);
|
||||
headerRow.values = ['Código', 'Material', 'Quantidade Atual', 'Quantidade Mínima', 'Tipo'];
|
||||
estilizarCabecalhoExcel(headerRow);
|
||||
|
||||
const alertas = alertasQuery.data || [];
|
||||
const materiais = materiaisQuery.data || [];
|
||||
|
||||
alertas.forEach((alerta, idx) => {
|
||||
const material = materiais.find((m) => m._id === alerta.materialId);
|
||||
const row = worksheet.addRow({
|
||||
codigo: material?.codigo || '-',
|
||||
nome: material?.nome || 'Material não encontrado',
|
||||
quantidadeAtual: alerta.quantidadeAtual,
|
||||
quantidadeMinima: alerta.quantidadeMinima,
|
||||
tipo: alerta.tipo === 'estoque_zerado' ? 'Zerado' : 'Mínimo'
|
||||
});
|
||||
estilizarLinhaExcel(row, idx % 2 === 1);
|
||||
row.getCell(3).alignment = { vertical: 'middle', horizontal: 'center' };
|
||||
row.getCell(4).alignment = { vertical: 'middle', horizontal: 'center' };
|
||||
row.getCell(5).alignment = { vertical: 'middle', horizontal: 'center' };
|
||||
// Destaque para alertas críticos
|
||||
if (alerta.tipo === 'estoque_zerado') {
|
||||
row.getCell(5).fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: 'FFFFE0E0' }
|
||||
};
|
||||
row.getCell(5).font = { size: 10, color: { argb: 'FF721C24' }, bold: true };
|
||||
} else {
|
||||
row.getCell(5).fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: 'FFFFF3CD' }
|
||||
};
|
||||
row.getCell(5).font = { size: 10, color: { argb: 'FF856404' } };
|
||||
}
|
||||
});
|
||||
|
||||
worksheet.getColumn(3).numFmt = '#,##0';
|
||||
worksheet.getColumn(4).numFmt = '#,##0';
|
||||
worksheet.views = [{ state: 'frozen', ySplit: 2, topLeftCell: 'A3', activeCell: 'A3' }];
|
||||
worksheet.autoFilter = {
|
||||
from: { row: 2, column: 1 },
|
||||
to: { row: 2, column: 5 }
|
||||
};
|
||||
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
const blob = new Blob([buffer], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `relatorio-alertas-${format(new Date(), 'yyyy-MM-dd-HHmm')}.xlsx`;
|
||||
link.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar Excel:', error);
|
||||
alert('Erro ao gerar relatório Excel. Tente novamente.');
|
||||
} finally {
|
||||
gerandoRelatorio = false;
|
||||
tipoRelatorioGerando = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -149,12 +844,34 @@
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="card-title">Materiais por Categoria</h2>
|
||||
<button
|
||||
class="btn btn-sm btn-ghost"
|
||||
onclick={() => exportarRelatorio('materiais-categoria')}
|
||||
>
|
||||
<Download class="h-4 w-4" />
|
||||
</button>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
onclick={gerarPDFMateriaisCategoria}
|
||||
disabled={gerandoRelatorio}
|
||||
title="Gerar PDF"
|
||||
>
|
||||
{#if gerandoRelatorio && tipoRelatorioGerando === 'materiais-categoria-pdf'}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<FileText class="h-4 w-4" />
|
||||
{/if}
|
||||
PDF
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-success"
|
||||
onclick={gerarExcelMateriaisCategoria}
|
||||
disabled={gerandoRelatorio}
|
||||
title="Gerar Excel"
|
||||
>
|
||||
{#if gerandoRelatorio && tipoRelatorioGerando === 'materiais-categoria-excel'}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<FileSpreadsheet class="h-4 w-4" />
|
||||
{/if}
|
||||
Excel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if materiaisQuery.data && Object.keys(materiaisPorCategoria).length > 0}
|
||||
<div class="space-y-2">
|
||||
@@ -186,12 +903,34 @@
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="card-title">Movimentações do Mês</h2>
|
||||
<button
|
||||
class="btn btn-sm btn-ghost"
|
||||
onclick={() => exportarRelatorio('movimentacoes-mes')}
|
||||
>
|
||||
<Download class="h-4 w-4" />
|
||||
</button>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
onclick={gerarPDFMovimentacoesMes}
|
||||
disabled={gerandoRelatorio}
|
||||
title="Gerar PDF"
|
||||
>
|
||||
{#if gerandoRelatorio && tipoRelatorioGerando === 'movimentacoes-mes-pdf'}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<FileText class="h-4 w-4" />
|
||||
{/if}
|
||||
PDF
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-success"
|
||||
onclick={gerarExcelMovimentacoesMes}
|
||||
disabled={gerandoRelatorio}
|
||||
title="Gerar Excel"
|
||||
>
|
||||
{#if gerandoRelatorio && tipoRelatorioGerando === 'movimentacoes-mes-excel'}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<FileSpreadsheet class="h-4 w-4" />
|
||||
{/if}
|
||||
Excel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -224,12 +963,34 @@
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="card-title">Materiais com Estoque Baixo</h2>
|
||||
<button
|
||||
class="btn btn-sm btn-ghost"
|
||||
onclick={() => exportarRelatorio('estoque-baixo')}
|
||||
>
|
||||
<Download class="h-4 w-4" />
|
||||
</button>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
onclick={gerarPDFEstoqueBaixo}
|
||||
disabled={gerandoRelatorio}
|
||||
title="Gerar PDF"
|
||||
>
|
||||
{#if gerandoRelatorio && tipoRelatorioGerando === 'estoque-baixo-pdf'}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<FileText class="h-4 w-4" />
|
||||
{/if}
|
||||
PDF
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-success"
|
||||
onclick={gerarExcelEstoqueBaixo}
|
||||
disabled={gerandoRelatorio}
|
||||
title="Gerar Excel"
|
||||
>
|
||||
{#if gerandoRelatorio && tipoRelatorioGerando === 'estoque-baixo-excel'}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<FileSpreadsheet class="h-4 w-4" />
|
||||
{/if}
|
||||
Excel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if materiaisQuery.data}
|
||||
{@const estoqueBaixo = materiaisQuery.data.filter(m => m.estoqueAtual <= m.estoqueMinimo)}
|
||||
@@ -279,12 +1040,34 @@
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="card-title">Alertas Recentes</h2>
|
||||
<button
|
||||
class="btn btn-sm btn-ghost"
|
||||
onclick={() => exportarRelatorio('alertas')}
|
||||
>
|
||||
<Download class="h-4 w-4" />
|
||||
</button>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
onclick={gerarPDFAlertas}
|
||||
disabled={gerandoRelatorio}
|
||||
title="Gerar PDF"
|
||||
>
|
||||
{#if gerandoRelatorio && tipoRelatorioGerando === 'alertas-pdf'}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<FileText class="h-4 w-4" />
|
||||
{/if}
|
||||
PDF
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-success"
|
||||
onclick={gerarExcelAlertas}
|
||||
disabled={gerandoRelatorio}
|
||||
title="Gerar Excel"
|
||||
>
|
||||
{#if gerandoRelatorio && tipoRelatorioGerando === 'alertas-excel'}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<FileSpreadsheet class="h-4 w-4" />
|
||||
{/if}
|
||||
Excel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if alertasQuery.data && alertasQuery.data.length > 0}
|
||||
<div class="space-y-2">
|
||||
|
||||
Reference in New Issue
Block a user