Getting Started

ERP via PostgREST

Configurazione del catalogo sorgenti ERP, query string PostgREST, filtri globali e specifici, refresh automatico, mapping cella destinazione.

Catalogo sorgenti

Una sorgente ERP è una vista PostgREST tipizzata registrata nella tabella foglidicalcolosorgenticatalogo:

INSERT INTO foglidicalcolo.foglidicalcolosorgenticatalogo (
  codice, nome, descrizione, endpoint, parametri, isattivo, ordinamento
) VALUES (
  'fatture_riepilogo',
  'Riepilogo fatture',
  'Fatture per periodo, tipologia operazione, cliente',
  '/v_fatture_riepilogo',
  '{
    "tipooperazione": { "tipo": "select", "label": "Tipo operazione",
      "options": [{"value":"V","label":"Vendita"}, {"value":"A","label":"Acquisto"}] },
    "periodo":        { "tipo": "daterange", "label": "Periodo", "required": false },
    "idcliente":      { "tipo": "integer", "label": "Cliente" }
  }'::jsonb,
  true,
  10
);

La libreria carica il catalogo via FogliPostgrestService.catalogo() la prima volta che il pannello ERP si apre (cache in useSpreadsheetERP.catalogo).

DataSourceConfig nel foglio

Ogni foglio dichiara N sorgenti attive in foglidicalcolo.sorgentierp (campo JSONB):

interface DataSourceConfig {
  codice: string                       // riferisce SorgenteCatalogo.codice
  cellaDestinazione: string            // es. "A1", "C5"
  sheetIndex: number                   // tab Fortune Sheet (0-based)
  filtri: Record<string, unknown>      // filtri specifici di questa istanza
  usaFiltriGlobali: boolean            // ereditare dai filtri globali del foglio?
  limitRighe?: number                  // default 100
}

Esempio:

const sorgenti: DataSourceConfig[] = [
  {
    codice: 'fatture_riepilogo',
    cellaDestinazione: 'A1',
    sheetIndex: 0,
    filtri: { tipooperazione: 'V' },
    usaFiltriGlobali: true,    // eredita periodo + idcliente
    limitRighe: 500,
  },
  {
    codice: 'ordini_riepilogo',
    cellaDestinazione: 'A1',
    sheetIndex: 1,             // tab "Ordini"
    filtri: {},
    usaFiltriGlobali: true,
    limitRighe: 500,
  },
]

Filtri globali

I GlobalFilters del foglio sono persistiti nel campo filtriglobali JSONB e applicati a tutte le sorgenti con usaFiltriGlobali: true.

interface GlobalFilters {
  periodo?: { da: string; a: string }   // ISO 8601, es. "2026-01-01"
  idcliente?: number | null
  idreparto?: number | null
  testoLibero?: string
  [key: string]: unknown                // estendibile
}

Il composable useGlobalFilters autosalva i cambi nel DB (debounced 1000ms) e useSpreadsheetERP fa refresh debounced 500ms quando i filtri cambiano.

Query string PostgREST generata

ErpDataService.fetchSorgente() costruisce la query string combinando filtri specifici e globali. Mapping:

Chiave inputOutput query string
dataDa (da periodo.da se usaFiltriGlobali)datadocumento=gte.{val}
dataA (da periodo.a)datadocumento=lte.{val}
altre chiavi{key}=eq.{val}
limitescluso dalla query string (usa header Range)
null / undefinedescluse

Esempio risolto:

config.filtri:        { tipooperazione: 'V' }
filtriGlobali:        { periodo: { da: '2026-01-01', a: '2026-12-31' }, idcliente: 42 }
usaFiltriGlobali:     true

GET /v_fatture_riepilogo?tipooperazione=eq.V&datadocumento=gte.2026-01-01&datadocumento=lte.2026-12-31&idcliente=eq.42
Headers: Range: 0-499, Prefer: count=exact

Paginazione e count

ErpDataService usa headers PostgREST per gestire paginazione e conteggio totale:

Range: 0-{limitRighe - 1}
Prefer: count=exact

Risposta:

Content-Range: 0-499/4231

totaleRighe = 4231, righe.length = 500. Il consumer (useSpreadsheetERP) memorizza il totale in ErpDataSource.totaleRighe.

Scrittura nelle celle

Dopo il fetch, useSpreadsheetERP chiama l'adapter per scrivere i risultati:

// pseudo-code interno
const range: CellValue[][] = righe.map((row) =>
  Object.values(row) as CellValue[]
)
adapter.setRange(startRow, startCol, range, sheetIndex)

cellaDestinazione (es. "C5") viene convertita in (row, col) zero-based.

Refresh

Tre meccanismi attivano un refresh:

TriggerCosa fa
refreshAll() esplicito (es. click toolbar)Fetch in parallelo di tutte le sorgenti
Cambio filtriGlobali (watcher reattivo)debouncedRefreshAll (500ms)
refreshInterval impostato (es. 60s)setInterval(refreshAll, interval * 1000)

In caso di errore, l'errore va in useSpreadsheetERP.errors[codice] e ErpDataSource.error. Le altre sorgenti continuano comunque.

Esempio end-to-end

<script setup lang="ts">
import { ref } from 'vue'
import type { ErpRefreshEvent, SpreadsheetSaveEvent } from '@pzeta/vue-foglidicalcolo'

const foglioId = ref<number>(42)

function onErpRefresh(event: ErpRefreshEvent) {
  console.log('Sorgenti ricaricate:', event.sources)
  console.log('Errori:', event.errors)
}

function onSave(event: SpreadsheetSaveEvent) {
  console.log(`Salvata rev. ${event.revisione}`)
}
</script>

<template>
  <SpreadsheetEditor
    :foglio-id="foglioId"
    height="700px"
    show-erp-panel
    show-global-filters
    :refresh-interval="60"
    @erp-refresh="onErpRefresh"
    @save="onSave"
  />
</template>

Funzioni ERP-native

In aggiunta al fetch via panel, l'adapter può registrare funzioni formula ERP nel motore Fortune Sheet:

=ERP.FATTURATO("V"; "2026-01-01"; "2026-12-31"; 42)
=ERP.ORDINI(42; "2026-01-01"; "2026-12-31")
=ERP.SCADENZE("2026-01-01"; "2026-01-31")

Vedi erp-functions. Le funzioni di default lanciano Not implemented — il consumer deve fornire l'implementazione concreta che chiama ErpDataService.