Composables

useSpreadsheet

Composable principale dell'editor. Gestisce load/save snapshot, lock pessimistico con heartbeat, autosave, undo/redo e cella API.

Signature

function useSpreadsheet(options: UseSpreadsheetOptions): UseSpreadsheetReturn

Composable centrale dell'editor. Orchestra:

  • carica/salva il foglio (metadati + snapshot)
  • acquisisce/rilascia/heartbeat del lock pessimistico
  • gestisce FortuneSheetAdapter per interazione con il workbook
  • traccia stato dirty + autosave opzionale
  • espone API celle (getCellValue/setCellValue)

Opzioni

interface UseSpreadsheetOptions {
  foglioId: Ref<number | null>
  mode: Ref<'edit' | 'view'>
  autosaveInterval?: Ref<number | null>      // secondi
  postgrestUrlOverride?: Ref<string | null>  // override URL puntuale
  onSave?: (event: SpreadsheetSaveEvent) => void
  onError?: (event: SpreadsheetErrorEvent) => void
}
OpzioneTipoDescrizione
foglioIdRef<number | null>ID foglio reattivo. Cambio → load() automatico
modeRef<'edit' | 'view'>Modalità reattiva. view forza read-only
autosaveIntervalRef<number | null>Secondi. Se valorizzato, setInterval(save, n*1000)
postgrestUrlOverrideRef<string | null>Override URL PostgREST (raramente usato)
onSavecallbackNotifica salvataggio riuscito
onErrorcallbackNotifica errori (LOAD/SAVE/LOCK/ACL/...)

Risultato

interface UseSpreadsheetReturn {
  state: Ref<FoglioState>
  snapshot: Ref<FortuneSheetSnapshot | null>
  isSaving: Ref<boolean>
  isLoading: Ref<boolean>
  isDirty: Ref<boolean>
  error: Ref<SpreadsheetErrorEvent | null>
  lockInfo: Ref<LockInfo | null>
  isReadOnly: ComputedRef<boolean>
  canSave: ComputedRef<boolean>
  isLockOwner: ComputedRef<boolean>

  load(idfolio: number): Promise<void>
  save(): Promise<SpreadsheetSaveEvent>
  markDirty(): void
  setSnapshot(snapshot: FortuneSheetSnapshot): void
  acquisisciLock(): Promise<AcquisisciLockResult>
  rilasciaLock(): Promise<void>
  exportXlsx(filename?: string): Promise<void>
  undo(): void
  redo(): void
  getCellValue(row: number, col: number, sheet?: number): CellValue
  setCellValue(row: number, col: number, value: CellValue, sheet?: number): void

  adapter: FortuneSheetAdapter
}

Computed

isReadOnly = computed(() =>
  mode.value === 'view' || state.permessoEffettivo === 'lettura'
)

canSave = computed(() =>
  isDirty.value && !isReadOnly.value && !isSaving.value
)

isLockOwner = computed(() =>
  lockInfo.value !== null && !lockInfo.value.islocked
)
isLockOwner è true quando lockInfo è valorizzato ma islocked === false — significa "noi abbiamo il lock attivo, nessun altro lo blocca". È un'invariante del DB: chi possiede il lock vede islocked === false, chi non lo possiede vede islocked === true.

Lifecycle interno

new useSpreadsheet({ foglioId, mode, ... })
   ├─ Istanzia: FogliPostgrestService + LockService + FortuneSheetAdapter
   └─ Inizializza state, refs e watchers

load(idfolio)
   ├─ state.isLoading = true
   ├─ FogliPostgrestService.caricaFoglio(idfolio)
   │   → metadati: nome, permessoEffettivo, filtriGlobali, sorgentierp, lock
   ├─ FogliPostgrestService.caricaUltimaVersione(idfolio)
   │   → ultimo snapshot + numerorevisione
   ├─ adapter.loadSnapshot(snapshot)
   ├─ Se mode='edit' && permesso !== 'lettura':
   │   ├─ LockService.acquisisciLock(idfolio)
   │   │   ├─ successo: lockInfo aggiornato, heartbeat avviato
   │   │   └─ LOCK_OCCUPIED: state.isReadOnly forzato (UI)
   │   └─ Avvia heartbeatInterval (5 min) → LockService.rinnovaLock
   ├─ Se autosaveInterval valorizzato: setInterval(save, n*1000)
   └─ state.isLoading = false

save()
   ├─ state.isSaving = true
   ├─ snapshot = adapter.getSnapshot()
   ├─ numerorevisione = (state.ultimaRevisione ?? 0) + 1
   ├─ FogliPostgrestService.salvaVersione({ idfolio, numerorevisione, snapshot })
   ├─ state.ultimaRevisione = numerorevisione
   ├─ state.isDirty = false
   ├─ state.lastSavedAt = new Date()
   └─ onSave?({ idfolio, nome, revisione, timestamp })

onUnmounted (gestito esternamente da SpreadsheetEditor)
   ├─ clearInterval(heartbeat)
   ├─ clearInterval(autosave)
   └─ rilasciaLock() (se isLockOwner)

Lock pessimistico

const result = await spreadsheet.acquisisciLock()
if (result.successo === false && result.errore === 'LOCK_OCCUPIED') {
  console.warn('Foglio bloccato da altro utente:', result)
}

Vedi LockService per dettagli sulle RPC.

Autosave

Se autosaveInterval.value === 60:

setInterval(() => {
  if (canSave.value) await save()
}, 60_000)

L'autosave non chiama save() se isDirty === false, isSaving === true, o isReadOnly === true.

API celle

spreadsheet.getCellValue(0, 0, 0) // → CellValue (string | number | boolean | null)
spreadsheet.setCellValue(0, 0, 'Ciao', 0)
spreadsheet.setCellValue(0, 1, 42)

Wrapper dell'adapter, comodi quando vuoi scriptare il foglio da fuori.

Undo/Redo

spreadsheet.undo()  // adapter.undo()
spreadsheet.redo()  // adapter.redo()

L'adapter espone anche canUndo() / canRedo() per disabilitare UI. Se vuoi usarli, accedi direttamente all'adapter:

if (spreadsheet.adapter.canUndo()) {
  spreadsheet.undo()
}

markDirty() e onChange

L'adapter.onChange() è registrato esternamente (da SpreadsheetEditor) e chiama spreadsheet.markDirty() quando cambia una cella. Da quel momento isDirty.value === true finché save() non lo riporta a false.

Esempio: editor minimale custom

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useSpreadsheet } from '@pzeta/vue-foglidicalcolo'

const props = defineProps<{ foglioId: number }>()
const container = ref<HTMLElement | null>(null)

const foglioId = ref<number | null>(props.foglioId)
const mode = ref<'edit' | 'view'>('edit')

const spreadsheet = useSpreadsheet({
  foglioId,
  mode,
  autosaveInterval: ref(60),
  onSave: (e) => console.log('saved rev.', e.revisione),
  onError: (e) => alert(`Errore: ${e.message}`),
})

onMounted(async () => {
  if (container.value) {
    await spreadsheet.adapter.initialize(container.value)
    spreadsheet.adapter.onChange(() => spreadsheet.markDirty())
    await spreadsheet.load(props.foglioId)
  }
})

onUnmounted(() => {
  spreadsheet.adapter.offChange()
  spreadsheet.adapter.destroy()
})
</script>

<template>
  <div ref="container" style="height: 600px; width: 100%" />
  <button :disabled="!spreadsheet.canSave.value" @click="spreadsheet.save()">
    Salva
  </button>
</template>

Tipi correlati