feat: enhance 'Almoxarifado' functionality by integrating barcode scanning for material entry and exit, improving user experience with loading indicators and error handling for better inventory management

This commit is contained in:
2025-12-22 10:52:46 -03:00
parent e19c24b9ab
commit b1db926ab4
8 changed files with 1125 additions and 287 deletions

View File

@@ -169,7 +169,8 @@
{
label: 'Listar Materiais',
link: '/almoxarifado/materiais',
permission: { recurso: 'almoxarifado', acao: 'listar' }
permission: { recurso: 'almoxarifado', acao: 'listar' },
excludePaths: ['/almoxarifado/materiais/cadastro']
},
{
label: 'Movimentações',

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { onMount, onDestroy, tick } from 'svelte';
import { Html5Qrcode, type Html5QrcodeResult } from 'html5-qrcode';
import { Camera, X, Scan } from 'lucide-svelte';
@@ -29,7 +29,43 @@
};
async function startScanning() {
if (!scannerElement) return;
// Aguardar o DOM ser atualizado
await tick();
// Verificar se o elemento existe no DOM
const element = document.getElementById(scannerId);
if (!element) {
// Tentar novamente após um pequeno delay
setTimeout(async () => {
const retryElement = document.getElementById(scannerId);
if (!retryElement) {
const errorMsg = 'Elemento do scanner não encontrado no DOM';
error = errorMsg;
scanning = false;
if (onError) {
onError(errorMsg);
}
return;
}
await startScanningInternal();
}, 100);
return;
}
await startScanningInternal();
}
async function startScanningInternal() {
const element = document.getElementById(scannerId);
if (!element) {
const errorMsg = 'Elemento do scanner não encontrado';
error = errorMsg;
scanning = false;
if (onError) {
onError(errorMsg);
}
return;
}
try {
error = null;
@@ -37,26 +73,80 @@
scanner = new Html5Qrcode(scannerId);
await scanner.start(
{ facingMode: 'environment' },
config,
(decodedText: string, decodedResult: Html5QrcodeResult) => {
handleScannedCode(decodedText);
},
(errorMessage: string) => {
// Ignorar erros de leitura contínua
if (errorMessage.includes('No MultiFormat Readers')) {
return;
// Tentar primeiro com câmera traseira (environment), depois frontal (user)
let cameraConfig = { facingMode: 'environment' as const };
try {
await scanner.start(
cameraConfig,
config,
(decodedText: string, decodedResult: Html5QrcodeResult) => {
handleScannedCode(decodedText);
},
(errorMessage: string) => {
// Ignorar erros de leitura contínua
if (errorMessage.includes('No MultiFormat Readers')) {
return;
}
}
);
} catch (cameraError) {
// Se falhar com câmera traseira, tentar com frontal (útil para PC)
if (cameraConfig.facingMode === 'environment') {
console.log('Tentando câmera frontal...');
cameraConfig = { facingMode: 'user' };
await scanner.start(
cameraConfig,
config,
(decodedText: string, decodedResult: Html5QrcodeResult) => {
handleScannedCode(decodedText);
},
(errorMessage: string) => {
if (errorMessage.includes('No MultiFormat Readers')) {
return;
}
}
);
} else {
throw cameraError;
}
);
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Erro ao iniciar scanner';
let errorMessage = 'Erro ao iniciar scanner';
if (err instanceof Error) {
errorMessage = err.message;
// Mensagens de erro mais amigáveis
if (errorMessage.includes('Permission denied') || errorMessage.includes('NotAllowedError')) {
errorMessage = 'Permissão de câmera negada. Por favor, permita o acesso à câmera nas configurações do navegador.';
} else if (errorMessage.includes('NotFoundError') || errorMessage.includes('No camera found')) {
errorMessage = 'Nenhuma câmera encontrada. Verifique se há uma câmera conectada ao dispositivo.';
} else if (errorMessage.includes('NotReadableError') || errorMessage.includes('TrackStartError')) {
errorMessage = 'Câmera está sendo usada por outro aplicativo. Feche outros aplicativos que possam estar usando a câmera.';
} else if (errorMessage.includes('OverconstrainedError')) {
errorMessage = 'Câmera não suporta as configurações necessárias.';
}
}
error = errorMessage;
scanning = false;
// Limpar scanner em caso de erro
if (scanner) {
try {
await scanner.clear();
} catch (clearErr) {
console.error('Erro ao limpar scanner:', clearErr);
}
scanner = null;
}
if (onError) {
onError(errorMessage);
}
console.error('Erro ao iniciar scanner:', err);
}
}
@@ -128,7 +218,12 @@
$effect(() => {
if (enabled && !scanning) {
startScanning();
// Aguardar um pouco para garantir que o DOM foi atualizado
setTimeout(() => {
if (enabled && !scanning) {
startScanning();
}
}, 50);
} else if (!enabled && scanning) {
stopScanning();
}
@@ -171,12 +266,39 @@
{#if error}
<div class="alert alert-error mb-4">
<span>{error}</span>
<button
type="button"
class="btn btn-sm btn-ghost mt-2"
onclick={async () => {
error = null;
scanning = false;
// Limpar scanner anterior se existir
if (scanner) {
try {
await scanner.clear();
} catch (err) {
console.error('Erro ao limpar scanner:', err);
}
scanner = null;
}
// Aguardar um pouco antes de tentar novamente
await tick();
setTimeout(() => {
if (enabled && !scanning) {
startScanning();
}
}, 100);
}}
>
Tentar novamente
</button>
</div>
{/if}
{#if scanning}
<div class="relative">
<div id={scannerId} bind:this={scannerElement}></div>
<!-- Sempre renderizar o elemento quando enabled for true -->
<div class="relative">
<div id={scannerId} bind:this={scannerElement}></div>
{#if scanning}
<div class="mt-4 text-center">
<p class="text-sm text-base-content/70">
Posicione o código de barras dentro da área de leitura
@@ -185,13 +307,13 @@
Ou use um leitor USB/Bluetooth para escanear
</p>
</div>
</div>
{:else if !error}
<div class="text-center py-8">
<Camera class="h-16 w-16 mx-auto mb-4 text-base-content/30" />
<p class="text-base-content/70">Iniciando scanner...</p>
</div>
{/if}
{:else if !error}
<div class="text-center py-8">
<Camera class="h-16 w-16 mx-auto mb-4 text-base-content/30" />
<p class="text-base-content/70">Iniciando scanner...</p>
</div>
{/if}
</div>
<div class="card-actions justify-end mt-4">
<button type="button" class="btn btn-ghost" onclick={() => { enabled = false; }}>