Mi familia tiene una finca en Ugarteche. Nogales viejos — algunos tienen más de 40 años. Por ende hay actividades que se repiten cada temporada: poda, cosecha, fumigación, riego, análisis de suelo.

Cuando empezamos a buscar software de gestión agrícola para llevar un registro, todos los sistemas que encontramos tenían el mismo problema: los cultivos estaban hardcodeados. Podías registrar maíz, soja, trigo, girasol. Si querías registrar nogales en producción desde hace cuatro décadas, el sistema no sabía cómo manejarlos. ¿En qué "etapa de crecimiento" está un nogal adulto? ¿Cuántas semanas faltan para la cosecha de un árbol que lleva 40 años produciendo?

Eso me llevó a construir CampoLog. Está en desarrollo activo — lo uso en la finca, pero todavía no es algo que recomendaría poner en producción sin conocer el código.

El modelo de datos que no asume nada

La decisión de diseño más importante es que los cultivos no tienen etapas. Una implantación (parcela + cultivo + fecha de inicio) existe o no existe. Las actividades son libres: vos definís los tipos de actividad para tu establecimiento.

El modelo central tiene tres capas:

Las reglas de inventario

Los tipos de actividad pueden tener reglas de movimiento de inventario asociadas: "cuando registro una fumigación, descuenta X litros del producto Y del inventario". Lo configurás una vez y después cada vez que registrás una fumigación, el movimiento se genera automáticamente.

-- Definición de la regla
INSERT INTO activity_type_rules (type_id, product_id, quantity_per_hectare, unit)
VALUES (
    (SELECT id FROM activity_types WHERE slug='fumigacion'),
    (SELECT id FROM products WHERE sku='HERB-001'),
    2.5,  -- litros por hectárea
    'L'
);

-- Trigger al registrar la actividad
-- El sistema calcula: hectáreas_de_la_parcela × 2.5 L = movimiento automático

El sistema nunca te dice "ese cultivo no está soportado". Si querés registrar que hoy hiciste análisis de suelo en los nogales del lote A y usaste 200 ml de solución para muestreo, lo registrás sin que el sistema te pregunte si el nogal es un cultivo anual o perenne.

La PWA de inventario para el campo

El módulo de inventario tiene una Progressive Web App separada (/campoLog/app/) diseñada para usarse desde el celular mientras recorrés la finca. El flow es simple: entrás, escaneás el código de barras del producto (o lo buscás por nombre), cargás el movimiento, y guardás. Si hay señal, se sincroniza en el momento. Si no hay señal, el movimiento se guarda en IndexedDB del navegador y se sincroniza automáticamente cuando volvés a zona de cobertura.

// Service worker: interceptar peticiones de sync
self.addEventListener('sync', function(event) {
    if (event.tag === 'inventory-sync') {
        event.waitUntil(syncPendingMovements());
    }
});

async function syncPendingMovements() {
    const pending = await db.getAll('pending_movements');
    for (const mov of pending) {
        const resp = await fetch('/campoLog/api/movements', {
            method: 'POST', body: JSON.stringify(mov)
        });
        if (resp.ok) await db.delete('pending_movements', mov.id);
    }
}

Las fotos y Backblaze B2

Las actividades pueden tener fotos adjuntas: el estado de una planta antes y después de la poda, evidencia de plaga detectada, foto del análisis de laboratorio. Las fotos se suben a Backblaze B2, no al hosting compartido, que tiene límites de almacenamiento molestos. Una foto típica son 100-200 KB; con 300 actividades fotodocumentadas, estamos en ~50 MB, dentro del plan gratuito de B2.

La URL de B2 se guarda en la base de datos, no el binario. El hosting sirve solo metadatos; B2 sirve las imágenes directamente.

El error que casi borra 6 meses de inventario

Durante una migración de esquema, un ALTER TABLE para agregar una columna falló silenciosamente en el hosting porque la tabla ya existía de una migración parcial abortada. El script asumía que si llegaba a ese punto era porque nunca se había ejecutado antes.

El resultado fue que los nuevos movimientos tenían el campo nuevo como NULL cuando deberían tener un valor. No borré datos, pero el historial quedó incompleto para 3 semanas de registros. Tuve que reconstruirlos manualmente a partir de los logs.

Ahora todas las migraciones son idempotentes: verifican primero si la columna existe antes de intentar crearla:

-- Migración idempotente
SELECT COUNT(*) FROM pragma_table_info('movements') WHERE name='batch_id';
-- Si devuelve 0, ejecutar ALTER TABLE; si devuelve 1, skip.

Aprendizaje caro pero definitivo. Toda migración que toca tablas con datos reales tiene que poder ejecutarse dos veces sin romper nada.

CampoLog está desplegado pero por ahora es de uso interno — acceso por login, repo privado. Si alguna vez lo libero, va a ser acá. Mientras tanto, si tenés el mismo problema o alguna idea, dejame un comentario.

El diseño multi-usuario

Aunque en la práctica lo uso yo solo, CampoLog fue diseñado desde el principio como multi-usuario. Cada registro de actividad, implantación y movimiento de inventario tiene un user_id. Las parcelas y los cultivos son compartidos entre todos los usuarios del establecimiento; las actividades son de quien las registra.

Esto abre la posibilidad de que el encargado del campo y el dueño vean el mismo sistema pero con vistas diferentes: el encargado registra actividades, el dueño ve el resumen y los reportes. Todavía no está implementado el sistema de roles, pero la estructura de datos lo soporta sin cambios de esquema.

La visión a largo plazo es que CampoLog pueda ser multi-tenant real: cada establecimiento agropecuario con su instancia de datos aislada. El stack actual (PHP + MySQL en hosting compartido) no escala a eso, pero el modelo de datos sí lo hace. Si algún día llega a ese punto, la migración al backend sería el trabajo, no el rediseño del modelo.