Perfecto, con toda esa informacin tengo el panorama completo. Vamos paso a paso.

---

# ?? Gua completa: Web App de Bodega (PWA)

## Arquitectura final

```
[Dispositivo Android + Lector de barras]
        ? HTTP (WiFi LAN)
[WAMP Server (Apache + PHP)]
        ? PDO Firebird
[Firebird 3.0 (mismo servidor)]
```

---

## PASO 1: Preparar el servidor WAMP

### 1.1 Verificar que PHP tenga el driver Firebird habilitado

Ya lo usas con PDO, pero verifica. Abre `http://localhost/` en el servidor y crea un archivo de prueba:

```php
<?php
// Archivo: C:\wamp64\www\info.php
phpinfo();
```

Abre `http://localhost/info.php` y busca **"Firebird/Interbase"** en la seccin PDO. Debe aparecer. Si no:

1. Abre `php.ini` (el que usa Apache, no el de CLI)
2. Descomenta la lnea: `extension=pdo_firebird`
3. Reinicia WAMP

### 1.2 Verificar que Apache escuche en la red local

Por defecto WAMP solo escucha en `localhost`. Para que los dispositivos Android accedan:

1. Click izquierdo en el cono de WAMP ? **Apache** ? **httpd.conf**
2. Busca `Listen 0.0.0.0:80` (si dice `Listen 127.0.0.1:80`, cmbialo)
3. Busca el bloque `<Directory "c:/wamp64/www/">` y asegrate de que diga:

```apache
<Directory "c:/wamp64/www/">
    Options Indexes FollowSymLinks
    AllowOverride All
    Require all granted
</Directory>
```

4. Reinicia WAMP
5. Desde el Android, prueba acceder a `http://192.168.x.x/info.php` (la IP del servidor)

---

## PASO 2: Crear la estructura del proyecto

Crea esta estructura de carpetas en `C:\wamp64\www\bodega\`:

```
bodega/
+-- index.php              ? Login
+-- menu.php               ? Men principal
+-- entrada.php            ? Pantalla de entradas
+-- salida.php             ? Pantalla de salidas
+-- api/
   +-- config.php         ? Conexin a Firebird
   +-- login.php          ? Autenticacin
   +-- validar_entrada.php
   +-- validar_salida.php
   +-- grabar_movimiento.php
   +-- get_bodegas.php
   +-- get_viajes.php
+-- css/
   +-- app.css            ? Estilos
+-- js/
   +-- app.js             ? Lgica JavaScript
+-- manifest.json          ? Para instalar como PWA
```

---

## PASO 3: Archivo de conexin a Firebird

```php
<?php
// api/config.php

session_start();

// Ruta a la base de datos Firebird (ajusta segn tu caso)
define('FB_HOST', 'localhost');
define('FB_DATABASE', 'C:/ruta/a/tu/base_datos.fdb'); // Ajusta esta ruta
define('FB_CHARSET', 'UTF8');

/**
 * Conecta a Firebird con las credenciales del usuario logueado
 */
function getConnection() {
    if (!isset($_SESSION['fb_user']) || !isset($_SESSION['fb_pass'])) {
        http_response_code(401);
        echo json_encode(['error' => 'No autenticado']);
        exit;
    }
    
    try {
        $dsn = 'firebird:host=' . FB_HOST . ';dbname=' . FB_DATABASE . ';charset=' . FB_CHARSET;
        $pdo = new PDO($dsn, $_SESSION['fb_user'], $_SESSION['fb_pass']);
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        return $pdo;
    } catch (PDOException $e) {
        http_response_code(500);
        echo json_encode(['error' => 'Error de conexin: ' . $e->getMessage()]);
        exit;
    }
}

/**
 * Responde en JSON
 */
function jsonResponse($data, $code = 200) {
    http_response_code($code);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data);
    exit;
}
```

---

## PASO 4: API de Login

<?php
// api/login.php

session_start();
header('Content-Type: application/json; charset=utf-8');

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    echo json_encode(['error' => 'Mtodo no permitido']);
    exit;
}

$input = json_decode(file_get_contents('php://input'), true);
$usuario = strtoupper(trim($input['usuario'] ?? ''));
$clave = $input['clave'] ?? '';

if (empty($usuario) || empty($clave)) {
    echo json_encode(['success' => false, 'mensaje' => 'Usuario y clave son obligatorios']);
    exit;
}

// Ruta a la base de datos - AJUSTA ESTA RUTA
$dbPath = 'C:/ruta/a/tu/base_datos.fdb';

// Intentar conexin a Firebird con las credenciales
try {
    $dsn = 'firebird:host=localhost;dbname=' . $dbPath . ';charset=UTF8';
    $pdo = new PDO($dsn, $usuario, $clave);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    echo json_encode(['success' => false, 'mensaje' => 'Usuario o clave incorrectos']);
    exit;
}

// Conexin exitosa: obtener datos del usuario desde tabla USUARIOS
try {
    $stmt = $pdo->prepare("
        SELECT u.OPER_ID, u.BODC_COD, bc.BODC_NOMBRE, bc.BODC_VIAJE
        FROM USUARIOS u
        LEFT JOIN BODEGA_CONTROL bc ON bc.BODC_COD = u.BODC_COD
        WHERE UPPER(u.USER_OPER) = ?
    ");
    $stmt->execute([$usuario]);
    $userData = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$userData) {
        echo json_encode(['success' => false, 'mensaje' => 'Usuario no encontrado en el sistema']);
        exit;
    }

    // Guardar en sesin
    $_SESSION['fb_user']       = $usuario;
    $_SESSION['fb_pass']       = $clave;
    $_SESSION['oper_id']       = $userData['OPER_ID'];
    $_SESSION['bodega_cod']    = $userData['BODC_COD'];
    $_SESSION['bodega_nombre'] = $userData['BODC_NOMBRE'];
    $_SESSION['bodega_viaje']  = $userData['BODC_VIAJE'];

    $response = [
        'success'      => true,
        'oper_id'      => $userData['OPER_ID'],
        'tiene_bodega' => !empty($userData['BODC_COD']),
        'bodega_cod'   => $userData['BODC_COD'],
        'bodega_nombre'=> $userData['BODC_NOMBRE']
    ];

    // Si no tiene bodega asignada, enviar lista de bodegas disponibles
    if (empty($userData['BODC_COD'])) {
        $stmtBod = $pdo->query("
            SELECT BODC_COD, BODC_NOMBRE
            FROM BODEGA_CONTROL
            ORDER BY BODC_NOMBRE
        ");
        $response['bodegas'] = $stmtBod->fetchAll(PDO::FETCH_ASSOC);
    }

    echo json_encode($response);

} catch (PDOException $e) {
    echo json_encode(['success' => false, 'mensaje' => 'Error consultando datos: ' . $e->getMessage()]);
}
---

## PASO 5: API para seleccionar bodega (usuarios sin bodega fija)

```php
<?php
// api/seleccionar_bodega.php

require_once 'config.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    jsonResponse(['error' => 'Mtodo no permitido'], 405);
}

$input = json_decode(file_get_contents('php://input'), true);
$bodegaCod = intval($input['bodega_cod'] ?? 0);

if ($bodegaCod <= 0) {
    jsonResponse(['success' => false, 'mensaje' => 'Bodega no vlida']);
}

$pdo = getConnection();

$stmt = $pdo->prepare("
    SELECT BODC_COD, BODC_NOMBRE, BODC_VIAJE 
    FROM BODEGA_CONTROL 
    WHERE BODC_COD = ?
");
$stmt->execute([$bodegaCod]);
$bodega = $stmt->fetch(PDO::FETCH_ASSOC);

if (!$bodega) {
    jsonResponse(['success' => false, 'mensaje' => 'Bodega no encontrada']);
}

$_SESSION['bodega_cod'] = $bodega['BODC_COD'];
$_SESSION['bodega_nombre'] = $bodega['BODC_NOMBRE'];
$_SESSION['bodega_viaje'] = $bodega['BODC_VIAJE'];

jsonResponse([
    'success' => true,
    'bodega_cod' => $bodega['BODC_COD'],
    'bodega_nombre' => $bodega['BODC_NOMBRE']
]);
```

---

## PASO 6: API de validacin y grabacin

### Validar entrada

```php
<?php
// api/validar_entrada.php

require_once 'config.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    jsonResponse(['error' => 'Mtodo no permitido'], 405);
}

$input = json_decode(file_get_contents('php://input'), true);
$codigo = trim($input['codigo'] ?? '');
$bodega = intval($_SESSION['bodega_cod'] ?? 0);

if (empty($codigo) || $bodega <= 0) {
    jsonResponse(['success' => false, 'mensaje' => 'Cdigo o bodega no vlidos']);
}

// Validar que sea numrico
if (!ctype_digit($codigo)) {
    jsonResponse(['success' => false, 'estado' => 0, 'mensaje' => 'El cdigo debe ser numrico']);
}

$pdo = getConnection();

try {
    $stmt = $pdo->prepare("SELECT ESTADO FROM VALIDA_ENTRADA(?, ?)");
    $stmt->execute([$codigo, $bodega]);
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    $estado = intval($row['ESTADO']);
    
    $mensajes = [
        1 => 'OK',
        2 => 'Artculo no existe',
        3 => 'Artculo no existe en la bodega anterior'
    ];
    
    jsonResponse([
        'success' => ($estado === 1),
        'estado' => $estado,
        'mensaje' => $mensajes[$estado] ?? 'Estado desconocido'
    ]);
    
} catch (PDOException $e) {
    jsonResponse(['success' => false, 'mensaje' => 'Error: ' . $e->getMessage()], 500);
}
```

### Validar salida

```php
<?php
// api/validar_salida.php

require_once 'config.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    jsonResponse(['error' => 'Mtodo no permitido'], 405);
}

$input = json_decode(file_get_contents('php://input'), true);
$codigo = trim($input['codigo'] ?? '');
$bodega = intval($_SESSION['bodega_cod'] ?? 0);

if (empty($codigo) || $bodega <= 0) {
    jsonResponse(['success' => false, 'mensaje' => 'Cdigo o bodega no vlidos']);
}

if (!ctype_digit($codigo)) {
    jsonResponse(['success' => false, 'estado' => 0, 'mensaje' => 'El cdigo debe ser numrico']);
}

$pdo = getConnection();

try {
    $stmt = $pdo->prepare("SELECT ESTADO FROM VALIDA_SALIDA(?, ?)");
    $stmt->execute([$codigo, $bodega]);
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    $estado = intval($row['ESTADO']);
    
    $mensajes = [
        1 => 'OK',
        2 => 'Artculo no existe',
        3 => 'Artculo no existe en esta bodega'
    ];
    
    jsonResponse([
        'success' => ($estado === 1),
        'estado' => $estado,
        'mensaje' => $mensajes[$estado] ?? 'Estado desconocido'
    ]);
    
} catch (PDOException $e) {
    jsonResponse(['success' => false, 'mensaje' => 'Error: ' . $e->getMessage()], 500);
}
```

### Grabar movimiento

```php
<?php
// api/grabar_movimiento.php

require_once 'config.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    jsonResponse(['error' => 'Mtodo no permitido'], 405);
}

$input = json_decode(file_get_contents('php://input'), true);
$codigo = trim($input['codigo'] ?? '');
$entrada = strtoupper(trim($input['entrada'] ?? ''));  // 'S' o 'N'
$viaje = isset($input['viaje']) ? intval($input['viaje']) : null;

$operId = intval($_SESSION['oper_id'] ?? 0);
$bodega = intval($_SESSION['bodega_cod'] ?? 0);

// Validaciones bsicas
if (empty($codigo) || $bodega <= 0 || $operId <= 0) {
    jsonResponse(['success' => false, 'mensaje' => 'Datos incompletos']);
}

if (!in_array($entrada, ['S', 'N'])) {
    jsonResponse(['success' => false, 'mensaje' => 'Tipo de movimiento no vlido']);
}

if (!ctype_digit($codigo)) {
    jsonResponse(['success' => false, 'mensaje' => 'El cdigo debe ser numrico']);
}

$pdo = getConnection();

try {
    $sql = "INSERT INTO MOVIMIENTO_INVENTARIO 
            (OPER_ID, BODC_COD, MVIN_CODIGO, MVIN_ENTRADA, MVIN_VIAJE) 
            VALUES (?, ?, ?, ?, ?)";
    
    $stmt = $pdo->prepare($sql);
    $stmt->execute([$operId, $bodega, $codigo, $entrada, $viaje]);
    
    jsonResponse([
        'success' => true,
        'mensaje' => ($entrada === 'S') ? 'Entrada registrada' : 'Salida registrada'
    ]);
    
} catch (PDOException $e) {
    jsonResponse(['success' => false, 'mensaje' => 'Error al grabar: ' . $e->getMessage()], 500);
}
```

---

## PASO 7: API para obtener viajes disponibles

<?php
// api/get_viajes.php

require_once 'config.php';

if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
    jsonResponse(['error' => 'Mtodo no permitido'], 405);
}

$pdo = getConnection();

try {
    $stmt = $pdo->query("
        SELECT VIDE_ID, VIDE_VEHICULO, VIDE_CONDUCTOR, VIDE_FECHA
        FROM VIAJES_DESPACHO
        WHERE VIDE_ABIERTO = 1
        ORDER BY VIDE_FECHA DESC, VIDE_ID DESC
    ");
    $viajes = $stmt->fetchAll(PDO::FETCH_ASSOC);

    jsonResponse(['success' => true, 'viajes' => $viajes]);

} catch (PDOException $e) {
    jsonResponse(['success' => false, 'mensaje' => 'Error: ' . $e->getMessage()], 500);
}

---

## PASO 8: La pantalla de Login (`index.php`)

```php
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="mobile-web-app-capable" content="yes">
    <title>Bodega SYSplus</title>
    <link rel="manifest" href="manifest.json">
    <link rel="icon" type="image/png" href="icon-192.png">
    <link rel="stylesheet" href="css/app.css">
</head>
<body>
    <div class="container">
        <div class="login-box">
            <h1>?? Bodega</h1>
            <h2>SYSplus</h2>
            
            <form id="loginForm">
                <div class="form-group">
                    <label for="usuario">Usuario</label>
                    <input type="text" id="usuario" name="usuario" 
                           autocomplete="username" autocapitalize="characters" required>
                </div>
                <div class="form-group">
                    <label for="clave">Contrasea</label>
                    <input type="password" id="clave" name="clave" 
                           autocomplete="current-password" required>
                </div>
                <button type="submit" class="btn btn-primary btn-block">Ingresar</button>
            </form>
            
            <div id="loginError" class="alert alert-error" style="display:none;"></div>
        </div>
        
        <!-- Modal para seleccionar bodega (usuarios sin bodega fija) -->
        <div id="modalBodega" class="modal" style="display:none;">
            <div class="modal-content">
                <h3>Seleccione Bodega</h3>
                <select id="selectBodega" class="form-control"></select>
                <button id="btnSelBodega" class="btn btn-primary btn-block" style="margin-top:15px;">
                    Continuar
                </button>
            </div>
        </div>
    </div>

    <script>
    document.getElementById('loginForm').addEventListener('submit', async function(e) {
        e.preventDefault();
        const errorDiv = document.getElementById('loginError');
        errorDiv.style.display = 'none';
        
        const usuario = document.getElementById('usuario').value.trim();
        const clave = document.getElementById('clave').value;
        
        try {
            const resp = await fetch('api/login.php', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({usuario, clave})
            });
            const data = await resp.json();
            
            if (data.success) {
                if (data.tiene_bodega) {
                    // Tiene bodega asignada, ir al men
                    window.location.href = 'menu.php';
                } else {
                    // Mostrar selector de bodega
                    const select = document.getElementById('selectBodega');
                    select.innerHTML = '<option value="">-- Seleccione --</option>';
                    data.bodegas.forEach(b => {
                        select.innerHTML += `<option value="${b.BODC_COD}">${b.BODC_NOMBRE}</option>`;
                    });
                    document.getElementById('modalBodega').style.display = 'flex';
                }
            } else {
                errorDiv.textContent = data.mensaje;
                errorDiv.style.display = 'block';
            }
        } catch (err) {
            errorDiv.textContent = 'Error de conexin con el servidor';
            errorDiv.style.display = 'block';
        }
    });
    
    document.getElementById('btnSelBodega').addEventListener('click', async function() {
        const bodegaCod = document.getElementById('selectBodega').value;
        if (!bodegaCod) { alert('Seleccione una bodega'); return; }
        
        const resp = await fetch('api/seleccionar_bodega.php', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({bodega_cod: parseInt(bodegaCod)})
        });
        const data = await resp.json();
        
        if (data.success) {
            window.location.href = 'menu.php';
        } else {
            alert(data.mensaje);
        }
    });
    </script>
</body>
</html>
```

---

## PASO 9: Men principal (`menu.php`)

```php
<?php
session_start();
if (!isset($_SESSION['fb_user'])) { header('Location: index.php'); exit; }
?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>Men - Bodega</title>
    <link rel="stylesheet" href="css/app.css">
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>?? Bodega</h1>
            <p class="subtitle">
                <?= htmlspecialchars($_SESSION['fb_user']) ?>  
                <?= htmlspecialchars($_SESSION['bodega_nombre'] ?? 'Sin bodega') ?>
            </p>
        </div>
        
        <div class="menu-grid">
            <a href="entrada.php" class="menu-card entrada">
                <div class="menu-icon">??</div>
                <div class="menu-title">Entrada</div>
                <div class="menu-desc">Ingreso de artculos a bodega</div>
            </a>
            
            <a href="salida.php" class="menu-card salida">
                <div class="menu-icon">??</div>
                <div class="menu-title">Salida</div>
                <div class="menu-desc">Despacho de artculos de bodega</div>
            </a>
        </div>
        
        <div style="text-align:center; margin-top:30px;">
            <a href="api/logout.php" class="btn btn-secondary">Cerrar Sesin</a>
        </div>
    </div>
</body>
</html>
```

Crea tambin el logout:

```php
<?php
// api/logout.php
session_start();
session_destroy();
header('Location: ../index.php');
exit;
```

---

## PASO 10: Pantalla de Entradas (`entrada.php`)

```php
<?php
session_start();
if (!isset($_SESSION['fb_user'])) { header('Location: index.php'); exit; }
$bodega = $_SESSION['bodega_cod'] ?? 0;
$bodegaNombre = $_SESSION['bodega_nombre'] ?? '';
?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>Entrada - Bodega</title>
    <link rel="stylesheet" href="css/app.css">
</head>
<body>
    <div class="container">
        <div class="header header-entrada">
            <div class="header-top">
                <a href="menu.php" class="btn-back">? Men</a>
                <h1>?? Entrada</h1>
            </div>
            <p class="subtitle"><?= htmlspecialchars($bodegaNombre) ?></p>
        </div>
        
        <!-- Campo de lectura de cdigo -->
        <div class="scan-area">
            <label for="inputCodigo">Escanee o digite el cdigo:</label>
            <input type="text" id="inputCodigo" inputmode="numeric" pattern="[0-9]*"
                   autofocus autocomplete="off" placeholder="Cdigo del artculo">
        </div>
        
        <!-- ltimo resultado -->
        <div id="resultado" class="resultado" style="display:none;">
            <div id="resultIcon" class="result-icon"></div>
            <div id="resultMsg" class="result-msg"></div>
        </div>
        
        <!-- Contador de sesin -->
        <div class="contador">
            Artculos ingresados: <span id="contador">0</span>
        </div>
        
        <!-- Historial reciente -->
        <div class="historial">
            <h3>ltimos movimientos</h3>
            <ul id="historial"></ul>
        </div>
    </div>

    <script>
    const TIPO = 'S'; // S = Entrada
    let contador = 0;
    const input = document.getElementById('inputCodigo');
    
    // Mantener el foco siempre en el input (para el lector de barras)
    input.addEventListener('blur', () => setTimeout(() => input.focus(), 100));
    
    // Al presionar Enter (el lector de barras enva Enter al final)
    input.addEventListener('keydown', function(e) {
        if (e.key === 'Enter') {
            e.preventDefault();
            procesarCodigo(this.value.trim());
            this.value = '';
        }
    });
    
    async function procesarCodigo(codigo) {
        if (!codigo) return;
        
        // Validar que sea numrico
        if (!/^\d+$/.test(codigo)) {
            mostrarResultado(false, 'El cdigo debe ser numrico');
            return;
        }
        
        // 1. Validar
        try {
            const respVal = await fetch('api/validar_entrada.php', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({codigo})
            });
            const dataVal = await respVal.json();
            
            if (!dataVal.success) {
                mostrarResultado(false, dataVal.mensaje);
                agregarHistorial(codigo, false, dataVal.mensaje);
                return;
            }
            
            // 2. Grabar
            const respGrab = await fetch('api/grabar_movimiento.php', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({codigo, entrada: TIPO, viaje: null})
            });
            const dataGrab = await respGrab.json();
            
            if (dataGrab.success) {
                contador++;
                document.getElementById('contador').textContent = contador;
                mostrarResultado(true, `? ${codigo} ingresado`);
                agregarHistorial(codigo, true, 'Ingresado');
            } else {
                mostrarResultado(false, dataGrab.mensaje);
                agregarHistorial(codigo, false, dataGrab.mensaje);
            }
            
        } catch (err) {
            mostrarResultado(false, 'Error de conexin');
        }
    }
    
    function mostrarResultado(ok, msg) {
        const div = document.getElementById('resultado');
        div.style.display = 'flex';
        div.className = 'resultado ' + (ok ? 'resultado-ok' : 'resultado-error');
        document.getElementById('resultIcon').textContent = ok ? '?' : '?';
        document.getElementById('resultMsg').textContent = msg;
        
        // Vibrar en error
        if (!ok && navigator.vibrate) navigator.vibrate(300);
    }
    
    function agregarHistorial(codigo, ok, msg) {
        const ul = document.getElementById('historial');
        const li = document.createElement('li');
        li.className = ok ? 'hist-ok' : 'hist-error';
        const hora = new Date().toLocaleTimeString('es-CO', {hour:'2-digit', minute:'2-digit', second:'2-digit'});
        li.innerHTML = `<span class="hist-hora">${hora}</span> 
                        <span class="hist-codigo">${codigo}</span> 
                        <span class="hist-msg">${msg}</span>`;
        ul.insertBefore(li, ul.firstChild);
        
        // Mantener mximo 20 en pantalla
        while (ul.children.length > 20) ul.removeChild(ul.lastChild);
    }
    
    // Foco inicial
    input.focus();
    </script>
</body>
</html>
```

---

## PASO 11: Pantalla de Salidas (`salida.php`)
<?php
session_start();
if (!isset($_SESSION['fb_user'])) { header('Location: index.php'); exit; }
$bodega = $_SESSION['bodega_cod'] ?? 0;
$bodegaNombre = $_SESSION['bodega_nombre'] ?? '';
$requiereViaje = ($_SESSION['bodega_viaje'] ?? 0) == 1;
?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>Salida - Bodega</title>
    <link rel="stylesheet" href="css/app.css">
</head>
<body>
    <div class="container">
        <div class="header header-salida">
            <div class="header-top">
                <a href="menu.php" class="btn-back">? Men</a>
                <h1>?? Salida</h1>
            </div>
            <p class="subtitle"><?= htmlspecialchars($bodegaNombre) ?></p>
        </div>
        
        <?php if ($requiereViaje): ?>
        <!-- Selector de viaje (se muestra primero) -->
        <div id="panelViaje">
            <div class="viaje-box">
                <h3>?? Seleccione el Viaje</h3>
                <select id="selectViaje" class="form-control">
                    <option value="">Cargando viajes...</option>
                </select>
                <div id="viajeInfo" class="viaje-info" style="display:none;"></div>
                <button id="btnIniciarViaje" class="btn btn-primary btn-block" 
                        style="margin-top:15px;" disabled>
                    Iniciar Despacho
                </button>
            </div>
        </div>
        <?php endif; ?>
        
        <!-- Panel de escaneo (oculto si requiere viaje hasta seleccionarlo) -->
        <div id="panelScan" <?= $requiereViaje ? 'style="display:none;"' : '' ?>>
            
            <?php if ($requiereViaje): ?>
            <div id="viajeActivo" class="viaje-activo">
                ?? Viaje: <span id="viajeActivoText"></span>
                <button id="btnCambiarViaje" class="btn-link">Cambiar</button>
            </div>
            <?php endif; ?>
            
            <!-- Campo de lectura de cdigo -->
            <div class="scan-area">
                <label for="inputCodigo">Escanee o digite el cdigo:</label>
                <input type="text" id="inputCodigo" inputmode="numeric" pattern="[0-9]*"
                       autofocus autocomplete="off" placeholder="Cdigo del artculo">
            </div>
            
            <!-- ltimo resultado -->
            <div id="resultado" class="resultado" style="display:none;">
                <div id="resultIcon" class="result-icon"></div>
                <div id="resultMsg" class="result-msg"></div>
            </div>
            
            <!-- Contador de sesin -->
            <div class="contador">
                Artculos despachados: <span id="contador">0</span>
            </div>
            
            <!-- Historial reciente -->
            <div class="historial">
                <h3>ltimos movimientos</h3>
                <ul id="historial"></ul>
            </div>
        </div>
    </div>

    <script>
    const TIPO = 'N'; // N = Salida
    const REQUIERE_VIAJE = <?= $requiereViaje ? 'true' : 'false' ?>;
    let contador = 0;
    let viajeSeleccionado = null;
    
    // =============================================
    // LGICA DE VIAJE
    // =============================================
    if (REQUIERE_VIAJE) {
        cargarViajes();
        
        document.getElementById('selectViaje').addEventListener('change', function() {
            const btn = document.getElementById('btnIniciarViaje');
            const info = document.getElementById('viajeInfo');
            
            if (this.value) {
                btn.disabled = false;
                // Mostrar info del viaje seleccionado
                const opt = this.options[this.selectedIndex];
                info.innerHTML = opt.dataset.info || '';
                info.style.display = 'block';
            } else {
                btn.disabled = true;
                info.style.display = 'none';
            }
        });
        
        document.getElementById('btnIniciarViaje').addEventListener('click', function() {
            const select = document.getElementById('selectViaje');
            viajeSeleccionado = parseInt(select.value);
            const textoViaje = select.options[select.selectedIndex].text;
            
            // Ocultar panel de viaje, mostrar panel de escaneo
            document.getElementById('panelViaje').style.display = 'none';
            document.getElementById('panelScan').style.display = 'block';
            document.getElementById('viajeActivoText').textContent = textoViaje;
            
            // Enfocar el input
            const input = document.getElementById('inputCodigo');
            input.focus();
        });
        
        document.getElementById('btnCambiarViaje').addEventListener('click', function() {
            document.getElementById('panelViaje').style.display = 'block';
            document.getElementById('panelScan').style.display = 'none';
            viajeSeleccionado = null;
        });
    }
    
    async function cargarViajes() {
    try {
        const resp = await fetch('api/get_viajes.php');
        const data = await resp.json();
        const select = document.getElementById('selectViaje');

        select.innerHTML = '<option value="">-- Seleccione un viaje --</option>';

        if (data.success && data.viajes.length > 0) {
            data.viajes.forEach(v => {
                const opt = document.createElement('option');
                opt.value = v.VIDE_ID;
                opt.textContent = 'Viaje ' + v.VIDE_ID + ' - ' + v.VIDE_VEHICULO + ' - ' + v.VIDE_FECHA;
                opt.dataset.info = 'Conductor: ' + v.VIDE_CONDUCTOR + ' | Vehculo: ' + v.VIDE_VEHICULO;
                select.appendChild(opt);
            });
        } else {
            select.innerHTML = '<option value="">No hay viajes abiertos</option>';
        }
    } catch (err) {
        console.error('Error cargando viajes:', err);
    }
}

    // =============================================
    // LGICA DE ESCANEO
    // =============================================
    const input = document.getElementById('inputCodigo');
    
    // Mantener el foco siempre en el input (para el lector de barras)
    input.addEventListener('blur', () => setTimeout(() => input.focus(), 100));
    
    // Al presionar Enter (el lector de barras enva Enter al final)
    input.addEventListener('keydown', function(e) {
        if (e.key === 'Enter') {
            e.preventDefault();
            procesarCodigo(this.value.trim());
            this.value = '';
        }
    });
    
    async function procesarCodigo(codigo) {
        if (!codigo) return;
        
        // Validar que sea numrico
        if (!/^\d+$/.test(codigo)) {
            mostrarResultado(false, 'El cdigo debe ser numrico');
            return;
        }
        
        // Si requiere viaje y no se ha seleccionado
        if (REQUIERE_VIAJE && !viajeSeleccionado) {
            mostrarResultado(false, 'Debe seleccionar un viaje primero');
            return;
        }
        
        try {
            // 1. Validar salida
            const respVal = await fetch('api/validar_salida.php', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({codigo})
            });
            const dataVal = await respVal.json();
            
            if (!dataVal.success) {
                mostrarResultado(false, dataVal.mensaje);
                agregarHistorial(codigo, false, dataVal.mensaje);
                return;
            }
            
            // 2. Grabar movimiento
            const respGrab = await fetch('api/grabar_movimiento.php', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({
                    codigo: codigo, 
                    entrada: TIPO, 
                    viaje: viajeSeleccionado
                })
            });
            const dataGrab = await respGrab.json();
            
            if (dataGrab.success) {
                contador++;
                document.getElementById('contador').textContent = contador;
                mostrarResultado(true, '? ' + codigo + ' despachado');
                agregarHistorial(codigo, true, 'Despachado');
            } else {
                mostrarResultado(false, dataGrab.mensaje);
                agregarHistorial(codigo, false, dataGrab.mensaje);
            }
            
        } catch (err) {
            mostrarResultado(false, 'Error de conexin');
        }
    }
    
    function mostrarResultado(ok, msg) {
        const div = document.getElementById('resultado');
        div.style.display = 'flex';
        div.className = 'resultado ' + (ok ? 'resultado-ok' : 'resultado-error');
        document.getElementById('resultIcon').textContent = ok ? '?' : '?';
        document.getElementById('resultMsg').textContent = msg;
        
        // Vibrar en error
        if (!ok && navigator.vibrate) navigator.vibrate(300);
    }
    
    function agregarHistorial(codigo, ok, msg) {
        const ul = document.getElementById('historial');
        const li = document.createElement('li');
        li.className = ok ? 'hist-ok' : 'hist-error';
        const hora = new Date().toLocaleTimeString('es-CO', {
            hour:'2-digit', minute:'2-digit', second:'2-digit'
        });
        li.innerHTML = '<span class="hist-hora">' + hora + '</span> ' +
                        '<span class="hist-codigo">' + codigo + '</span> ' +
                        '<span class="hist-msg">' + msg + '</span>';
        ul.insertBefore(li, ul.firstChild);
        
        // Mantener mximo 20 en pantalla
        while (ul.children.length > 20) ul.removeChild(ul.lastChild);
    }
    
    // Foco inicial (si no requiere viaje)
    if (!REQUIERE_VIAJE) input.focus();
    </script>
</body>
</html>

PASO 12: Los estilos CSS (css/app.css)
/* css/app.css */

/* =============================================
   RESET Y BASE
   ============================================= */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background: #f0f2f5;
    color: #333;
    font-size: 16px;
    min-height: 100vh;
}

.container {
    max-width: 480px;
    margin: 0 auto;
    padding: 15px;
    min-height: 100vh;
}

/* =============================================
   FORMULARIOS
   ============================================= */
.form-group {
    margin-bottom: 15px;
}

.form-group label {
    display: block;
    font-weight: 600;
    margin-bottom: 5px;
    color: #555;
}

input[type="text"],
input[type="password"],
select {
    width: 100%;
    padding: 14px 12px;
    border: 2px solid #ddd;
    border-radius: 10px;
    font-size: 18px;
    outline: none;
    transition: border-color 0.2s;
    background: #fff;
}

input[type="text"]:focus,
input[type="password"]:focus,
select:focus {
    border-color: #4a90d9;
}

.form-control {
    width: 100%;
    padding: 14px 12px;
    border: 2px solid #ddd;
    border-radius: 10px;
    font-size: 16px;
    background: #fff;
}

/* =============================================
   BOTONES
   ============================================= */
.btn {
    display: inline-block;
    padding: 14px 24px;
    border: none;
    border-radius: 10px;
    font-size: 17px;
    font-weight: 600;
    cursor: pointer;
    text-decoration: none;
    text-align: center;
    transition: background 0.2s, transform 0.1s;
}

.btn:active {
    transform: scale(0.97);
}

.btn-primary {
    background: #4a90d9;
    color: #fff;
}

.btn-primary:hover {
    background: #3a7bc8;
}

.btn-primary:disabled {
    background: #a0c0e8;
    cursor: not-allowed;
}

.btn-secondary {
    background: #e0e0e0;
    color: #555;
}

.btn-block {
    display: block;
    width: 100%;
}

.btn-back {
    text-decoration: none;
    color: #fff;
    font-size: 16px;
    font-weight: 500;
    opacity: 0.9;
}

.btn-link {
    background: none;
    border: none;
    color: #fff;
    text-decoration: underline;
    cursor: pointer;
    font-size: 14px;
    padding: 0;
    margin-left: 10px;
}

/* =============================================
   LOGIN
   ============================================= */
.login-box {
    background: #fff;
    padding: 30px 25px;
    border-radius: 16px;
    box-shadow: 0 4px 20px rgba(0,0,0,0.08);
    margin-top: 60px;
}

.login-box h1 {
    text-align: center;
    font-size: 32px;
    margin-bottom: 0;
}

.login-box h2 {
    text-align: center;
    font-size: 16px;
    color: #888;
    font-weight: 400;
    margin-bottom: 30px;
}

/* =============================================
   ALERTAS
   ============================================= */
.alert {
    padding: 12px 16px;
    border-radius: 10px;
    margin-top: 15px;
    font-size: 15px;
}

.alert-error {
    background: #ffe0e0;
    color: #c0392b;
    border: 1px solid #f5c6cb;
}

/* =============================================
   MODAL
   ============================================= */
.modal {
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    background: rgba(0,0,0,0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 100;
    padding: 20px;
}

.modal-content {
    background: #fff;
    padding: 25px;
    border-radius: 16px;
    width: 100%;
    max-width: 400px;
    box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}

.modal-content h3 {
    margin-bottom: 15px;
    text-align: center;
}

/* =============================================
   HEADER
   ============================================= */
.header {
    background: #4a90d9;
    color: #fff;
    padding: 20px;
    border-radius: 16px;
    margin-bottom: 20px;
}

.header-entrada {
    background: linear-gradient(135deg, #27ae60, #2ecc71);
}

.header-salida {
    background: linear-gradient(135deg, #e67e22, #f39c12);
}

.header-top {
    display: flex;
    align-items: center;
    gap: 15px;
    margin-bottom: 5px;
}

.header h1 {
    font-size: 22px;
    flex: 1;
}

.subtitle {
    font-size: 14px;
    opacity: 0.9;
}

/* =============================================
   MEN PRINCIPAL
   ============================================= */
.menu-grid {
    display: flex;
    flex-direction: column;
    gap: 15px;
    margin-top: 20px;
}

.menu-card {
    display: block;
    padding: 25px 20px;
    border-radius: 16px;
    text-decoration: none;
    color: #fff;
    transition: transform 0.15s;
    box-shadow: 0 4px 15px rgba(0,0,0,0.15);
}

.menu-card:active {
    transform: scale(0.97);
}

.menu-card.entrada {
    background: linear-gradient(135deg, #27ae60, #2ecc71);
}

.menu-card.salida {
    background: linear-gradient(135deg, #e67e22, #f39c12);
}

.menu-icon {
    font-size: 40px;
    margin-bottom: 10px;
}

.menu-title {
    font-size: 22px;
    font-weight: 700;
}

.menu-desc {
    font-size: 14px;
    opacity: 0.9;
    margin-top: 4px;
}

/* =============================================
   ZONA DE ESCANEO
   ============================================= */
.scan-area {
    background: #fff;
    padding: 20px;
    border-radius: 16px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.06);
    margin-bottom: 15px;
}

.scan-area label {
    display: block;
    font-weight: 600;
    margin-bottom: 10px;
    color: #555;
}

.scan-area input {
    font-size: 24px;
    padding: 16px 14px;
    text-align: center;
    font-weight: 700;
    letter-spacing: 2px;
}

/* =============================================
   RESULTADO
   ============================================= */
.resultado {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 18px 20px;
    border-radius: 16px;
    margin-bottom: 15px;
    animation: slideIn 0.3s ease;
}

@keyframes slideIn {
    from { opacity: 0; transform: translateY(-10px); }
    to   { opacity: 1; transform: translateY(0); }
}

.resultado-ok {
    background: #d4edda;
    border: 2px solid #28a745;
}

.resultado-error {
    background: #ffe0e0;
    border: 2px solid #dc3545;
}

.result-icon {
    font-size: 32px;
}

.result-msg {
    font-size: 17px;
    font-weight: 600;
}

/* =============================================
   CONTADOR
   ============================================= */
.contador {
    text-align: center;
    font-size: 18px;
    color: #666;
    padding: 15px;
    background: #fff;
    border-radius: 16px;
    margin-bottom: 15px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.06);
}

.contador span {
    font-size: 28px;
    font-weight: 800;
    color: #4a90d9;
}

/* =============================================
   HISTORIAL
   ============================================= */
.historial {
    background: #fff;
    padding: 15px 20px;
    border-radius: 16px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.06);
}

.historial h3 {
    font-size: 15px;
    color: #888;
    margin-bottom: 10px;
}

.historial ul {
    list-style: none;
}

.historial li {
    padding: 8px 0;
    border-bottom: 1px solid #f0f0f0;
    font-size: 14px;
    display: flex;
    gap: 10px;
    align-items: center;
}

.historial li:last-child {
    border-bottom: none;
}

.hist-ok {
    color: #27ae60;
}

.hist-error {
    color: #e74c3c;
}

.hist-hora {
    color: #aaa;
    font-size: 12px;
    min-width: 65px;
}

.hist-codigo {
    font-weight: 700;
    min-width: 80px;
}

.hist-msg {
    flex: 1;
}

/* =============================================
   VIAJE
   ============================================= */
.viaje-box {
    background: #fff;
    padding: 25px 20px;
    border-radius: 16px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.06);
}

.viaje-box h3 {
    text-align: center;
    margin-bottom: 15px;
}

.viaje-info {
    background: #f8f9fa;
    padding: 12px;
    border-radius: 8px;
    font-size: 14px;
    color: #555;
    margin-top: 10px;
    text-align: center;
}

.viaje-activo {
    background: #fff3cd;
    color: #856404;
    padding: 12px 16px;
    border-radius: 10px;
    margin-bottom: 15px;
    font-weight: 600;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 5px;
}

.viaje-activo .btn-link {
    color: #856404;
}

PASO 13: Manifiesto PWA (manifest.json)
Esto permite "instalar" la web app con cono en la pantalla de inicio del Android:
{
    "name": "Bodega SYSplus",
    "short_name": "Bodega",
    "description": "Control de entradas y salidas de bodega",
    "start_url": "/bodega/index.php",
    "display": "standalone",
    "orientation": "portrait",
    "background_color": "#f0f2f5",
    "theme_color": "#4a90d9",
    "icons": [
        {
            "src": "icon-192.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        {
            "src": "icon-512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ]
}
Crear los conos
Necesitas dos imgenes PNG cuadradas (puedes usar cualquier generador online):

icon-192.png ? 192192 pxeles
icon-512.png ? 512512 pxeles
Puedes generar uno rpido en favicon.io buscando el emoji ??.
PASO 14: Instalar como app en Android
Desde el dispositivo Android:

Abre Chrome
Navega a http://192.168.x.x/bodega/ (la IP de tu servidor)
Toca el men (?) de Chrome
Selecciona "Agregar a pantalla de inicio" o "Instalar app"
Confirma ? aparece el cono en el escritorio
Al abrirla se ve sin barra de Chrome, como una app nativa
PASO 15: Configuracin del lector de cdigo de barras
La mayora de lectores de cdigos integrados en dispositivos Android funcionan en modo teclado (keyboard wedge): el cdigo escaneado se "escribe" en el campo de texto enfocado y enva un Enter al final. Por eso el diseo ya funciona as:

El input siempre tiene el foco (se re-enfoca automticamente si pierde el foco)
Al detectar Enter, procesa el cdigo y limpia el campo
Queda listo para el siguiente escaneo
Verifica en la configuracin del lector de tu dispositivo:

Modo: Keyboard Wedge / Input Method
Sufijo: Enter (CR o LF) ? esto es lo ms importante
Codificacin: UTF-8
PASO 16: Verificacin y pruebas
Lista de chequeo antes de probar:
#	Verificacin	Cmo
1	PHP tiene pdo_firebird	phpinfo()
2	WAMP escucha en la red	Acceder desde otro PC a http://IP_SERVIDOR/bodega/
3	Ruta de la BD en config.php	Debe coincidir con la ruta real del .fdb
4	Campos de las tablas	Ajustar los nombres de columnas en las consultas SQL
5	Parmetros de los SP	Verificar que VALIDA_ENTRADA y VALIDA_SALIDA reciben los parmetros en el orden correcto
6	Android llega al servidor	Conectar a misma WiFi, probar http://IP/bodega/
7	Lector de barras	Verificar que enva Enter como sufijo
Orden de pruebas recomendado:
Desde el PC del servidor: abre http://localhost/bodega/ y prueba el login
Desde otro PC en red: abre http://192.168.x.x/bodega/
Desde el Android (sin lector): prueba escribiendo cdigos a mano
Desde el Android (con lector): prueba escaneando
Resumen de la arquitectura final
unknown
Copy
+----------------------------------+
    Android (Chrome / PWA)        
  +----------------------------+  
    index.php     ? Login       
    menu.php      ? Men        
    entrada.php   ? Entradas    
    salida.php    ? Salidas     
  +----------------------------+  
+---------------------------------+
                 HTTP (WiFi LAN)
+---------------?------------------+
    WAMP Server                   
  +----------------------------+  
    api/login.php               
    api/validar_entrada.php     
    api/validar_salida.php      
    api/grabar_movimiento.php   
    api/get_viajes.php          
  +----------------------------+  
                PDO Firebird     
  +------------?---------------+  
    Firebird 3.0                
    +- SP VALIDA_ENTRADA        
    +- SP VALIDA_SALIDA         
    +- MOVIMIENTO_INVENTARIO    
    +- BODEGA_CONTROL           
    +- VIAJES_DESPACHO          
    +- USUARIOS                 
  +----------------------------+  
+----------------------------------+
