Archivio richieste con filtri avanzati

filtri-avanzati-app-hr

12 minuti di lettura

Un archivio richieste non è una lista infinita da scorrere. È uno strumento per rispondere a domande precise: quali richieste sono state inviate in un periodo? Quali risultano rifiutate? Quante assenze di una certa tipologia riguardano un reparto specifico?Se l’archivio non consente di restringere la ricerca, il dato esiste ma diventa difficile da usare. Se invece applica filtri senza mostrare quali criteri sono attivi, l’utente non sa perché sta vedendo proprio quei risultati.
LeavesArchiveScreen affronta il problema combinando quattro filtri, un periodo, paginazione e stati di caricamento. La logica è distribuita tra schermata, useLeavesArchiveScreen, useLeavesArchive, FilterModal e PeriodFilterModal.

Che cosa deve risolvere un archivio

Un archivio efficace deve rendere visibili tre informazioni: quali elementi sono stati recuperati, con quali filtri, e se esistono altri risultati oltre quelli mostrati. La schermata deve inoltre distinguere con chiarezza il loading iniziale, i risultati o la lista vuota, la paginazione o il refresh, e un errore recuperabile.

Una lista vuota dopo una ricerca valida non è un errore. Un errore di rete non è una lista vuota. Confondere i due casi porta l’utente a modificare i filtri quando dovrebbe soltanto riprovare. Per un archivio HR sono importanti anche un identificativo stabile, lo stato della richiesta, il dipendente, la tipologia e il periodo: la card dovrebbe mostrare abbastanza contesto da evitare l’apertura del dettaglio per ogni singolo elemento.


I filtri disponibili

La schermata espone quattro aree. La tipologia permette di selezionare una o più causali, come ferie, permessi o altre assenze configurate. Il reparto consente di restringere la ricerca a una struttura organizzativa. Lo stato filtra richieste pendenti, approvate, rifiutate o altri stati previsti dal contratto. Il periodo, infine, usa un intervallo con data iniziale e finale.

I filtri discreti possono essere rappresentati come array di identificativi:

const selectedFilters = {
  types: [],
  department_id: [],
  status: [],
};

Il periodo vive separatamente:

const dateRange = {
  start: '2026-08-01',
  end: '2026-08-31',
};

Separare i due concetti evita di confondere una lista di opzioni con un intervallo temporale, che ha regole di validazione diverse.


La struttura di LeavesArchiveScreen

La schermata riceve dal proprio hook tutto ciò che serve al rendering:

const {
  loading,
  loadingMore,
  refreshing,
  items,
  hasMore,
  error,
  filterOptions,
  selectedFilters,
  dateRange,
  isViewingToday,
  loadFirstPage,
  loadMore,
  onRefresh,
  activeFilter,
  tempFilters,
  openFilter,
  toggleTempSelection,
  applyFilters,
  cancelFilters,
  handleChangeRange,
  handleChangeStatus,
  resetFiltri,
} = useLeavesArchiveScreen();

Il rendering costruisce header, barra dei filtri, lista e modal:

La screen non costruisce l’URL, e non decide come trasformare i filtri in query: queste responsabilità appartengono all’hook e al contratto applicativo, non al componente di presentazione.


useLeavesArchiveScreen e lo stato della UI

L’hook compone useLeavesArchive, e aggiunge la gestione del filtro attivo:

export function useLeavesArchiveScreen() {
  const archive = useLeavesArchive();
  const {
    selectedFilters,
    dateRange,
    setDateRange,
    loadFirstPage,
    updateStatus,
    resetFiltri,
  } = archive;

  const [activeFilter, setActiveFilter] = useState(null);
  const [tempFilters, setTempFilters] = useState(selectedFilters);

  useEffect(() => {
    loadFirstPage();
  }, [selectedFilters, dateRange]);

  return {
    ...archive,
    activeFilter,
    tempFilters,
  };
}

Quando cambia un filtro applicato o il periodo, viene caricata la prima pagina. È importante che il data hook resetti anche pagina corrente e risultati precedenti — altrimenti la lista può contenere elementi della query precedente, mischiati a quelli nuovi.

isViewingToday modifica il contesto della UI: un intervallo limitato alla giornata corrente può mostrare “Assenti oggi”, mentre un intervallo storico mostra “Archivio assenze”. Titolo e stato vuoto devono restare coerenti con questo contesto specifico.


FilterModal e selezione temporanea

FilterModal gestisce selezioni multiple senza applicarle immediatamente. Quando si apre un filtro:

const openFilter = filterName => {
  if (filterName === '__reset__') {
    resetFiltri();
    return;
  }

  setTempFilters(selectedFilters);
  setActiveFilter(filterName);
};

Il toggle aggiorna la chiave corrispondente:

const key =
  activeFilter === 'Tipologia'
    ? 'types'
    : activeFilter === 'Reparto'
      ? 'department_id'
      : 'status';

setTempFilters(prev => ({
  ...prev,
  [key]: prev[key].includes(option.id)
    ? prev[key].filter(id => id !== option.id)
    : [...prev[key], option.id],
}));

Solo con applyFilters lo stato temporaneo diventa quello effettivamente applicato:

const applyFilters = useCallback(() => {
  archive.setSelectedFilters(tempFilters);
  setActiveFilter(null);
}, [archive, tempFilters]);

Con cancelFilters, la lista resta invariata e il modal viene semplicemente chiuso. La barra filtri dovrebbe mostrare il numero di selezioni attive, per esempio “Tipologia (2) · Reparto · Stato (1) · 01/08–31/08 · Resetta”. Un pulsante di reset permette di tornare rapidamente alla ricerca predefinita, e deve cancellare sia i filtri discreti sia il periodo, non soltanto i chip visibili in quel momento.


PeriodFilterModal e intervalli di date

Il periodo richiede un componente dedicato, perché non è una semplice lista di opzioni. PeriodFilterModal mantiene un intervallo locale, e comunica il risultato quando entrambe le date sono presenti:

La selezione tipica segue una sequenza di regole: se non esiste un inizio, il giorno diventa start; se l’intervallo è già completo, una nuova selezione ne inizia uno nuovo; se la seconda data è precedente alla prima, le date vengono invertite automaticamente; altrimenti la seconda selezione diventa semplicemente end. Il pulsante “Applica” resta disabilitato finché l’intervallo non è completo.

Il contratto dovrebbe usare un formato stabile, per esempio YYYY-MM-DD, e non bisogna mai inviare al backend la stringa formattata per l’utente, come 01/08/26: la localizzazione appartiene alla UI, il contratto deve ricevere un formato non ambiguo. Preset come “oggi”, “questa settimana” o “questo mese” possono ridurre i tap, ma devono produrre lo stesso oggetto del calendario, e passare dalle stesse validazioni.


Costruire il fetch parametrizzato

Il data hook dovrebbe costruire una query esplicita:

const params = {
  page,
  types: selectedFilters.types,
  department_id: selectedFilters.department_id,
  status: selectedFilters.status,
  start_date: dateRange.start,
  end_date: dateRange.end,
};

const result = await onGetLeaves(params);

La convenzione per i valori vuoti deve essere condivisa con il backend — array vuoto, proprietà assente, stringa vuota o null — e mescolare convenzioni diverse tra i filtri può generare query differenti da quelle attese. Il contratto dovrebbe inoltre definire nome dei campi, pagina iniziale, dimensione pagina e forma della risposta.

La schermata non dovrebbe dedurre hasMore semplicemente contando gli elementi, se il backend fornisce già un’informazione autorevole a riguardo.


Paginazione, refresh e stati

La lista distingue quattro caricamenti: loading per la prima pagina, loadingMore per la pagina successiva, refreshing per l’aggiornamento manuale, e l’errore. Il footer mostra un indicatore soltanto durante loadingMore, evitando di sostituire l’intera lista:

const renderFooter = () =>
  loadingMore ? (
    <View style={styles.footer}>
      <ActivityIndicator size="small" color={colors.brand} />
    </View>
  ) : null;

onEndReached deve essere disabilitato quando non ci sono altre pagine, ed evitare richieste parallele mentre una pagina è già in corso. Se il refresh fallisce, l’ultima lista valida può restare visibile: l’errore va mostrato in modo non distruttivo, con un’azione di retry — cancellare i risultati perché una richiesta successiva è fallita peggiora soltanto l’esperienza.

L’empty state deve spiegare il contesto: “Nessuno assente oggi” quando si guarda la giornata corrente, “Nessun risultato” quando i filtri non restituiscono elementi. Il testo può suggerire di modificare i filtri, non di controllare la connessione se la richiesta è comunque riuscita.


Aggiornare lo stato di una richiesta

L’archivio può permettere un’azione sullo stato tramite handleChangeStatus:

const handleChangeStatus = useCallback(
  async (id, newStatus) => {
    await updateStatus(id, newStatus);
  },
  [updateStatus],
);

Anche qui la card non deve modificare soltanto il proprio stato locale: l’azione deve raggiungere il contratto, e ottenere conferma dal backend. Dopo il successo si può aggiornare la card, ricaricare la pagina corrente, rimuovere l’elemento se non appartiene più al filtro, o sincronizzare contatori e altre liste — la strategia dipende dalla risposta del server e dal significato specifico dell’azione.


Errori comuni

Filtrare solo sul client funziona per dataset piccoli, ma non per archivi aziendali grandi o paginati. Applicare i filtri a ogni tap aumenta le richieste, e rende difficile annullare una selezione multipla. Usare date localizzate nella query è ambiguo: 01/08/2026 dipende dalla convenzione del server, e va sempre evitato. Non azzerare la pagina a ogni nuova query rischia di saltare o duplicare risultati. Confondere empty ed errore lascia l’utente senza sapere se la richiesta è riuscita senza risultati, o se non è riuscita affatto. Permettere paginazione concorrente, con più loadMore simultanei, può duplicare elementi o sovrascrivere la pagina corrente. Nascondere i filtri attivi, infine, impedisce all’utente di capire il motivo del risultato che sta guardando.


Domande frequenti

Posso applicare i filtri solo sul client?

Solo se il dataset è piccolo e già disponibile. Per archivi aziendali è preferibile filtrare e paginare sul server.

Perché separare FilterModal e PeriodFilterModal?

Il primo gestisce opzioni discrete; il secondo intervalli, calendario e validazioni temporali, che hanno regole molto diverse.

Come gestire un refresh fallito?

Conservando l’ultima lista valida, mostrando l’errore, e offrendo un pulsante “Riprova” ben visibile.

Come gestire il cambio periodo?

Aggiornando il range normalizzato, azzerando pagina e risultati precedenti, e richiamando il fetch con i nuovi parametri.

Perché usare una chiave stabile?

Per evitare che React Native riutilizzi una card con dati sbagliati quando lista e filtri cambiano nel tempo.

Conclusione

Un archivio avanzato è, in fondo, un piccolo motore di ricerca: filtri coerenti, periodo normalizzato, query parametrizzata, paginazione e stati distinti. LeavesArchiveScreen mantiene il rendering, useLeavesArchiveScreen orchestra i cambiamenti, e il contratto applicativo gestisce il backend.

Quando il filtro attivo è visibile, e ogni stato è esplicito, l’utente non deve indovinare che cosa sta guardando. È questo il passaggio che trasforma una lista di richieste in uno strumento davvero operativo.