# Sistema de Sincronizacion BD Local <-> BD Nube

## Descripcion General

Sistema de replicacion master-master entre la BD local (`pos_abarrotes`) y una BD en la nube.
Cada sucursal tiene su propia BD local y envia/recibe datos a/de un servidor central en la nube.

- **Push**: Local -> Nube (enviar cambios locales al servidor)
- **Pull**: Nube -> Local (recibir cambios de otras sucursales)
- **Full Sync**: Push + Pull en secuencia

---

## Archivos del Sistema

| Archivo | Ubicacion | Proposito |
|---------|-----------|-----------|
| `sync_config.php` | `reportes/sync/` | Definicion de tablas, dependencias FK, modos de sync |
| `sync_utils.php` | `reportes/sync/` | Funciones utilitarias (API calls, mapeo FK, timestamps, logging) |
| `sync_engine.php` | `reportes/sync/` | Orquestador principal: push, pull, status, log, reset |
| `sync_receive.php` | `reportes/sync/` | Endpoints del lado NUBE: recibe datos de sucursales |
| `sync_migration.sql` | `reportes/sync/` | SQL para BD LOCAL: tablas sync + agregar updated_at |
| `sync_migration_cloud.sql` | `reportes/sync/` | SQL para BD NUBE: lo mismo + columnas sucursal, id_origen |
| `sync_migration_cloud_edits.sql` | `reportes/sync/` | SQL para BD NUBE: agrega editado_nube + triggers BEFORE UPDATE |
| `api.php` | `reportes/funciones_php/` | Router principal - contiene los cases de sync |

### Frontend (Electron/React)

| Archivo | Ubicacion | Proposito |
|---------|-----------|-----------|
| `SyncContext.js` | `src/context/` | Contexto global de sync: timer automatico, estado, acciones |
| `SyncModal.js` | `src/components/sync/` | Modal con controles de sync, historial, errores |
| `SyncModal.css` | `src/components/sync/` | Estilos del modal |
| `Sidebar.js` | `src/components/layout/` | Indicador de sync en sidebar (modificado) |
| `SettingsPage.js` | `src/pages/settings/` | Panel de control de sync en configuracion (modificado) |
| `apiService.js` | `src/services/` | Metodos de API para sync (modificado) |
| `App.js` | `src/` | Monta el SyncProvider (modificado) |

---

## Estructura de la Base de Datos

### Tablas de Control (creadas por sync_migration.sql)

#### `sync_log` - Historial de sincronizaciones
```sql
id INT AUTO_INCREMENT PRIMARY KEY
tabla VARCHAR(100)          -- Nombre de la tabla sincronizada
sucursal VARCHAR(100)       -- Sucursal que ejecuto el sync
direccion ENUM('push','pull')
registros_enviados INT
registros_recibidos INT
registros_conflictos INT
ultimo_sync_timestamp DATETIME(6)
estado ENUM('iniciado','completado','error')
error_mensaje TEXT
duracion_ms INT
created_at DATETIME(6)
```

#### `sync_control` - Marca de agua (watermark) por tabla
```sql
id INT AUTO_INCREMENT PRIMARY KEY
tabla VARCHAR(100)
sucursal VARCHAR(100)
last_push_at DATETIME(6)   -- Ultimo push exitoso
last_pull_at DATETIME(6)   -- Ultimo pull exitoso
UNIQUE KEY (tabla, sucursal)
```

#### `sync_id_map` - Mapeo de IDs remotos a locales
```sql
id INT AUTO_INCREMENT PRIMARY KEY
tabla VARCHAR(100)
sucursal_origen VARCHAR(100)  -- De donde vino el registro
id_remoto INT                 -- ID original en la sucursal origen
id_local INT                  -- ID asignado en esta BD
UNIQUE KEY (tabla, sucursal_origen, id_remoto)
```

### Columnas Agregadas a Tablas Existentes

**BD Local:** Se agrego `updated_at DATETIME(6) ON UPDATE CURRENT_TIMESTAMP(6)` + indice a las 48 tablas sincronizadas. Esto permite detectar que registros cambiaron desde el ultimo sync.

**BD Nube:** Lo mismo que local + se agrego a CADA tabla:
- `sucursal VARCHAR(100)` - Identifica de que sucursal vino el registro
- `id_origen INT` - El ID original que tenia en la sucursal
- `UNIQUE KEY uk_sucursal_id_origen (sucursal, id_origen)` - Clave para deduplicacion

---

## Tablas Sincronizadas (48 tablas, 7 niveles)

Las tablas se sincronizan en orden topologico para respetar foreign keys.

### Nivel 0 - Base (sin dependencias FK)
| Tabla | PK | Modo | Notas |
|-------|----|----|-------|
| categorias | id | bidirectional | |
| almacenes | id | bidirectional | |
| configuraciones | id | bidirectional | |
| cajas | id | owned | Por sucursal |
| cat_regimen_fiscal | id | bidirectional | Catalogo SAT |
| cat_uso_cfdi | id | bidirectional | Catalogo SAT |
| cat_forma_pago | id | bidirectional | Catalogo SAT |
| cat_metodo_pago | id | bidirectional | Catalogo SAT |
| precio_rangos | id | bidirectional | |

### Nivel 1 - Dependen de Nivel 0
| Tabla | PK | Modo | FK |
|-------|----|----|------|
| roles | id | bidirectional | |
| proveedores | id | bidirectional | |
| vehiculos | id | bidirectional | |
| rangos_flete | id | bidirectional | |

### Nivel 2 - Dependen de Nivel 0-1
| Tabla | PK | Modo | FK | Notas |
|-------|----|----|------|-------|
| permisos | id | bidirectional | | |
| usuarios | id | bidirectional | rol_id -> roles | exclude_cols: ['password'] |
| productos | id | bidirectional | categoria_id -> categorias | |
| clientes | id | bidirectional | | |

### Nivel 3 - Tablas de relacion/detalle
| Tabla | PK | FK | Modo |
|-------|----|----|------|
| rol_permisos | id | rol_id->roles, permiso_id->permisos | bidirectional |
| usuario_almacenes | id | usuario_id->usuarios, almacen_id->almacenes | bidirectional |
| producto_equivalencias | id | producto_id->productos, producto_equivalente_id->productos | bidirectional |
| descuentos_volumen | id | producto_id->productos | bidirectional |
| cliente_telefonos | id | cliente_id->clientes | bidirectional |
| cliente_direcciones | id | cliente_id->clientes | bidirectional |
| cliente_datos_fiscales | id | cliente_id->clientes | bidirectional |
| configuracion_rangos | id | | bidirectional |
| diferencia_costos | id | producto_id->productos | bidirectional |
| vehiculo_rangos | id | vehiculo_id->vehiculos, rango_flete_id->rangos_flete | bidirectional |

### Nivel 4 - Transacciones principales
| Tabla | PK | FK | Modo |
|-------|----|----|------|
| ventas | id | cliente_id->clientes, usuario_id->usuarios, almacen_id->almacenes | owned |
| ventas_mostrador | id | usuario_id->usuarios, caja_id->cajas | owned |
| ordenes_compra | id | proveedor_id->proveedores, usuario_id->usuarios, almacen_id->almacenes | owned |
| rutas_entrega | id | vehiculo_id->vehiculos, usuario_id->usuarios | owned |
| sesiones | id | usuario_id->usuarios | owned |
| log_actividad | id | usuario_id->usuarios | owned |
| log_seguridad | id | usuario_id->usuarios | owned |

### Nivel 5 - Detalles de transacciones
| Tabla | PK | FK | Modo |
|-------|----|----|------|
| venta_detalles | id | venta_id->ventas, producto_id->productos | owned |
| venta_pagos | id | venta_id->ventas | owned |
| ventas_mostrador_detalles | id | venta_mostrador_id->ventas_mostrador, producto_id->productos | owned |
| ventas_mostrador_pagos | id | venta_mostrador_id->ventas_mostrador | owned |
| cortes_caja | id | usuario_id->usuarios, caja_id->cajas | owned |
| retiros_efectivo | id | usuario_id->usuarios, caja_id->cajas | owned |
| ingresos_efectivo | id | usuario_id->usuarios, caja_id->cajas | owned |
| orden_compra_detalle | id | orden_compra_id->ordenes_compra, producto_id->productos | owned |
| ruta_pedidos | id | ruta_id->rutas_entrega, venta_id->ventas | owned |
| pagos_clientes | id | cliente_id->clientes, usuario_id->usuarios | owned |
| facturas | id | cliente_id->clientes | owned |

### Nivel 6 - Detalles de factura
| Tabla | PK | FK | Modo |
|-------|----|----|------|
| factura_conceptos | id | factura_id->facturas | owned |
| factura_ventas | id | factura_id->facturas, venta_id->ventas | owned |
| factura_ventas_mostrador | id | factura_id->facturas, venta_mostrador_id->ventas_mostrador | owned |

---

## Como Funciona el Sync

### Flujo de Push (Local -> Nube)

```
1. Leer last_push_at de sync_control para la tabla
2. SELECT * FROM tabla WHERE updated_at > last_push_at (lotes de 500)
3. Enviar a la nube via POST tipo=syncReceive
4. La nube recibe y hace UPSERT por (sucursal, id_origen):
   a. Si no existe: INSERT con sucursal e id_origen del registro
   b. Si existe: UPDATE si el timestamp es mas reciente (last-write-wins)
5. La nube mapea FKs: traduce IDs de sucursal a IDs de nube via sync_id_map
6. Actualizar last_push_at en sync_control
```

### Flujo de Pull (Nube -> Local)

```
1. Leer last_pull_at de sync_control
2. Pedir a la nube via POST tipo=syncSend: "dame registros de OTRAS sucursales desde last_pull_at"
3. La nube retorna registros con sucursal e id_origen
4. Por cada registro recibido:
   a. Buscar en sync_id_map si ya existe localmente
   b. Mapear foreign keys remotas a IDs locales via sync_id_map
   c. Si no existe: INSERT y guardar mapeo en sync_id_map
   d. Si existe: UPDATE si timestamp es mas reciente
5. Actualizar last_pull_at
```

### Resolucion de Conflictos

- **Last-write-wins**: El registro con `updated_at` mas reciente gana
- **Empate de timestamps**: La sucursal con nombre alfabeticamente menor gana
- **Soft deletes**: Se usa `activo=0` en vez de DELETE para evitar registros fantasma

### Mapeo de IDs (sync_id_map)

Problema: La sucursal A tiene un producto con `id=5`, la sucursal B tiene otro producto distinto con `id=5`.

Solucion:
- La nube guarda ambos con sus propios auto_increment IDs
- `sync_id_map` traduce: `(tabla=productos, sucursal=sucB, id_remoto=5) -> id_local=123`
- Al hacer pull en sucursal A, el producto de sucursal B se inserta con un nuevo ID local
- Las FKs se remapean automaticamente usando estos mapeos

---

## Configuracion en la BD

Se guardan como configuraciones globales (categoria: 'sync'):

| Clave | Descripcion | Ejemplo |
|-------|------------|---------|
| `caja_api_nube_url` | URL del API en la nube | `https://servidor.com/api.php` |
| `caja_api_nube_negocio` | Identificador del negocio | `abarrotes_martinez` |
| `caja_api_nube_sucursal` | Identificador de esta sucursal | `sucursal_centro` |
| `caja_api_nube_auto_sync` | Auto-sync activo (0/1) | `1` |
| `caja_api_nube_auto_sync_interval` | Intervalo en minutos | `5` |

---

## Endpoints API

### Lado Local (sync_engine.php)
| tipo | Descripcion |
|------|-------------|
| `syncPush` | Push todas las tablas a la nube |
| `syncPushTable` | Push una tabla especifica |
| `syncPull` | Pull todas las tablas desde la nube |
| `syncPullTable` | Pull una tabla especifica |
| `syncFullSync` | Push + Pull completo |
| `syncStatus` | Estado de sync por tabla |
| `syncLog` | Historial de sincronizaciones |
| `syncReset` | Limpiar datos de sync (target: local/cloud/both) |

### Lado Nube (sync_receive.php)
| tipo | Descripcion |
|------|-------------|
| `syncReceive` | Recibe datos de una sucursal (UPSERT) |
| `syncSend` | Envia datos a una sucursal (registros de OTRAS sucursales) |
| `syncCloudReset` | Limpia todos los datos sincronizados en la nube |

---

## Como Agregar una Nueva Tabla al Sync

### Paso 1: Agregar a sync_config.php

Agregar la tabla al array `$SYNC_TABLES` con la estructura correcta:

```php
'mi_nueva_tabla' => [
    'pk' => 'id',                    // Nombre de la columna primary key
    'timestamp' => 'updated_at',     // Columna de deteccion de cambios
    'fk' => [                        // Foreign keys (columna => tabla referenciada)
        'usuario_id' => 'usuarios',
        'producto_id' => 'productos'
    ],
    'mode' => 'bidirectional',       // 'bidirectional' o 'owned'
    'level' => 3,                    // Nivel topologico (ver dependencias FK)
    'exclude_cols' => [],            // Columnas a excluir del sync (ej: passwords)
],
```

**Reglas para el nivel:**
- Nivel 0: Sin FKs a otras tablas sincronizadas
- Nivel N: Todas las tablas referenciadas por FK deben tener nivel < N
- Si la tabla referencia a `productos` (nivel 2), el nivel minimo es 3

**Reglas para mode:**
- `bidirectional`: Datos compartidos entre sucursales (productos, clientes, categorias)
- `owned`: Datos que pertenecen a una sucursal (ventas, cortes, logs)

### Paso 2: Agregar updated_at a la tabla (si no existe)

```sql
-- En BD local
ALTER TABLE mi_nueva_tabla
ADD COLUMN updated_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6);

ALTER TABLE mi_nueva_tabla ADD INDEX idx_updated_at (updated_at);

-- En BD nube (ademas de updated_at)
ALTER TABLE mi_nueva_tabla
ADD COLUMN sucursal VARCHAR(100) NOT NULL DEFAULT '',
ADD COLUMN id_origen INT NOT NULL DEFAULT 0,
ADD UNIQUE KEY uk_sucursal_id_origen (sucursal, id_origen),
ADD INDEX idx_sucursal (sucursal);
```

### Paso 3: Verificar

1. Hacer un `syncReset` (limpiar sync)
2. Ejecutar `syncPush` - la nueva tabla debe aparecer en los logs
3. Verificar en la nube que los registros llegaron con sucursal e id_origen correctos

---

## Como Agregar un Nuevo Componente/Pagina que use Sync

### Acceder al estado de sync desde cualquier componente:

```jsx
import { useSync } from '../../context/SyncContext';

const MiComponente = () => {
  const {
    active,           // bool: auto-sync activo?
    syncing,          // bool: sync en progreso?
    lastResult,       // objeto: ultimo resultado de sync
    syncHistory,      // array: ultimos 20 resultados
    syncConfig,       // {url, negocio, sucursal} o null
    countdown,        // int: segundos para proximo sync
    interval,         // int: minutos entre syncs
    manualSync,       // fn('push'|'pull'|'full')
    activate,         // fn(minutos) - activar auto-sync
    deactivate,       // fn() - desactivar auto-sync
    setShowModal,     // fn(bool) - abrir/cerrar modal de sync
  } = useSync();

  // Ejecutar sync manual
  const hacerSync = async () => {
    const result = await manualSync('full');
    if (result.success) {
      console.log('Sync exitoso');
    }
  };

  return (
    <div>
      <p>Sync: {active ? 'Activo' : 'Inactivo'}</p>
      {syncing && <p>Sincronizando...</p>}
      <button onClick={hacerSync}>Sincronizar Ahora</button>
    </div>
  );
};
```

### Notas importantes:
- El `SyncProvider` se monta en `App.js` al autenticarse
- Se limpia automaticamente al hacer logout
- La config se carga de la BD, no de localStorage
- El timer persiste mientras la app este abierta y el usuario autenticado

---

## Troubleshooting

### Push falla con error de FK en la nube
- Causa: La tabla referenciada no se sincronizo primero
- Solucion: Verificar que el `level` de la tabla sea mayor que el de sus dependencias

### Registros no se detectan para push
- Causa: La columna `updated_at` no tiene `ON UPDATE CURRENT_TIMESTAMP`
- Verificar: `SHOW CREATE TABLE mi_tabla` debe mostrar `on update current_timestamp`

### IDs no coinciden despues del pull
- Causa: Normal. Cada BD tiene sus propios auto_increment
- El `sync_id_map` maneja la traduccion. Nunca comparar IDs directamente entre sucursales.

### Error "Tabla no valida para sync"
- La tabla no esta en el array `$SYNC_TABLES` de sync_config.php

### Sync queda en estado "iniciado" sin completar
- Posible timeout del servidor. Verificar `max_execution_time` en PHP y timeout en sync_config.
- Los registros se procesan en lotes de 500 para evitar timeouts.

---

## Sistema editado_nube (Ediciones en el Servidor)

### Problema
Cuando alguien edita un registro directamente en la BD de la nube (ej: cambiar precio de un producto),
ese cambio debe reflejarse en la sucursal duena del registro, pero NO en otras sucursales.

### Solucion: Flag `editado_nube`

Cada tabla en la BD nube tiene una columna `editado_nube TINYINT(1) DEFAULT 0`.

**Triggers BEFORE UPDATE**: Cada tabla tiene un trigger que marca `editado_nube = 1` automaticamente
cuando se edita un registro, EXCEPTO cuando la variable de sesion `@is_sync = 1` esta activa
(lo que indica que la edicion viene del proceso de sync, no de una persona).

### Flujo completo

```
1. Alguien edita producto X de sucursal A en la nube
   -> Trigger marca editado_nube = 1

2. Sucursal A hace pull (syncSend):
   - Query 1: Registros de OTRAS sucursales (editado_nube = 0) -> comportamiento normal
   - Query 2: Registros de MISMA sucursal con editado_nube = 1 -> devuelve producto X
   - Despues de enviar, resetea editado_nube = 0

3. Sucursal A recibe producto X en pullTable():
   - Detecta que remoteSucursal === sucursal local
   - Usa id_origen como ID local directo (no busca en sync_id_map)
   - No remapea FKs (ya usan IDs locales)
   - Actualiza el registro local con los datos editados en nube

4. Sucursal B hace pull:
   - Query 1: NO incluye producto X de sucursal A (editado_nube ya es 0, y ademas es de otra sucursal)
   - Sucursal B NUNCA ve el cambio hecho en nube para sucursal A
```

### Cuando la sucursal hace push DESPUES de una edicion en nube

Si la sucursal A hace push y la nube tiene `editado_nube = 1` para un registro:
- El sync recibe el push y hace conflicto resolution (last-write-wins)
- Si la sucursal gana: se actualiza Y se resetea `editado_nube = 0`
- Si la nube gana: el registro queda con editado_nube = 1, esperando el proximo pull

### Migracion

Ejecutar `sync_migration_cloud_edits.sql` en la BD NUBE:
```sql
source /ruta/a/sync_migration_cloud_edits.sql
```

Esto:
1. Agrega columna `editado_nube` a las 48 tablas
2. Crea indices para busquedas rapidas
3. Crea triggers BEFORE UPDATE en cada tabla

### Para nuevas tablas

Al agregar una tabla al sistema de sync, tambien hay que:

1. Agregar `editado_nube TINYINT(1) NOT NULL DEFAULT 0` en la BD nube
2. Crear indice: `CREATE INDEX idx_editado_nube ON nueva_tabla (editado_nube, sucursal);`
3. Crear trigger:
```sql
DELIMITER //
DROP TRIGGER IF EXISTS trg_editado_nube_nueva_tabla//
CREATE TRIGGER trg_editado_nube_nueva_tabla BEFORE UPDATE ON `nueva_tabla`
FOR EACH ROW BEGIN
    IF @is_sync IS NULL OR @is_sync != 1 THEN
        IF NEW.sucursal != '' AND NEW.sucursal IS NOT NULL THEN
            SET NEW.editado_nube = 1;
        END IF;
    END IF;
END//
DELIMITER ;
```
