Concetti

ItemAdapter (NormalizedItem)

View-model normalizzato delle card. Disaccoppia ElementCard dalla forma del backend, eliminando alias di campo (priorita vs priority vs priorita_nome).

Perché esiste

Senza un adapter, il componente ElementCard dovrebbe sapere di:

  • naming italiano (titolo, priorita, etichette, assegnatario)
  • naming inglese (title, priority, tags, assignee)
  • naming flat-RPC (priorita_nome, priorita_colore, nome_assegnatario)

L'ItemAdapter traduce ogni item raw nel modello canonico NormalizedItem, eliminando la logica di alias dal componente. Il consumer fornisce un adapter; il default ne gestisce automaticamente le tre forme storiche.

Modello canonico

interface NormalizedItem {
  /** Identificatore univoco della scheda. */
  id: ItemId
  /** Identificatore della colonna/stato a cui appartiene. */
  columnId: KanbanColumnId
  /** Titolo principale visualizzato nella card. */
  title: string
  /** Descrizione/dettaglio. Vuoto o null = non visualizzato. */
  description?: string | null
  /** Priorità mappata. Undefined = nessuna priorità. */
  priority?: NormalizedPriority
  /** Tag/etichette. Sempre array (vuoto se assenti). */
  tags: NormalizedTag[]
  /** Persona/team assegnata. Undefined = nessun assegnatario. */
  assignee?: NormalizedAssignee
  /** Data di scadenza (string ISO, Date, o null). */
  dueDate?: string | Date | null
  /** Numero/codice della scheda (ticket #, codice CRM, ecc.). */
  number?: string | number
  /** Numero di commenti. 0 se nessuno. */
  commentCount: number
  /** Attività collegate. Sempre array. */
  activities: Array<Record<string, unknown>>
  /** Allegati. Sempre array. */
  attachments: Array<Record<string, unknown>>
  /** Item raw originale, utile per consumer che vogliono campi extra. */
  raw: Record<string, unknown>
}

interface NormalizedPriority { name: string; color?: string }
interface NormalizedTag { id: string | number; name: string; color?: string }
interface NormalizedAssignee { id?: string | number; name: string }
raw è sempre presente: l'item originale del backend resta accessibile nel NormalizedItem. I template card custom possono leggere campi extra direttamente da item.raw.miocampo senza estendere l'interfaccia.

Tipo ItemAdapter

type ItemAdapter<TItem = Record<string, unknown>> = (item: TItem) => NormalizedItem

Il generic TItem permette di tipizzare il backend (ItemAdapter<MyApiCard>); il default Record<string, unknown> accetta qualsiasi forma.

Adapter di default

defaultPostgRESTItemAdapter gestisce automaticamente le 3 forme storiche di @pzeta/vue-kanban:

FormaEsempio chiavi
Italiano (PostgREST diretto)titolo, descrizione, priorita, etichette, assegnatario, datascadenza, numero
Inglese (REST esterni)title, description, priority, tags, assignee, dueDate, number
Flat-RPC (vista denormalizzata)priorita_nome, priorita_colore, nome_assegnatario, id_assegnatario

Esposto come default su EnhancedKanbanBoard e KanbanFasiBoard. Non serve passarlo esplicitamente per backend PZeta standard.

Adapter custom

Per backend con shape diversa (es. CRM esterno con UUID e campi custom):

import type { ItemAdapter, NormalizedItem } from '@pzeta/vue-kanban'

interface MyApiCard {
  uuid: string
  status: string
  headline: string
  summary: string
  priority?: { label: string; hex: string }
  labels: Array<{ id: number; name: string; color: string }>
  owner?: { id: number; fullName: string }
  dueAt: string | null
  code: string
  commentsCount: number
  activities: unknown[]
  files: unknown[]
}

const myAdapter: ItemAdapter<MyApiCard> = (card) => ({
  id: card.uuid,
  columnId: card.status,
  title: card.headline,
  description: card.summary,
  priority: card.priority
    ? { name: card.priority.label, color: card.priority.hex }
    : undefined,
  tags: card.labels.map(l => ({ id: l.id, name: l.name, color: l.color })),
  assignee: card.owner ? { id: card.owner.id, name: card.owner.fullName } : undefined,
  dueDate: card.dueAt,
  number: card.code,
  commentCount: card.commentsCount,
  activities: card.activities,
  attachments: card.files,
  raw: card,
})

Iniezione nei componenti

Via prop

<EnhancedKanbanBoard
  board-id-or-code="X"
  :item-adapter="myAdapter"
/>

Via provide/inject (avanzato)

L'adapter è fornito ai discendenti tramite ITEM_ADAPTER_KEY. Componenti card custom posizionati nel sotto-albero possono recuperarlo:

<script setup lang="ts">
import { inject } from 'vue'
import { ITEM_ADAPTER_KEY, defaultPostgRESTItemAdapter } from '@pzeta/vue-kanban'

const adapter = inject(ITEM_ADAPTER_KEY, defaultPostgRESTItemAdapter)

const props = defineProps<{ rawItem: Record<string, unknown> }>()
const normalized = computed(() => adapter(props.rawItem))
</script>

<template>
  <div class="custom-card">
    <h4>{{ normalized.title }}</h4>
    <span v-if="normalized.priority" :style="{ color: normalized.priority.color }">
      {{ normalized.priority.name }}
    </span>
  </div>
</template>

Pattern: esporre campi extra dal backend

I campi non presenti nel modello canonico restano accessibili tramite raw:

<script setup lang="ts">
import type { NormalizedItem } from '@pzeta/vue-kanban'

const props = defineProps<{ item: NormalizedItem }>()

const valoreContratto = computed(() => props.item.raw.valore_contratto as number | undefined)
const cliente = computed(() => props.item.raw.cliente as { nome: string } | undefined)
</script>

<template>
  <div>
    <h4>{{ item.title }}</h4>
    <p v-if="cliente">Cliente: {{ cliente.nome }}</p>
    <p v-if="valoreContratto">€ {{ valoreContratto.toLocaleString('it-IT') }}</p>
  </div>
</template>

TypeScript

import {
  type ItemAdapter,
  type NormalizedItem,
  type NormalizedPriority,
  type NormalizedTag,
  type NormalizedAssignee,
  defaultPostgRESTItemAdapter,
  ITEM_ADAPTER_KEY,
} from '@pzeta/vue-kanban'