Approvazioni pendenti: il ruolo manager nell’app

approvazioni-pendenti-ferie-permessi-app-hr-manager

12 minuti di lettura

Una richiesta di ferie non termina quando il dipendente preme “Invia”. In molte organizzazioni la richiesta passa da un responsabile che deve leggerla, valutarla rispetto all’organizzazione del lavoro, e decidere se approvarla o rifiutarla.

Portare questo flusso su mobile non significa soltanto aggiungere due pulsanti colorati. Significa rappresentare una macchina a stati, applicare autorizzazioni, gestire paginazione e filtri, evitare doppie azioni, e lasciare traccia dell’esito.

Nel package feature-leaves, PendingApprovalsScreen usa usePendingApprovalsScreen, che a sua volta compone usePendingApprovals e collega le azioni a ApprovalCard.


Il problema delle richieste pendenti

Una lista manageriale deve rispondere rapidamente a tre domande: quali richieste devo gestire, quali informazioni mi servono per decidere, e che cosa è successo dopo la mia azione. La lista non dovrebbe mostrare richieste già concluse come se fossero ancora operative, e deve distinguere l’assenza di risultati da un errore di rete.

Il flusso può essere rappresentato così:

Manager apre la schermata
        ↓
Caricamento prima pagina
        ↓
Filtri per tipologia, reparto o mansione
        ↓
Lettura della richiesta
        ↓
Approva oppure rifiuta
        ↓
Aggiornamento stato e lista

Se uno stato viene aggiornato soltanto nella card locale, ma il server rifiuta l’operazione, il manager vede un risultato falso. L’azione deve quindi passare sempre dal contratto applicativo, mai limitarsi a un cambiamento visivo locale.


Modellare la macchina a stati

Il modello minimo è pending → approved oppure pending → rejected. Il backend può usare valori numerici, ma il dominio dovrebbe ragionare con nomi semantici:

const LEAVE_STATUS = {
  PENDING: 0,
  APPROVED: 1,
  REJECTED: 2,
};

I numeri sono esempi: il contratto reale resta la fonte autorevole. Ogni transizione dovrebbe definire lo stato di partenza, lo stato di arrivo, il ruolo autorizzato, un’eventuale motivazione obbligatoria, l’effetto sul contatore, e il messaggio mostrato all’utente. Un rifiuto può richiedere una motivazione, un’approvazione può aggiornare il saldo del dipendente: il cambio di stato non è quindi soltanto una proprietà visuale della card.


La schermata PendingApprovalsScreen

La schermata usa SafeAreaView, un header, una barra filtri e una FlatList:

Tre stati sono volutamente distinti tra loro: loading per quando la prima richiesta non è ancora terminata, error per quando la richiesta è fallita, e la lista vuota per quando la richiesta è riuscita ma non ci sono elementi. Questa distinzione evita di mostrare “nessuna richiesta” quando il server, in realtà, non è raggiungibile.

L’hook usePendingApprovalsScreen

L’hook della schermata aggiunge logica UI al data hook:

export function usePendingApprovalsScreen() {
  const data = usePendingApprovals();
  const { selectedFilters, setSelectedFilters, loadFirstPage, updateStatus } = data;

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

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

  return {
    ...data,
    activeFilter,
    tempFilters,
    handleApprove,
    handleReject,
  };
}

L’effetto ricarica la prima pagina quando cambiano i filtri applicati. Il data hook conserva invece caricamento, elementi, paginazione, opzioni e aggiornamento dello stato. Le callback di approvazione sono semplici, ma devono essere stabili e protette dal doppio tap:

const handleApprove = useCallback(
  async id => updateStatus(id, 1),
  [updateStatus],
);

const handleReject = useCallback(
  async id => updateStatus(id, 2),
  [updateStatus],
);

In una versione completa, ApprovalCard dovrebbe anche ricevere uno stato pendingAction per disabilitare il pulsante mentre la richiesta è in corso.


Filtri temporanei e filtri applicati

La schermata distingue lo stato già applicato da quello che l’utente sta modificando nel modal:

const openFilter = useCallback(
  filterName => {
    if (filterName === '__reset__') {
      setSelectedFilters({
        types: [],
        department_id: [],
        task_id: [],
        employee: '',
      });
      return;
    }

    setTempFilters(selectedFilters);
    setActiveFilter(filterName);
  },
  [selectedFilters, setSelectedFilters],
);

Il toggle modifica soltanto tempFilters:

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

Solo “Conferma” aggiorna selectedFilters, e attiva davvero una nuova richiesta. “Annulla” chiude semplicemente il modal, senza modificare la lista già mostrata. I filtri disponibili sono tipologia, reparto, mansione ed eventuale dipendente. Il conteggio visualizzato sui chip aiuta il manager a capire, con un colpo d’occhio, quanti criteri sono attualmente attivi.


Caricamento, paginazione e refresh

Una lista di approvazioni può crescere rapidamente. La schermata usa FlatList, e carica pagine successive quando l’utente raggiunge il fondo.

onEndReached deve essere protetto da hasMore e loadingMore, altrimenti possono partire richieste duplicate: React Native può invocare l’evento più volte quando ci si avvicina al fondo della lista. Il refresh deve riportare la lista alla prima pagina, o aggiornare coerentemente la pagina corrente, ed è utile conservare gli elementi precedenti durante un refresh fallito, mostrando un messaggio e un’azione “Riprova”. keyExtractor={item => String(item.id)} usa l’identificativo della richiesta come chiave: deve restare stabile, perché usare l’indice della lista può produrre card riutilizzate con contenuto sbagliato dopo un filtro o un aggiornamento.


Approvare e rifiutare una richiesta

ApprovalCard riceve le callback:

Dopo il tap, il flusso ideale è: disabilitare le azioni della card, inviare il cambio di stato, attendere la risposta del server, rimuovere o aggiornare l’elemento, e infine aggiornare i contatori mostrando un feedback. La strategia di aggiornamento dipende dal backend: si può rimuovere la richiesta dalla lista pendente dopo il successo, aggiornare la card, o ricaricare l’intera pagina. In tutti i casi, il fallimento deve ripristinare uno stato coerente — un’azione non dovrebbe sparire dalla lista prima della conferma server, salvo che l’interfaccia mostri chiaramente uno stato ottimistico e reversibile.

Contratto onUpdateLeaveStatus

Nel contratto applicativo:

onUpdateLeaveStatus: async (id, status) =>
  apiClient.post('/giustificativi/modifica_stato', {
    id,
    status: Number(status),
  });

Il package conosce l’intento — aggiornare lo stato della richiesta — mentre la shell conosce l’endpoint e il formato HTTP. L’adapter è anche il punto giusto per convertire status semantici in numeri, normalizzare la risposta, tradurre errori tecnici, inserire header e autenticazione, e aggiornare la sincronizzazione dei contatori. Il package, in nessun caso, deve costruire direttamente l’URL /giustificativi/modifica_stato.


Autorizzazioni e concorrenza

Nascondere i pulsanti in base al ruolo non è sufficiente: il backend deve verificare che il manager possa davvero modificare quella richiesta. Vanno gestiti anche i casi concorrenti: un altro manager ha già approvato, la richiesta è stata ritirata dal dipendente, il saldo è cambiato nel frattempo, la lista mostrata è ormai vecchia, oppure token o sessione non sono più validi. In caso di conflitto, la UI dovrebbe informare l’utente e ricaricare la richiesta — non dovrebbe mai mostrare “approvata” solo perché il tap è andato a buon fine a livello locale.

Un doppio tap o un retry di rete può ripetere la stessa azione: il backend dovrebbe trattare in modo sicuro una transizione già applicata, mentre il client deve disabilitare le azioni durante la Promise in corso.


Errori comuni

Usare la stessa UI per lista vuota ed errore confonde due situazioni diverse: la prima è un risultato valido, la seconda richiede un retry. Aggiornare la card prima del server lascia il client in uno stato falso in caso di rifiuto o conflitto. Non distinguere loading iniziale e caricamento successivo produce un loader a schermo intero durante loadMore, interrompendo inutilmente il lavoro del manager. Applicare i filtri a ogni tap aumenta le richieste, e rende difficile annullare una selezione multipla. Proteggere soltanto i pulsanti nel client lascia l’autorizzazione priva del controllo che invece deve avvenire lato backend. Non registrare la motivazione del rifiuto, infine, è un problema quando la procedura la richiede: deve diventare parte del contratto e dello storico.


Domande frequenti

Il manager può approvare dal telefono?

Solo se il ruolo e il processo aziendale lo prevedono. Il backend deve verificare l’autorizzazione in ogni caso.

Perché non ricaricare tutta la lista dopo ogni azione?

È una soluzione semplice e affidabile. Per dataset grandi si può aggiornare localmente e sincronizzare, ma bisogna gestire con cura errori e concorrenza.

I filtri devono essere server-side?

Per liste piccole può bastare il client. Per approvazioni numerose è preferibile filtrare e paginare direttamente sul server.

Che cosa mostrare dopo un rifiuto?

Lo stato aggiornato e, quando previsto, una motivazione leggibile. I dettagli tecnici dell’errore non aiutano il manager a capire cosa fare.

Conclusione

PendingApprovalsScreen è un flusso gestionale, non una semplice lista. Deve caricare richieste, applicare filtri, supportare paginazione, eseguire azioni autorizzate, e rappresentare transizioni di stato confermate dal server.

La qualità della schermata dipende dal contratto e dal modello di dominio tanto quanto dal componente visuale. Quando questi elementi sono espliciti, anche il ruolo del manager diventa più chiaro, e il rischio di aggiornamenti incoerenti diminuisce.

Nel prossimo articolo scoprirai come abbiamo gestito i filtri avanzati.