# Arquitectura Offline-First — POS Retail

> Documentación técnica del sistema de sincronización y operación offline del módulo POS Retail de Automia.

---

## Índice

1. [Visión general](#1-visión-general)
2. [Arquitectura del sistema](#2-arquitectura-del-sistema)
3. [Backend — Módulo de Sync](#3-backend--módulo-de-sync)
4. [Frontend — Base de datos local (IndexedDB / Dexie.js)](#4-frontend--base-de-datos-local-indexeddb--dexiejs)
5. [Frontend — SyncStore (Zustand)](#5-frontend--syncstore-zustand)
6. [Frontend — Hook useOfflinePOS](#6-frontend--hook-useofflinepos)
7. [Frontend — Componente SyncStatusBar](#7-frontend--componente-syncstatusbar)
8. [PWA y Service Worker](#8-pwa-y-service-worker)
9. [Flujo de operación completo](#9-flujo-de-operación-completo)
10. [Estructura de archivos](#10-estructura-de-archivos)
11. [Consideraciones y limitaciones](#11-consideraciones-y-limitaciones)

---

## 1. Visión general

El POS Retail de Automia está diseñado para operar en **zonas urbanas donde la conexión a internet puede ser intermitente o poco confiable**. Para garantizar la continuidad operativa, se implementa una arquitectura **offline-first** que permite:

- **Operar sin conexión**: El POS puede buscar productos, buscar clientes y registrar ventas sin conexión a internet.
- **Sincronizar automáticamente**: Cada 5 minutos, si hay conexión, el sistema sincroniza datos con el servidor.
- **Sincronizar manualmente**: El operador puede forzar una sincronización en cualquier momento con un botón.
- **Encolar ventas offline**: Las ventas realizadas sin conexión se guardan localmente y se envían al servidor cuando se restaura la conexión.
- **Funcionar como PWA instalable**: La aplicación se puede instalar en el dispositivo y carga sin internet gracias al Service Worker.

### Tecnologías utilizadas

| Componente               | Tecnología                        |
| ------------------------ | --------------------------------- |
| Base de datos local      | IndexedDB via **Dexie.js**        |
| Estado de sincronización | **Zustand** (store reactivo)      |
| Service Worker           | **Workbox** via `vite-plugin-pwa` |
| Backend sync             | **NestJS** + Prisma ORM           |
| Comunicación             | REST API con JWT                  |

---

## 2. Arquitectura del sistema

```
┌─────────────────────────────────────────────────────────────┐
│                        POS RETAIL (Browser)                 │
│                                                             │
│  ┌──────────────┐   ┌──────────────┐   ┌────────────────┐  │
│  │ POSRetail    │   │  SyncStore   │   │  SyncStatusBar │  │
│  │ Template     │──▶│  (Zustand)   │──▶│  (UI)          │  │
│  └──────┬───────┘   └──────┬───────┘   └────────────────┘  │
│         │                  │                                │
│         ▼                  ▼                                │
│  ┌──────────────┐   ┌──────────────┐                        │
│  │ useOffline   │   │  sync.       │                        │
│  │ POS (hook)   │   │  service.js  │                        │
│  └──────┬───────┘   └──────┬───────┘                        │
│         │                  │                                │
│         ▼                  │                                │
│  ┌──────────────┐          │                                │
│  │  Dexie.js    │◀─────────┘                                │
│  │  (IndexedDB) │                                           │
│  └──────────────┘                                           │
│                                                             │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  Service Worker (Workbox) — cachea app shell + fonts │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────┬───────────────────────────────────────┘
                      │ HTTP (cuando hay conexión)
                      ▼
┌─────────────────────────────────────────────────────────────┐
│                   SERVIDOR (NestJS)                          │
│                                                             │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  SyncController (/api/v1/sync/)                      │   │
│  │    GET  /full          → Sync completo               │   │
│  │    GET  /delta?since=  → Solo cambios recientes      │   │
│  │    POST /push/ventas   → Recibir ventas offline      │   │
│  └──────────────────────┬───────────────────────────────┘   │
│                         │                                   │
│  ┌──────────────────────▼───────────────────────────────┐   │
│  │  SyncService (Prisma ORM → PostgreSQL)               │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
```

---

## 3. Backend — Módulo de Sync

### Ubicación

```
smartfactvoice-backend/src/sync/
├── sync.module.ts      # Módulo NestJS
├── sync.service.ts     # Lógica de negocio
└── sync.controller.ts  # Endpoints REST
```

### Endpoints

Todos los endpoints requieren autenticación JWT (`Bearer token`) y usan la `empresa_id` del token del usuario.

| Método | Ruta                      | Descripción                                                                                                                                                                                                                                |
| ------ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `GET`  | `/sync/full`              | Devuelve **todos** los catálogos en una sola llamada. Se usa para la primera sincronización o sync manual completo.                                                                                                                        |
| `GET`  | `/sync/delta?since=<ISO>` | Devuelve solo los registros **modificados desde** el timestamp `since`. Se usa para sync automático periódico.                                                                                                                             |
| `GET`  | `/sync/productos?since=`  | Solo productos (con relaciones: unidad medida, afectación IVA, categoría, marca, stock).                                                                                                                                                   |
| `GET`  | `/sync/clientes?since=`   | Solo clientes (con persona, tipo operación, lista precios).                                                                                                                                                                                |
| `GET`  | `/sync/categorias?since=` | Solo categorías.                                                                                                                                                                                                                           |
| `GET`  | `/sync/medios-pago`       | Medios de pago activos de la empresa.                                                                                                                                                                                                      |
| `GET`  | `/sync/referenciales`     | Datos referenciales estáticos (afectación IVA, tipos de operación, impuesto, transacción, indicador presencia, condición operación, naturaleza receptor, tipo documento identidad, tipo contribuyente, forma procesamiento pago, monedas). |
| `GET`  | `/sync/numeraciones`      | Numeraciones de documentos activas (con punto expedición, timbrado, tipo documento).                                                                                                                                                       |
| `GET`  | `/sync/condiciones-pago`  | Condiciones de pago activas.                                                                                                                                                                                                               |
| `POST` | `/sync/push/ventas`       | Recibe un array de ventas realizadas offline. Body: `{ ventas: [...] }`                                                                                                                                                                    |

### Respuesta del sync completo (`GET /sync/full`)

```json
{
  "productos": [...],
  "clientes": [...],
  "categorias": [...],
  "medios_pago": [...],
  "referenciales": {
    "afectacion_iva": [...],
    "tipo_operacion": [...],
    "tipo_impuesto": [...],
    "tipo_transaccion": [...],
    "indicador_presencia": [...],
    "condicion_operacion": [...],
    "naturaleza_receptor": [...],
    "tipo_documento_identidad": [...],
    "tipo_contribuyente": [...],
    "forma_procesamiento_pago": [...],
    "monedas": [...]
  },
  "numeraciones": [...],
  "condiciones_pago": [...],
  "synced_at": "2026-02-12T23:00:00.000Z"
}
```

### Respuesta del sync delta (`GET /sync/delta?since=...`)

Idéntica estructura pero solo incluye registros **modificados** después de `since` para `productos`, `clientes` y `categorías`. Los demás catálogos (medios pago, numeraciones, condiciones pago) se envían siempre completos porque cambian con poca frecuencia.

Incluye campos adicionales:

```json
{
  "is_delta": true,
  "since": "2026-02-12T22:55:00.000Z",
  "synced_at": "2026-02-12T23:00:00.000Z"
}
```

### Lógica delta (ejemplo: productos)

```typescript
async getProductosDelta(empresaId: string, since?: string) {
  const where = { empresa_id: empresaId, deleted: false };
  if (since) {
    where.updated_at = { gte: new Date(since) };
  }
  return this.prisma.productos.findMany({ where, include: { ... } });
}
```

Se filtra por `updated_at >= since` para obtener solo lo que cambió. Si `since` no se envía, devuelve todo.

### Push de ventas offline (`POST /sync/push/ventas`)

```json
// Request body
{
  "ventas": [
    {
      "_offline_id": "offline_1707123456_abc123",
      "_offline_created_at": "2026-02-12T20:00:00.000Z",
      "cabecera": { ... },
      "items": [ ... ],
      "pagos": [ ... ]
    }
  ]
}

// Response
{
  "processed": 1,
  "results": [
    {
      "_offline_id": "offline_1707123456_abc123",
      "status": "queued",
      "message": "Venta recibida para procesamiento"
    }
  ]
}
```

---

## 4. Frontend — Base de datos local (IndexedDB / Dexie.js)

### Ubicación

```
pos-ventas/src/offline/db.js
```

### ¿Qué es Dexie.js?

**Dexie.js** es un wrapper minimalista sobre la API nativa de IndexedDB del navegador. IndexedDB es una base de datos NoSQL integrada en todos los navegadores modernos, capaz de almacenar grandes cantidades de datos estructurados de forma persistente (sobrevive a recargas y reinicios del navegador).

Dexie simplifica las operaciones de lectura/escritura que con IndexedDB nativo serían verbosas y propensas a errores.

### Esquema de tablas

```javascript
db.version(1).stores({
  productos:
    'id, empresa_id, descripcion, cod_producto, codigo_barra, categoria_id, active, updated_at',
  clientes: 'id, cod_cliente, tipo_cliente, active, updated_at',
  categorias: 'id, descripcion, activo, padre_id',
  medios_pago: 'id, codigo, orden',
  referenciales: 'key',
  numeraciones: 'id, empresa_id, active',
  condiciones_pago: 'id, empresa_id, activo',
  offline_ventas: '++localId, _offline_id, created_at, status',
  sync_meta: 'key',
});
```

| Tabla              | Descripción                                       | PK                              |
| ------------------ | ------------------------------------------------- | ------------------------------- |
| `productos`        | Catálogo de productos con stock y relaciones      | `id` (UUID del servidor)        |
| `clientes`         | Clientes de la empresa con datos de persona       | `id` (UUID del servidor)        |
| `categorias`       | Categorías de productos                           | `id`                            |
| `medios_pago`      | Medios de pago activos                            | `id`                            |
| `referenciales`    | Datos referenciales agrupados por clave           | `key` (string: "referenciales") |
| `numeraciones`     | Numeraciones de documentos                        | `id`                            |
| `condiciones_pago` | Condiciones de pago                               | `id`                            |
| `offline_ventas`   | **Cola de ventas offline**                        | `++localId` (autoincremental)   |
| `sync_meta`        | Metadata de sincronización (última fecha, estado) | `key`                           |

### Funciones helper principales

```javascript
// Guardar un lote de items (upsert por id)
bulkPutItems('productos', arrayDeProductos);

// Encolar una venta offline
const offlineId = await enqueueOfflineVenta(facturaData);
// → Genera ID único: "offline_1707123456_abc123def"

// Obtener ventas pendientes de enviar
const pending = await getPendingOfflineVentas();

// Marcar venta como enviada
await markVentaSynced(localId);

// Buscar productos localmente (por descripción, código, código de barra)
const results = await searchProductosLocal('coca', 20);

// Buscar clientes localmente (por razón social, RUC, documento)
const results = await searchClientesLocal('juan', 20);

// Obtener productos por categoría
const results = await getProductosByCategoriaLocal(categoriaId, 50);
```

### Cola de ventas offline

Cada venta encolada tiene la siguiente estructura:

```javascript
{
  localId: 1,                          // Autoincremental local
  _offline_id: "offline_1707123456_abc123",  // ID único generado
  data: { cabecera, items, pagos, ... },      // Datos completos de la factura
  created_at: "2026-02-12T20:00:00.000Z",
  status: "pending"  // "pending" | "synced" | "failed"
}
```

---

## 5. Frontend — SyncStore (Zustand)

### Ubicación

```
pos-ventas/src/store/SyncStore.jsx
```

### ¿Qué hace?

Es un **store reactivo** (Zustand) que centraliza toda la lógica de sincronización. Cualquier componente React puede suscribirse a su estado para saber si hay conexión, si se está sincronizando, cuántas ventas hay pendientes, etc.

### Estado

```javascript
{
  isOnline: true,           // ¿Hay conexión a internet?
  isSyncing: false,         // ¿Se está sincronizando ahora?
  lastSyncAt: "2026-...",   // Timestamp de la última sync exitosa
  syncError: null,          // Mensaje de error (si falló la última sync)
  pendingVentasCount: 0,    // Cantidad de ventas offline pendientes de enviar
  syncProgress: null,       // { step: "productos", current: 2, total: 7 }
  autoSyncInterval: <ref>,  // Referencia al setInterval
}
```

### Acciones principales

#### `init()`

Se llama una vez al montar el componente POS. Realiza:

1. Carga la metadata de última sincronización desde IndexedDB.
2. Cuenta las ventas pendientes.
3. Registra los event listeners `online` / `offline` del navegador.
4. Inicia el auto-sync periódico.

#### `syncManual()` — Sincronización completa

1. Envía ventas pendientes al servidor (`_pushPendingVentas`).
2. Descarga **todos** los catálogos (`GET /sync/full`).
3. Guarda todo en IndexedDB (reemplaza lo existente).
4. Actualiza `lastSyncAt`.
5. Limpia ventas ya sincronizadas.

Se usa para: primera carga, botón "Sincronizar", o cuando se necesita forzar un refresh completo.

#### `syncAuto()` — Sincronización delta

1. Si nunca se sincronizó, redirige a `syncManual()`.
2. Envía ventas pendientes.
3. Descarga solo cambios desde `lastSyncAt` (`GET /sync/delta?since=...`).
4. Hace upsert (bulkPut) de los registros cambiados.
5. Para medios de pago, numeraciones y condiciones pago: reemplaza completo.

Se ejecuta: cada 5 minutos automáticamente y al reconectar a internet.

#### `_pushPendingVentas()` — Envío de ventas offline

1. Lee ventas con `status: "pending"` de IndexedDB.
2. Las envía al servidor (`POST /sync/push/ventas`).
3. Marca cada venta como `"synced"` o `"failed"` según la respuesta.
4. Actualiza el conteo de pendientes.

#### Auto-sync periódico

```javascript
// Se ejecuta cada 5 minutos si hay conexión
setInterval(
  () => {
    if (isOnline) syncAuto();
  },
  5 * 60 * 1000,
);
```

#### Detección de conexión

```javascript
window.addEventListener('online', () => {
  set({ isOnline: true });
  syncAuto(); // Al reconectar, sincronizar inmediatamente
});

window.addEventListener('offline', () => {
  set({ isOnline: false });
});
```

---

## 6. Frontend — Hook useOfflinePOS

### Ubicación

```
pos-ventas/src/hooks/useOfflinePOS.js
```

### Propósito

Provee funciones con **fallback transparente**: intentan usar la API del servidor, y si falla (por desconexión o error de red), leen de la base de datos local IndexedDB.

### Patrón de fallback

```javascript
const searchProductosFallback = async (query, limit, apiFn) => {
  if (isOnline && apiFn) {
    try {
      return await apiFn(query, limit); // ← Intenta API
    } catch {
      return searchProductosLocal(query, limit); // ← Fallback a Dexie
    }
  }
  return searchProductosLocal(query, limit); // ← Offline directo
};
```

### Funciones disponibles

| Función                                         | Descripción                           |
| ----------------------------------------------- | ------------------------------------- |
| `searchProductosFallback(query, limit, apiFn)`  | Busca productos (API → Dexie)         |
| `getProductosByCategoriaFallback(catId, apiFn)` | Productos por categoría (API → Dexie) |
| `getProductosFallback(apiFn, page, limit)`      | Grid de productos (API → Dexie)       |
| `searchClientesFallback(query, apiFn)`          | Busca clientes (API → Dexie)          |
| `getClientesFallback(apiFn, page, limit)`       | Lista clientes (API → Dexie)          |
| `getCategoriasFallback(apiFn)`                  | Categorías (API → Dexie)              |
| `getMediosPagoFallback(apiFn)`                  | Medios de pago (API → Dexie)          |
| `getReferencialesFallback(apiFn)`               | Referenciales (API → Dexie)           |
| `getCondicionesPagoFallback(apiFn)`             | Condiciones pago (API → Dexie)        |
| `createFacturaFallback(data, apiFn)`            | Crear factura (API → cola offline)    |

### Caso especial: `createFacturaFallback`

Esta función tiene un comportamiento diferente al resto:

```javascript
// Si hay conexión → envía a la API normalmente
// Si la API falla por red → encola en IndexedDB
// Si no hay conexión → encola directamente en IndexedDB

const result = await createFacturaFallback(facturaData, createFactura);
// result.offline === true  → se encoló localmente
// result.offline === false → se envió al servidor exitosamente
```

---

## 7. Frontend — Componente SyncStatusBar

### Ubicación

```
pos-ventas/src/components/ui/SyncStatusBar.jsx
```

### Descripción

Componente visual que muestra el estado de sincronización en la barra superior del POS. Tiene dos modos:

#### Modo `compact` (usado en el POS)

Muestra solo iconos pequeños:

- 🟢/🔴 Indicador de conexión (wifi on/off)
- 🔄 Botón de sync manual (con spinner cuando sincroniza)
- 🔶 Badge con cantidad de ventas pendientes (si hay)

#### Modo completo

Muestra toda la información expandida:

- Chip con estado de conexión ("En línea" / "Sin conexión")
- Texto con última sincronización ("Hace 3 min", "Nunca", etc.)
- Botón de sincronización con tooltip de progreso
- Chip con cantidad de ventas pendientes

### Integración en el POS

```jsx
// En POSRetailTemplate.jsx → TopInfoBar → info-left
<SyncStatusBar compact />
```

Se ubica junto al botón de configuración en la barra superior del POS.

---

## 8. PWA y Service Worker

### Configuración

Se usa `vite-plugin-pwa` que genera automáticamente un Service Worker basado en **Workbox** al hacer `npm run build`.

### Archivo: `vite.config.js`

```javascript
VitePWA({
  registerType: 'autoUpdate', // El SW se actualiza automáticamente
  manifest: {
    name: 'Smart POS',
    short_name: 'Smart POS',
    theme_color: '#f88533',
    display: 'standalone', // Se ve como app nativa
    // ...
  },
  workbox: {
    maximumFileSizeToCacheInBytes: 15 * 1024 * 1024, // 15MB
    runtimeCaching: [
      // Google Fonts → CacheFirst (cachea indefinidamente)
      // /api/v1/sync/* → NetworkFirst (intenta red, fallback a cache)
      // Imágenes → CacheFirst (cachea 30 días)
    ],
  },
});
```

### Estrategias de caché

| Recurso                   | Estrategia       | Descripción                                          |
| ------------------------- | ---------------- | ---------------------------------------------------- |
| App shell (JS, CSS, HTML) | **Precache**     | Se cachea al instalar el SW. Carga sin internet.     |
| Google Fonts              | **CacheFirst**   | Se descarga una vez y se sirve desde caché.          |
| API de sync (`/sync/*`)   | **NetworkFirst** | Intenta red primero (10s timeout), fallback a caché. |
| Imágenes de productos     | **CacheFirst**   | Se cachean por 30 días.                              |

### Archivos generados en `dist/`

| Archivo                | Descripción                        |
| ---------------------- | ---------------------------------- |
| `sw.js`                | Service Worker principal (Workbox) |
| `workbox-*.js`         | Runtime de Workbox                 |
| `manifest.webmanifest` | Manifest PWA para instalación      |
| `registerSW.js`        | Script de auto-registro del SW     |

### Meta tags HTML

```html
<link rel="apple-touch-icon" href="/logo_simple.png" />
<meta name="theme-color" content="#f88533" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
```

Estos permiten que en iOS la app se instale correctamente en la pantalla de inicio y se comporte como aplicación nativa.

---

## 9. Flujo de operación completo

### Escenario 1: Primera vez (con conexión)

```
1. El operador abre el POS en el navegador
2. El Service Worker se registra y cachea el app shell
3. SyncStore.init() detecta que nunca se sincronizó (lastSyncAt = null)
4. Se ejecuta syncManual() automáticamente:
   → GET /sync/full → descarga ~todos los catálogos
   → Guarda en IndexedDB (productos, clientes, categorías, etc.)
   → Guarda lastSyncAt = "2026-02-12T23:00:00Z"
5. El POS está listo para operar (online u offline)
```

### Escenario 2: Operación normal (con conexión)

```
1. Operador busca producto → API directa (rápida)
2. Operador busca cliente → API directa
3. Operador registra venta → createFactura() al servidor
4. Cada 5 minutos: syncAuto()
   → Push ventas pendientes (si hay)
   → GET /sync/delta?since=lastSyncAt → actualiza cambios en Dexie
```

### Escenario 3: Se pierde la conexión

```
1. window.offline → SyncStore.isOnline = false
2. SyncStatusBar cambia a rojo: "Sin conexión"
3. Operador busca producto → API falla → fallback a Dexie local ✅
4. Operador busca cliente → API falla → fallback a Dexie local ✅
5. Operador registra venta → enqueueOfflineVenta() → IndexedDB ✅
   → Toast: "Venta guardada localmente. Se enviará al reconectar."
   → Badge: "1 venta pendiente"
6. Operador registra otra venta → Badge: "2 ventas pendientes"
```

### Escenario 4: Se restaura la conexión

```
1. window.online → SyncStore.isOnline = true
2. SyncStatusBar cambia a verde: "En línea"
3. syncAuto() se ejecuta inmediatamente:
   a. _pushPendingVentas()
      → Lee 2 ventas de offline_ventas con status "pending"
      → POST /sync/push/ventas → envía al servidor
      → Marca ambas como "synced"
      → Badge: "0 ventas pendientes"
   b. GET /sync/delta?since=lastSyncAt
      → Descarga productos/clientes/categorías modificados
      → Actualiza Dexie con los cambios
   c. Actualiza lastSyncAt
```

### Escenario 5: Sync manual (botón)

```
1. Operador presiona botón 🔄 en la barra superior
2. SyncStatusBar muestra spinner + progreso:
   "Sincronizando: productos (2/7)"
3. syncManual() ejecuta:
   → Push ventas pendientes
   → GET /sync/full (descarga todo)
   → Reemplaza toda la DB local
4. Spinner desaparece, "Sync: Hace un momento"
```

---

## 10. Estructura de archivos

```
smartfactvoice-backend/
└── src/sync/
    ├── sync.module.ts          # Módulo NestJS (imports: PrismaModule)
    ├── sync.service.ts         # Lógica: delta, full, push ventas
    └── sync.controller.ts      # Endpoints REST (JWT protegidos)

pos-ventas/
├── index.html                  # Meta tags PWA
├── vite.config.js              # Plugin PWA + Workbox config
├── public/
│   └── logo_simple.png          # Icono PWA (512x512)
├── src/
│   ├── api/
│   │   └── sync.service.js     # Cliente API: syncFull, syncDelta, pushVentas
│   ├── offline/
│   │   └── db.js               # Dexie.js: esquema DB, helpers CRUD, búsqueda local
│   ├── store/
│   │   └── SyncStore.jsx       # Zustand: estado sync, auto-sync, push ventas
│   ├── hooks/
│   │   └── useOfflinePOS.js    # Hook: fallbacks API → Dexie para el POS
│   └── components/
│       ├── ui/
│       │   └── SyncStatusBar.jsx   # UI: indicador conexión, botón sync, badge
│       └── templates/
│           └── POSRetailTemplate.jsx  # POS integrado con offline fallbacks
└── docs/
    └── OFFLINE_SYNC.md         # Este documento
```

---

## 11. Consideraciones y limitaciones

### Almacenamiento

- **IndexedDB** tiene un límite de almacenamiento que varía por navegador (generalmente 50% del disco disponible, mínimo ~50MB). Para catálogos de productos normales, esto es más que suficiente.
- Las ventas offline se limpian (`clearSyncedVentas`) después de cada sync exitoso para no acumular datos innecesarios.

### Conflictos de datos

- El sistema actual usa una estrategia **"servidor gana"**: al sincronizar, los datos del servidor reemplazan los locales.
- Las ventas offline se envían con su `_offline_id` y `_offline_created_at` para trazabilidad.
- Si una venta offline falla al procesarse en el servidor, queda marcada como `"failed"` con el mensaje de error.

### Numeraciones offline

- Las numeraciones de documentos se sincronizan localmente, pero **la asignación de números de factura en modo offline debe ser manejada con cuidado** para evitar duplicados. Considerar reservar rangos de numeración por caja/dispositivo en una futura iteración.

### Seguridad

- El JWT se almacena en `localStorage`. Si el token expira mientras se está offline, las ventas se encolan igualmente y se intentarán enviar cuando haya conexión y un token válido.
- Los datos en IndexedDB no están encriptados. Para datos sensibles, considerar usar la Web Crypto API en el futuro.

### Service Worker

- El SW usa `registerType: 'autoUpdate'`, lo que significa que se actualiza silenciosamente cuando hay una nueva versión.
- El `maximumFileSizeToCacheInBytes` está configurado en 15MB para soportar el bundle grande (~7.5MB).
- Las Google Fonts se cachean para que la UI se vea correcta sin internet.

### Compatibilidad

- **Navegadores soportados**: Chrome 67+, Firefox 68+, Safari 14+, Edge 79+
- **iOS**: Funciona como PWA instalable desde Safari (con limitaciones propias de iOS como el límite de 7 días de caché del SW).
- **Android**: Instalable como PWA desde Chrome con experiencia nativa completa.

### Verificaciones Para el desarrollador :

\*\*Hay varias formas de inspeccionar IndexedDB:

\*\*Opción 1: Chrome DevTools (más fácil)
Abrí DevTools (F12)
Pestaña Application (o "Aplicación")
Panel izquierdo → Storage → IndexedDB
Expandí la base de datos POSOfflineDB
Ahí verás todas las tablas: productos, clientes, offline_ventas, sync_meta, etc.
Hacé click en cada tabla para ver los registros, filtrar y buscar
Para ver ventas pendientes específicamente: click en offline_ventas → filtrá por status = "pending".

\*\*Opción 2: Desde la consola del navegador
Podés ejecutar directamente en la consola de DevTools (F12 → Console):

js
// Importar Dexie DB (ya está cargada en la app)
const db = await import('/src/offline/db.js').then(m => m.default);

// Ver ventas pendientes de sincronizar
await db.offline_ventas.where('status').equals('pending').toArray();

// Ver todas las ventas offline (pendientes, sincronizadas, fallidas)
await db.offline_ventas.toArray();

// Ver metadata de última sincronización
await db.sync_meta.get('last_sync');

// Contar productos en la DB local
await db.productos.count();

// Contar clientes
await db.clientes.count();

// Buscar un producto específico
await db.productos.where('descripcion').startsWithIgnoreCase('coca').toArray();

\*\*Opción 3: Extensión de Chrome
Instalá la extensión "IndexedDB Debugger" o "idb-devtools" desde Chrome Web Store — te da una interfaz tipo tabla más cómoda para explorar los datos.

Consejo rápido
Si querés un resumen rápido del estado offline, pegá esto en la consola:

js
(async () => {
const db = await import('/src/offline/db.js').then(m => m.default);
const meta = await db.sync_meta.get('last_sync');
const pendientes = await db.offline_ventas.where('status').equals('pending').count();
const sincronizadas = await db.offline_ventas.where('status').equals('synced').count();
const fallidas = await db.offline_ventas.where('status').equals('failed').count();
console.table({
'Última sync': meta?.synced_at || 'Nunca',
'Productos locales': await db.productos.count(),
'Clientes locales': await db.clientes.count(),
'Categorías locales': await db.categorias.count(),
'Ventas pendientes': pendientes,
'Ventas sincronizadas': sincronizadas,
'Ventas fallidas': fallidas,
});
})();

\*\*La opción más directa para el día a día es Application → IndexedDB en DevTools.

\*\*Feedback submitted
