278 lines
11 KiB
PHP
278 lines
11 KiB
PHP
<?php
|
|
// registrar_salida.php
|
|
require_once 'includes/header.php';
|
|
|
|
// Restringir acceso solo a roles autorizados
|
|
$allowed_roles = ['Administrador General', 'Encargado de Stock'];
|
|
if (!isset($_SESSION['user_rol']) || !in_array($_SESSION['user_rol'], $allowed_roles)) {
|
|
$_SESSION['error'] = 'No tienes permiso para acceder a esta página.';
|
|
header('Location: index.php');
|
|
exit;
|
|
}
|
|
|
|
|
|
// Fetch products and cities for the dropdowns
|
|
try {
|
|
$pdo = db();
|
|
|
|
// Fetch products
|
|
$stmt_productos = $pdo->query("SELECT id, nombre, sku, codigo_barras FROM productos ORDER BY nombre ASC");
|
|
$productos = $stmt_productos->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Fetch cities
|
|
$stmt_ciudades = $pdo->query("SELECT id, nombre FROM ciudades ORDER BY nombre ASC");
|
|
$ciudades = $stmt_ciudades->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
} catch (PDOException $e) {
|
|
// Handle DB error
|
|
echo '<div class="alert alert-danger" role="alert">Error al conectar con la base de datos: ' . htmlspecialchars($e->getMessage()) . '</div>';
|
|
$productos = [];
|
|
$ciudades = [];
|
|
}
|
|
|
|
?>
|
|
|
|
<div class="container mt-4">
|
|
<div class="row justify-content-center">
|
|
<div class="col-md-8 col-lg-6">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body p-4">
|
|
<h1 class="h3 mb-3 text-center">Registrar Salida</h1>
|
|
<p class="text-muted text-center mb-4">Escanea o selecciona el producto para dar de baja.</p>
|
|
|
|
<?php
|
|
if (isset($_SESSION['error'])):
|
|
?>
|
|
<div class="alert alert-danger" role="alert">
|
|
<?php
|
|
echo htmlspecialchars($_SESSION['error']);
|
|
unset($_SESSION['error']);
|
|
?>
|
|
</div>
|
|
<?php
|
|
endif;
|
|
?>
|
|
<?php
|
|
if (isset($_SESSION['success'])):
|
|
?>
|
|
<div class="alert alert-success" role="alert">
|
|
<?php
|
|
echo htmlspecialchars($_SESSION['success']);
|
|
unset($_SESSION['success']);
|
|
?>
|
|
</div>
|
|
<?php
|
|
endif;
|
|
?>
|
|
|
|
<form action="handle_salida.php" method="POST">
|
|
|
|
<div class="mb-4">
|
|
<label for="scan_barcode" class="form-label"><strong>Escanear Código</strong></label>
|
|
<div class="d-grid gap-2">
|
|
<input type="text" class="form-control" id="scan_barcode" placeholder="Haz clic y escanea..." autofocus>
|
|
<button class="btn btn-outline-primary" type="button" id="btn-scan-camera">
|
|
<i class="fas fa-camera"></i> Escanear con Cámara
|
|
</button>
|
|
</div>
|
|
<div id="reader" style="width: 100%; display:none; margin-top: 10px;"></div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label for="producto_id" class="form-label">Producto</label>
|
|
<select class="form-select" id="producto_id" name="producto_id" required>
|
|
<option value="">Selecciona un producto</option>
|
|
<?php foreach ($productos as $producto): ?>
|
|
<option value="<?php echo htmlspecialchars($producto['id']); ?>" data-barcode="<?php echo htmlspecialchars($producto['codigo_barras'] ?? ''); ?>" data-sku="<?php echo htmlspecialchars($producto['sku']); ?>">
|
|
<?php echo htmlspecialchars($producto['nombre']); ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label for="ciudad_id" class="form-label">Ciudad</label>
|
|
<select class="form-select" id="ciudad_id" name="ciudad_id" required>
|
|
<option value="">Selecciona una ciudad</option>
|
|
<?php foreach ($ciudades as $ciudad): ?>
|
|
<option value="<?php echo htmlspecialchars($ciudad['id']); ?>">
|
|
<?php echo htmlspecialchars($ciudad['nombre']); ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label for="cantidad" class="form-label">Cantidad</label>
|
|
<input type="number" class="form-control" id="cantidad" name="cantidad" min="1" required>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label for="fecha" class="form-label">Fecha de Salida</label>
|
|
<input type="date" class="form-control" id="fecha" name="fecha" required>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label for="descripcion" class="form-label">Descripción (Opcional)</label>
|
|
<textarea class="form-control" id="descripcion" name="descripcion" rows="3" placeholder="Ej: Venta, traslado..."></textarea>
|
|
</div>
|
|
|
|
<div class="d-grid gap-2">
|
|
<button type="submit" class="btn btn-primary btn-lg">Registrar Salida</button>
|
|
<a href="productos.php" class="btn btn-outline-secondary">Cancelar</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Include html5-qrcode library -->
|
|
<script src="https://unpkg.com/html5-qrcode" type="text/javascript"></script>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const barcodeInput = document.getElementById('scan_barcode');
|
|
const productSelect = document.getElementById('producto_id');
|
|
const quantityInput = document.getElementById('cantidad');
|
|
const btnScan = document.getElementById('btn-scan-camera');
|
|
const readerDiv = document.getElementById('reader');
|
|
let html5QrCode = null;
|
|
let isScanning = false;
|
|
let audioCtx = null;
|
|
|
|
function initAudio() {
|
|
if (!audioCtx) {
|
|
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
}
|
|
if (audioCtx.state === 'suspended') {
|
|
audioCtx.resume();
|
|
}
|
|
}
|
|
|
|
// Function to search and select product
|
|
function searchProduct(code) {
|
|
if (code) {
|
|
let found = false;
|
|
for (let i = 0; i < productSelect.options.length; i++) {
|
|
const option = productSelect.options[i];
|
|
const barcode = option.getAttribute('data-barcode');
|
|
const sku = option.getAttribute('data-sku');
|
|
|
|
if (barcode === code || sku === code) {
|
|
productSelect.selectedIndex = i;
|
|
found = true;
|
|
barcodeInput.value = ''; // Clear input
|
|
quantityInput.focus(); // Move to quantity
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
alert('Producto no encontrado con el código: ' + code);
|
|
barcodeInput.value = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Manual input listener
|
|
barcodeInput.addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault(); // Prevent form submission
|
|
searchProduct(barcodeInput.value.trim());
|
|
}
|
|
});
|
|
|
|
// Camera scan listener
|
|
btnScan.addEventListener('click', function() {
|
|
initAudio();
|
|
if (isScanning) {
|
|
stopScanning();
|
|
} else {
|
|
startScanning();
|
|
}
|
|
});
|
|
|
|
function startScanning() {
|
|
readerDiv.style.display = 'block';
|
|
html5QrCode = new Html5Qrcode("reader");
|
|
|
|
const config = { fps: 10, qrbox: { width: 250, height: 250 } };
|
|
|
|
// Prefer back camera
|
|
html5QrCode.start({ facingMode: "environment" }, config, onScanSuccess, onScanFailure)
|
|
.then(() => {
|
|
isScanning = true;
|
|
btnScan.innerHTML = '<i class="fas fa-stop"></i> Detener Cámara';
|
|
btnScan.classList.remove('btn-outline-primary');
|
|
btnScan.classList.add('btn-danger');
|
|
})
|
|
.catch(err => {
|
|
console.error("Error starting scanner", err);
|
|
alert("No se pudo iniciar la cámara. Por favor, verifica los permisos y que estés usando HTTPS.");
|
|
readerDiv.style.display = 'none';
|
|
});
|
|
}
|
|
|
|
function stopScanning() {
|
|
if (html5QrCode) {
|
|
html5QrCode.stop().then(() => {
|
|
readerDiv.style.display = 'none';
|
|
isScanning = false;
|
|
btnScan.innerHTML = '<i class="fas fa-camera"></i> Escanear con Cámara';
|
|
btnScan.classList.remove('btn-danger');
|
|
btnScan.classList.add('btn-outline-primary');
|
|
html5QrCode.clear();
|
|
}).catch(err => {
|
|
console.error("Failed to stop scanner", err);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Professional scanner sound using Web Audio API
|
|
function playScanSound() {
|
|
if (!audioCtx) initAudio();
|
|
const oscillator = audioCtx.createOscillator();
|
|
const gainNode = audioCtx.createGain();
|
|
|
|
oscillator.connect(gainNode);
|
|
gainNode.connect(audioCtx.destination);
|
|
|
|
oscillator.type = 'sine';
|
|
oscillator.frequency.setValueAtTime(1200, audioCtx.currentTime); // 1200Hz beep
|
|
|
|
// Smooth envelope
|
|
gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
|
|
gainNode.gain.linearRampToValueAtTime(0.1, audioCtx.currentTime + 0.01);
|
|
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.15);
|
|
|
|
oscillator.start();
|
|
oscillator.stop(audioCtx.currentTime + 0.15);
|
|
}
|
|
|
|
function onScanSuccess(decodedText, decodedResult) {
|
|
// Handle the scanned code
|
|
console.log(`Code matched = ${decodedText}`, decodedResult);
|
|
|
|
// Play sound
|
|
playScanSound();
|
|
|
|
// Stop scanning after successful read
|
|
stopScanning();
|
|
|
|
// Fill input and search
|
|
barcodeInput.value = decodedText;
|
|
searchProduct(decodedText);
|
|
}
|
|
|
|
function onScanFailure(error) {
|
|
// handle scan failure, usually better to ignore and keep scanning.
|
|
// console.warn(`Code scan error = ${error}`);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<?php
|
|
require_once 'includes/footer.php';
|
|
?>
|