Costruire e Distribuire un’App HR Utilizzando Refine

Introduzione

In questo tutorial, costruiremo un’applicazione di gestione HR con il Refine Framework e la deployeremo sulla DigitalOcean App Platform.

Alla fine di questo tutorial, avremo un’applicazione di gestione HR che include:

  • Pagina di accesso: Consente agli utenti di accedere come manager o dipendente. I manager hanno accesso alle pagine Time Off e Requests, mentre i dipendenti hanno accesso solo alla pagina Time Off.
  • Pagine Time Off: Permette ai dipendenti di richiedere, visualizzare e annullare il loro tempo libero. Inoltre, i manager possono assegnare nuovi permessi di assenza.
  • Pagina delle richieste: Accessibile solo ai manager HR per approvare o rifiutare le richieste di assenza.

Nota: Puoi ottenere il codice sorgente completo dell’app che costruiremo in questo tutorial da questo repository GitHub

Durante queste operazioni, utilizzeremo:

  • API REST: Per recuperare e aggiornare i dati. Refine ha pacchetti provider di dati e API REST integrati, ma è anche possibile crearne uno proprio per soddisfare requisiti specifici. In questa guida, utilizzeremo NestJs CRUD come servizio backend e il pacchetto @refinedev/nestjsx-crud come provider di dati.
  • Material UI: Lo utilizzeremo per i componenti dell’interfaccia utente e lo personalizzeremo completamente secondo il nostro design. Refine supporta nativamente Material UI, ma è possibile utilizzare qualsiasi libreria UI preferita.

Una volta completata l’applicazione, la metteremo online utilizzando App Platform di DigitalOcean, che semplifica la configurazione, il lancio e la crescita di app e siti web statici. È possibile distribuire il codice semplicemente puntando a un repository GitHub e lasciando che App Platform si occupi della gestione dell’infrastruttura, degli ambienti di esecuzione dell’app e delle dipendenze.

Prerequisiti

Cos’è Refine?

Refine è un meta-framework React open source per costruire complesse applicazioni web B2B, principalmente casi d’uso focalizzati sulla gestione dei dati come strumenti interni, pannelli di amministrazione e dashboard. È progettato fornendo un insieme di hook e componenti per migliorare il processo di sviluppo con un flusso di lavoro migliore per lo sviluppatore.

Fornisce funzionalità complete e pronte per la produzione per applicazioni a livello enterprise per semplificare compiti a pagamento come la gestione dello stato e dei dati, l’autenticazione e il controllo degli accessi. Questo consente agli sviluppatori di rimanere concentrati sul nucleo della loro applicazione in un modo che è astratto da molti dettagli di implementazione opprimenti.

Passo 1 — Impostare il progetto

Utilizzeremo il comando npm create refine-app per inizializzare interattivamente il progetto.

npm create refine-app@latest

Seleziona le seguenti opzioni quando richiesto:

✔ Choose a project template · Vite
✔ What would you like to name your project?: · hr-app
✔ Choose your backend service to connect: · nestjsx-crud
✔ Do you want to use a UI Framework?: · Material UI
✔ Do you want to add example pages?: · No
✔ Do you need any Authentication logic?: · None
✔ Choose a package manager: · npm

Una volta completata la configurazione, naviga nella cartella del progetto e avvia la tua app con:

npm run dev

Apri http://localhost:5173 nel tuo browser per vedere l’app.

Preparazione del Progetto

Ora che abbiamo impostato il nostro progetto, apportiamo alcune modifiche alla struttura del progetto e rimuoviamo i file non necessari.

Per prima cosa, installa le dipendenze di terze parti:

  • @mui/x-date-pickers, @mui/x-date-pickers-pro: Questi sono componenti per la selezione delle date per Material UI. Li useremo per selezionare l’intervallo di date per le richieste di tempo libero.
  • react-hot-toast: Una libreria di toast minimalista per React. La useremo per mostrare messaggi di successo e di errore.
  • react-infinite-scroll-component: Un componente React per rendere facile lo scroll infinito. Lo useremo per caricare ulteriori richieste di tempo libero mentre l’utente scorre verso il basso per visualizzare più richieste.
  • dayjs: Una libreria di date leggera per analizzare, convalidare, manipolare e formattare le date.
  • vite-tsconfig-paths: Un plugin Vite che ti consente di utilizzare alias di percorso TypeScript nel tuo progetto Vite.
npm install @mui/x-date-pickers @mui/x-date-pickers-pro dayjs react-hot-toast react-infinite-scroll-component
npm install --save-dev vite-tsconfig-paths

Dopo aver installato le dipendenze, aggiorna vite.config.ts e tsconfig.json per utilizzare il plugin vite-tsconfig-paths. Questo abilita gli alias di percorso TypeScript nei progetti Vite, consentendo importazioni con l’alias @.

import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [tsconfigPaths({ root: __dirname }), react()],
});
{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "allowJs": false,
    "skipLibCheck": true,
    "esModuleInterop": false,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "baseUrl": "./src",
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

Successivamente, rimuoviamo i file e le cartelle non necessari:

  • src/contexts: Questa cartella contiene un singolo file che è ColorModeContext. Gestisce la modalità scura/chiara per l’app. Non lo utilizzeremo in questo tutorial.
  • src/components: Questa cartella contiene il componente <Header />. Utilizzeremo un componente intestazione personalizzato in questo tutorial.
rm -rf src/contexts src/components

Dopo aver rimosso i file e le cartelle, App.tsx genera un errore che risolveremo nei prossimi passaggi.
Durante il tutorial, copriremo la codifica delle pagine e dei componenti principali. Quindi, ottieni i file e le cartelle necessari dal repository GitHub. Con questi file, avremo una struttura di base per la nostra applicazione di gestione HR.

  • icone: Cartella icone contenente tutte le icone dell’app.
  • tipi:
  • utilità:
    • constants.ts: Costanti dell’app.
    • axios.ts: Istanza di Axios per richieste API, gestione dei token di accesso, token di aggiornamento e degli errori.
    • init-dayjs.ts: Inizializza Day.js con i plugin necessari.
  • fornitori:
    • access-control: Gestisce i permessi degli utenti utilizzando accessControlProvider; controlla la visibilità della pagina Requests in base al ruolo dell’utente.
    • auth-provider: Gestisce l’autenticazione con authProvider; garantisce che tutte le pagine siano protette e richiedano il login.
    • notification-provider: Visualizza messaggi di successo e di errore tramite react-hot-toast.
    • query-client: Client per query personalizzato per un controllo e una personalizzazione completi.
    • theme-provider: Gestisce il tema di Material UI.
  • componenti:
    • layout: Componenti di layout.
    • loading-overlay: Mostra un overlay di caricamento durante il recupero dei dati.
    • input: Rende i campi di input del modulo.
    • frame: Componente personalizzato che aggiunge bordi, titoli e icone alle sezioni della pagina.
    • modal: Componente di dialogo modale personalizzato.

Dopo aver copiato i file e le cartelle, la struttura dei file dovrebbe apparire così:

└── 📁src
    └── 📁components
        └── 📁frame
        └── 📁input
        └── 📁layout
            └── 📁header
            └── 📁page-header
            └── 📁sider
        └── 📁loading-overlay
        └── 📁modal
    └── 📁icons
    └── 📁providers
        └── 📁access-control
        └── 📁auth-provider
        └── 📁notification-provider
        └── 📁query-client
        └── 📁theme-provider
    └── 📁types
    └── 📁utilities
    └── App.tsx
    └── index.tsx
    └── vite-env.d.ts

Successivamente, aggiorna il file App.tsx per includere i provider e i componenti necessari.

src/App.tsx
import { Refine } from '@refinedev/core'
import { DevtoolsProvider, DevtoolsPanel } from '@refinedev/devtools'
import dataProvider from '@refinedev/nestjsx-crud'
import routerProvider, { UnsavedChangesNotifier, DocumentTitleHandler } from '@refinedev/react-router-v6'
import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom'
import { Toaster } from 'react-hot-toast'

import { Layout } from '@/components/layout'

import { ThemeProvider } from '@/providers/theme-provider'
import { authProvider } from '@/providers/auth-provider'
import { accessControlProvider } from '@/providers/access-control'
import { useNotificationProvider } from '@/providers/notification-provider'
import { queryClient } from '@/providers/query-client'

import { BASE_URL } from '@/utilities/constants'
import { axiosInstance } from '@/utilities/axios'

import { Role } from './types'

import '@/utilities/init-dayjs'

function App() {
  return (
    <BrowserRouter>
      <ThemeProvider>
        <DevtoolsProvider>
          <Refine
            authProvider={authProvider}
            routerProvider={routerProvider}
            dataProvider={dataProvider(BASE_URL, axiosInstance)}
            notificationProvider={useNotificationProvider}
            resources={[
              {
                name: 'employee',
                meta: {
                  scope: Role.EMPLOYEE,
                },
              },
              {
                name: 'manager',
                meta: {
                  scope: Role.MANAGER,
                },
              },
            ]}
            accessControlProvider={accessControlProvider}
            options={{
              reactQuery: {
                clientConfig: queryClient,
              },
              syncWithLocation: true,
              warnWhenUnsavedChanges: true,
              useNewQueryKeys: true,
            }}>
            <Routes>
              <Route
                element={
                  <Layout>
                    <Outlet />
                  </Layout>
                }>
                <Route index element={<h1>Hello World</h1>} />
              </Route>
            </Routes>
            <UnsavedChangesNotifier />
            <DocumentTitleHandler />
            <Toaster position='bottom-right' reverseOrder={false} />
            <DevtoolsPanel />
          </Refine>
        </DevtoolsProvider>
      </ThemeProvider>
    </BrowserRouter>
  )
}

export default App


Analizziamo le modifiche importanti che abbiamo apportato al file App.tsx:

  • <Refine />: Il componente principale di @refinedev/core che avvolge l’intera applicazione per fornire recupero dati, gestione dello stato e altre funzionalità.
  • <DevtoolsProvider /> e <DevtoolsPanel />: Utilizzati per scopi di debug e sviluppo.
  • <ThemeProvider />: Applica un tema personalizzato all’intera app.
  • Inizializzazione di Day.js: Per la manipolazione di date e orari.
  • risorse: Un array che specifica le entità dati (employee e manager) che Refine recupererà. Utilizziamo risorse padre e figlio per organizzare i dati e gestire i permessi. Ogni risorsa ha un scope che definisce il ruolo dell’utente, il quale controlla l’accesso a diverse parti dell’app.
  • queryClient: Un client di query personalizzato per il pieno controllo e la personalizzazione del recupero dei dati.
  • syncWithLocation: Abilita la sincronizzazione dello stato dell’app (filtri, ordinamenti, paginazione, ecc.) con l’URL.
  • warnWhenUnsavedChanges: Mostra un avviso quando l’utente tenta di navigare lontano da una pagina con modifiche non salvate.
  • <Layout />: Un componente di layout personalizzato che avvolge il contenuto dell’app. Contiene l’intestazione, la barra laterale e l’area principale del contenuto. Spiegheremo questo componente nei prossimi passaggi.

Ora siamo pronti per iniziare a costruire l’applicazione di gestione HR.


Passo 2— Personalizzazione e stile

Guarda più da vicino il theme-provider. Abbiamo personalizzato ampiamente il tema di Material UI per adattarlo al design dell’app di gestione HR, creando due temi, uno per i manager e uno per i dipendenti, per differenziarli con colori diversi.

Inoltre, abbiamo aggiunto Inter come font personalizzato per l’app. Per installarlo, devi aggiungere la seguente riga al file index.html:

<head>
  <meta charset="utf-8" />
  <link rel="icon" href="/favicon.ico" />

  <^>
  <link <^ />
  <^>
  href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"
    rel="stylesheet"   /> <^>

  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="theme-color" content="#000000" />
  <meta
    name="description"
    content="refine | Build your React-based CRUD applications, without constraints."
  />
  <meta
    data-rh="true"
    property="og:image"
    content="https://refine.dev/img/refine_social.png"
  />
  <meta
    data-rh="true"
    name="twitter:image"
    content="https://refine.dev/img/refine_social.png"
  />
  <title>
    Refine - Build your React-based CRUD applications, without constraints.
  </title>
</head>

Ispezionare il componente personalizzato <Layout /> Componente

Nel passaggio precedente abbiamo aggiunto un componente di layout personalizzato all’app. Normalmente, potremmo utilizzare il layout predefinito del framework UI, ma vogliamo mostrare come puoi effettuare delle personalizzazioni.

Il componente di layout contiene l’intestazione, la barra laterale e l’area dei contenuti principali. Utilizza <ThemedLayoutV2 /> come base e lo ha personalizzato per adattarsi al design dell’app di gestione HR.

<Sider />

La barra laterale contiene il logo dell’app e i link di navigazione. Su dispositivi mobili è una barra laterale collapsibile che si apre quando l’utente clicca sull’icona del menu. I link di navigazione sono preparati con il useMenu hook di Refine e vengono resi in base al ruolo dell’utente con l’aiuto del <CanAccess /> componente.

<UserSelect />

Montato sulla barra laterale, mostra l’avatar e il nome dell’utente connesso. Quando cliccato, apre un popover con i dettagli dell’utente e un pulsante di disconnessione. Gli utenti possono passare tra diversi ruoli selezionando dal menu a discesa. Questo componente consente di testare passando tra utenti con ruoli diversi.

<Header />

Non rende nulla sui dispositivi desktop. Su dispositivi mobili, mostra il logo dell’app e un’icona di menu per aprire la barra laterale. L’intestazione è fissa e sempre visibile nella parte superiore della pagina.

<PageHeader />

Mostra il titolo della pagina e i pulsanti di navigazione. Il titolo della pagina viene generato automaticamente con il hook useResource, che recupera il nome della risorsa dal contesto di Refine. Ci consente di condividere lo stesso stile e layout in tutta l’applicazione.

Passaggio 3 — Implementazione dell’Autenticazione e Autorizzazione

In questo passaggio, implementeremo la logica di autenticazione e autorizzazione per la nostra applicazione di Gestione delle Risorse Umane. Questo servirà come ottimo esempio di controllo degli accessi nelle applicazioni enterprise.

Quando gli utenti effettuano l’accesso come manager, potranno vedere le pagine Time Off e Requests. Se accedono come dipendenti, vedranno solo la pagina Time Off. I manager possono approvare o rifiutare le richieste di permesso sulla pagina Requests.

I dipendenti possono richiedere del tempo libero e visualizzare la loro cronologia sulla pagina Time Off. Per implementare ciò, utilizzeremo le funzionalità authProvider e accessControlProvider di Refine.

Authentication

In Refine, l’autenticazione è gestita dal authProvider. Questo ti consente di definire la logica di autenticazione per la tua app. Nel passaggio precedente, abbiamo già copiato il authProvider dal repository GitHub e lo abbiamo dato al componente <Refine /> come prop. Utilizzeremo i seguenti hooks e componenti per controllare il comportamento della nostra app in base se l’utente ha effettuato il login o meno.

  • useLogin: Un hook che fornisce una funzione mutate per effettuare il login dell’utente.
  • useLogout: Un hook che fornisce una funzione mutate per effettuare il logout dell’utente.
  • useIsAuthenticated: Un hook che restituisce un booleano che indica se l’utente è autenticato.
  • <Authenticated />: Un componente che rende i suoi figli solo se l’utente è autenticato.

Authorization

In Refine, l’autorizzazione è gestita dal accessControlProvider. Permette di definire ruoli e permessi degli utenti e controllare l’accesso a diverse parti dell’app in base al ruolo dell’utente. Nel passaggio precedente, abbiamo già copiato il accessControlProvider dal repository di GitHub e lo abbiamo fornito al componente <Refine /> come prop. Diamo un’occhiata più da vicino al accessControlProvider per vedere come funziona.

src/providers/access-control/index.ts

import type { AccessControlBindings } from "@refinedev/core";
import { Role } from "@/types";

export const accessControlProvider: AccessControlBindings = {
  options: {
    queryOptions: {
      keepPreviousData: true,
    },
    buttons: {
      hideIfUnauthorized: true,
    },
  },
  can: async ({ params, action }) => {
    const user = JSON.parse(localStorage.getItem("user") || "{}");
    if (!user) return { can: false };

    const scope = params?.resource?.meta?.scope;
    // se la risorsa non ha uno scope, non è accessibile
    if (!scope) return { can: false };

    if (user.role === Role.MANAGER) {
      return {
        can: true,
      };
    }

    if (action === "manager") {
      return {
        can: user.role === Role.MANAGER,
      };
    }

    if (action === "employee") {
      return {
        can: user.role === Role.EMPLOYEE,
      };
    }

    // gli utenti possono accedere alle risorse solo se il loro ruolo corrisponde allo scope della risorsa
    return {
      can: user.role === scope,
    };
  },
};


Nel nostro app, abbiamo due ruoli: MANAGER e EMPLOYEE.

I manager hanno accesso alla pagina Richieste, mentre i dipendenti hanno accesso solo alla pagina Permesso. Il accessControlProvider controlla il ruolo dell’utente e lo scope della risorsa per determinare se l’utente può accedere alla risorsa. Se il ruolo dell’utente corrisponde allo scope della risorsa, allora possono accedere alla risorsa. Altrimenti, viene loro negato l’accesso. Utilizzeremo il hook useCan e il componente <CanAccess /> per controllare il comportamento della nostra app basato sul ruolo dell’utente.

Configurazione della Pagina di Accesso

Nel passaggio precedente, abbiamo aggiunto il authProvider al componente <Refine />. Il authProvider è responsabile della gestione dell’autenticazione.

Prima di tutto, dobbiamo ottenere delle immagini. Utilizzeremo queste immagini come sfondi per la pagina di accesso. Crea una nuova cartella chiamata images nella cartella public e ottieni le immagini dal repository di GitHub.

Dopo aver ottenuto le immagini, creiamo un nuovo file chiamato index.tsx nella cartella src/pages/login e aggiungiamo il seguente codice:

src/pages/login/index.tsx
import { useState } from "react";
import { useLogin } from "@refinedev/core";
import {
  Avatar,
  Box,
  Button,
  Divider,
  MenuItem,
  Select,
  Typography,
} from "@mui/material";
import { HrLogo } from "@/icons";

export const PageLogin = () => {
  const [selectedEmail, setSelectedEmail] = useState<string>(
    mockUsers.managers[0].email,
  );

  const { mutate: login } = useLogin();

  return (
    <Box
      sx={{
        position: "relative",
        background:
          "linear-gradient(180deg, #7DE8CD 0%, #C6ECD9 24.5%, #5CD6D6 100%)",
        display: "flex",
        flexDirection: "column",
        justifyContent: "center",
        alignItems: "center",
        height: "100dvh",
      }}
    >
      <Box
        sx={{
          zIndex: 2,
          background: "white",
          width: "328px",
          padding: "24px",
          borderRadius: "36px",
          display: "flex",
          flexDirection: "column",
          gap: "24px",
        }}
      >
        <Box
          sx={{
            display: "flex",
            flexDirection: "column",
            alignItems: "center",
            gap: "16px",
          }}
        >
          <HrLogo />
          <Typography variant="body1" fontWeight={600}>
            Welcome to RefineHR
          </Typography>
        </Box>

        <Divider />

        <Box sx={{ display: "flex", flexDirection: "column", gap: "8px" }}>
          <Typography variant="caption" color="text.secondary">
            Select user
          </Typography>
          <Select
            size="small"
            value={selectedEmail}
            sx={{
              height: "40px",
              borderRadius: "12px",

              "& .MuiOutlinedInput-notchedOutline": {
                borderWidth: "1px !important",
                borderColor: (theme) => `${theme.palette.divider} !important`,
              },
            }}
            MenuProps={{
              sx: {
                "& .MuiList-root": {
                  paddingBottom: "0px",
                },

                "& .MuiPaper-root": {
                  border: (theme) => `1px solid ${theme.palette.divider}`,
                  borderRadius: "12px",
                  boxShadow: "none",
                },
              },
            }}
          >
            <Typography
              variant="caption"
              textTransform="uppercase"
              color="text.secondary"
              sx={{
                paddingLeft: "12px",
                paddingBottom: "8px",
                display: "block",
              }}
            >
              Managers
            </Typography>
            {mockUsers.managers.map((user) => (
              <MenuItem
                key={user.email}
                value={user.email}
                onClick={() => setSelectedEmail(user.email)}
              >
                <Box sx={{ display: "flex", alignItems: "center", gap: "8px" }}>
                  <Avatar
                    src={user.avatarUrl}
                    alt={`${user.firstName} ${user.lastName}`}
                    sx={{ width: "24px", height: "24px" }}
                  />
                  <Typography
                    noWrap
                    variant="caption"
                    sx={{ display: "flex", alignItems: "center" }}
                  >
                    {`${user.firstName} ${user.lastName}`}
                  </Typography>
                </Box>
              </MenuItem>
            ))}

            <Divider />

            <Typography
              variant="caption"
              textTransform="uppercase"
              color="text.secondary"
              sx={{
                paddingLeft: "12px",
                paddingBottom: "8px",
                display: "block",
              }}
            >
              Employees
            </Typography>
            {mockUsers.employees.map((user) => (
              <MenuItem
                key={user.email}
                value={user.email}
                onClick={() => setSelectedEmail(user.email)}
              >
                <Box sx={{ display: "flex", alignItems: "center", gap: "8px" }}>
                  <Avatar
                    src={user.avatarUrl}
                    alt={`${user.firstName} ${user.lastName}`}
                    sx={{ width: "24px", height: "24px" }}
                  />
                  <Typography
                    noWrap
                    variant="caption"
                    sx={{ display: "flex", alignItems: "center" }}
                  >
                    {`${user.firstName} ${user.lastName}`}
                  </Typography>
                </Box>
              </MenuItem>
            ))}
          </Select>
        </Box>

        <Button
          variant="contained"
          sx={{
            borderRadius: "12px",
            height: "40px",
            width: "100%",
            color: "white",
            backgroundColor: (theme) => theme.palette.grey[900],
          }}
          onClick={() => {
            login({ email: selectedEmail });
          }}
        >
          Sign in
        </Button>
      </Box>

      <Box
        sx={{
          zIndex: 1,
          width: {
            xs: "240px",
            sm: "370px",
            md: "556px",
          },
          height: {
            xs: "352px",
            sm: "554px",
            md: "816px",
          },
          position: "absolute",
          left: "0px",
          bottom: "0px",
        }}
      >
        <img
          src="/images/login-left.png"
          alt="flowers"
          width="100%"
          height="100%"
        />
      </Box>
      <Box
        sx={{
          zIndex: 1,
          width: {
            xs: "320px",
            sm: "480px",
            md: "596px",
          },
          height: {
            xs: "312px",
            sm: "472px",
            md: "584px",
          },
          position: "absolute",
          right: "0px",
          top: "0px",
        }}
      >
        <img
          src="/images/login-right.png"
          alt="flowers"
          width="100%"
          height="100%"
        />
      </Box>
    </Box>
  );
};

const mockUsers = {
  managers: [
    {
      email: "[email protected]",
      firstName: "Michael",
      lastName: "Scott",
      avatarUrl:
        "https://refine-hr-example.s3.eu-west-1.amazonaws.com/avatars/Michael-Scott.png",
    },
    {
      avatarUrl:
        "https://refine-hr-example.s3.eu-west-1.amazonaws.com/avatars/Jim-Halpert.png",
      firstName: "Jim",
      lastName: "Halpert",
      email: "[email protected]",
    },
    {
      avatarUrl:
        "https://refine-hr-example.s3.eu-west-1.amazonaws.com/avatars/Toby-Flenderson.png",
      firstName: "Toby",
      lastName: "Flenderson",
      email: "[email protected]",
    },
  ],
  employees: [
    {
      avatarUrl:
        "https://refine-hr-example.s3.eu-west-1.amazonaws.com/avatars/Pam-Beesly.png",
      firstName: "Pam",
      lastName: "Beesly",
      email: "[email protected]",
    },
    {
      avatarUrl:
        "https://refine-hr-example.s3.eu-west-1.amazonaws.com/avatars/Andy-Bernard.png",
      firstName: "Andy",
      lastName: "Bernard",
      email: "[email protected]",
    },
    {
      avatarUrl:
        "https://refine-hr-example.s3.eu-west-1.amazonaws.com/avatars/Ryan-Howard.png",
      firstName: "Ryan",
      lastName: "Howard",
      email: "[email protected]",
    },
  ],
};

Per semplificare il processo di autenticazione, abbiamo creato un oggetto mockUsers con due array: managers e employees. Ogni array contiene oggetti utente predefiniti. Quando un utente seleziona un’email dal menu a discesa e clicca sul pulsante Accedi, la funzione login viene chiamata con l’email selezionata. La funzione login è una funzione di mutazione fornita dal hook useLogin di Refine. Essa chiama authProvider.login con l’email selezionata.

Successivamente, importiamo il componente <PageLogin /> e aggiornare il file App.tsx con le modifiche evidenziate.

src/App.tsx
import { Authenticated, Refine } from "@refinedev/core";
import { DevtoolsProvider, DevtoolsPanel } from "@refinedev/devtools";
import { ErrorComponent } from "@refinedev/mui";
import dataProvider from "@refinedev/nestjsx-crud";
import routerProvider, {
  UnsavedChangesNotifier,
  DocumentTitleHandler,
} from "@refinedev/react-router-v6";
import {
  BrowserRouter,
  Routes,
  Route,
  Outlet,
  Navigate,
} from "react-router-dom";
import { Toaster } from "react-hot-toast";

import { PageLogin } from "@/pages/login";

import { Layout } from "@/components/layout";

import { ThemeProvider } from "@/providers/theme-provider";
import { authProvider } from "@/providers/auth-provider";
import { accessControlProvider } from "@/providers/access-control";
import { useNotificationProvider } from "@/providers/notification-provider";
import { queryClient } from "@/providers/query-client";

import { BASE_URL } from "@/utilities/constants";
import { axiosInstance } from "@/utilities/axios";

import { Role } from './types'

import "@/utilities/init-dayjs";

function App() {
  return (
    <BrowserRouter>
      <ThemeProvider>
        <DevtoolsProvider>
          <Refine
            authProvider={authProvider}
            routerProvider={routerProvider}
            dataProvider={dataProvider(BASE_URL, axiosInstance)}
            notificationProvider={useNotificationProvider}
            resources={[
              {
                name: 'employee',
                meta: {
                  scope: Role.EMPLOYEE,
                },
              },
              {
                name: 'manager',
                meta: {
                  scope: Role.MANAGER,
                },
              },
            ]}
            accessControlProvider={accessControlProvider}
            options={{
              reactQuery: {
                clientConfig: queryClient,
              },
              syncWithLocation: true,
              warnWhenUnsavedChanges: true,
              useNewQueryKeys: true,
            }}
          >
            <Routes>
              <Route
                element={
                  <Authenticated
                    key="authenticated-routes"
                    redirectOnFail="/login"
                  >
                    <Layout>
                      <Outlet />
                    </Layout>
                  </Authenticated>
                }
              >
                <Route index element={<h1>Hello World</h1>} />
              </Route>

              <Route
                element={
                  <Authenticated key="auth-pages" fallback={<Outlet />}>
                    <Navigate to="/" />
                  </Authenticated>
                }
              >
                <Route path="/login" element={<PageLogin />} />
              </Route>

              <Route
                element={
                  <Authenticated key="catch-all">
                    <Layout>
                      <Outlet />
                    </Layout>
                  </Authenticated>
                }
              >
                <Route path="*" element={<ErrorComponent />} />
              </Route>
            </Routes>

            <UnsavedChangesNotifier />
            <DocumentTitleHandler />
            <Toaster position="bottom-right" reverseOrder={false} />
            <DevtoolsPanel />
          </Refine>
        </DevtoolsProvider>
      </ThemeProvider>
    </BrowserRouter>
  );
}

export default App;

Nell’aggiornato file App.tsx, abbiamo aggiunto il <Authenticated /> componente di Refine. Questo componente viene utilizzato per proteggere le rotte che richiedono autenticazione. Prende una prop key per identificare univocamente il componente, una prop fallback da rendere quando l’utente non è autenticato, e una prop redirectOnFail per reindirizzare l’utente alla rotta specificata quando l’autenticazione fallisce. Sotto il cofano chiama il metodo authProvider.check per verificare se l’utente è autenticato.

Esaminiamo più da vicino cosa abbiamo su key="auth-pages"

<Route
  element={
    <Authenticated key="auth-pages" fallback={<Outlet />}>
      <Navigate to="/" />
    </Authenticated>
  }
>
  <Route path="/login" element={<PageLogin />} />
</Route>

<Authenticated /> componente avvolge la rotta “/login” per controllare lo stato di autenticazione dell’utente.

  • fallback={<Outlet />}: Se l’utente è non autenticato, renderizza la rotta nidificata (ossia, mostra il componente <PageLogin />).
  • Figli (<Navigate to="/" />): Se l’utente è autenticato, reindirizzalo alla home page (/).

Esaminiamo più da vicino cosa abbiamo su key="catch-all"

<Route
  element={
    <Authenticated key="catch-all">
      <Layout>
        <Outlet />
      </Layout>
    </Authenticated>
  }
>
  <Route path="*" element={<ErrorComponent />} />
</Route>

<Authenticated /> componente avvolge la rotta path="*" per controllare lo stato di autenticazione dell’utente. Questa rotta è una rotta catch-all che renderizza il <ErrorComponent /> quando l’utente è autenticato. Ci permette di mostrare una pagina 404 quando l’utente tenta di accedere a una rotta inesistente.

Ora, quando esegui l’app e navighi a http://localhost:5173/login, dovresti vedere la pagina di accesso con il menu a discesa per selezionare l’utente.

Al momento, la pagina “/” non sta facendo nulla. Nei prossimi passi implementeremo le pagine Time Off e Requests.

Passo 4 — Creazione di una pagina Time Off

Costruzione della pagina Elenco Permessi

In questo passaggio, costruiremo la pagina Permessi. I dipendenti possono richiedere permessi e vedere la loro cronologia dei permessi. I manager possono anche visualizzare la loro cronologia, ma invece di richiedere permessi, possono assegnarli direttamente a se stessi. Faremo funzionare questo utilizzando accessControlProvider di Refine, il componente <CanAccess /> e l’hook useCan.

<PageEmployeeTimeOffsList />

Prima di iniziare a costruire la pagina dei permessi, dobbiamo creare un paio di componenti per mostrare la cronologia dei permessi, le richieste di permesso imminenti e le statistiche dei permessi utilizzati. Alla fine di questo passaggio, utilizzeremo questi componenti per costruire la pagina dei permessi.

Costruzione del componente <TimeOffList /> per mostrare la cronologia dei permessi

Crea una nuova cartella chiamata time-offs nella cartella src/components. All’interno della cartella time-offs, crea un nuovo file chiamato list.tsx e aggiungi il seguente codice:

src/components/time-offs/list.tsx
import { useState } from "react";
import {
  type CrudFilters,
  type CrudSort,
  useDelete,
  useGetIdentity,
  useInfiniteList,
} from "@refinedev/core";
import {
  Box,
  Button,
  CircularProgress,
  IconButton,
  Popover,
  Typography,
} from "@mui/material";
import InfiniteScroll from "react-infinite-scroll-component";
import dayjs from "dayjs";
import { DateField } from "@refinedev/mui";
import { Frame } from "@/components/frame";
import { LoadingOverlay } from "@/components/loading-overlay";
import { red } from "@/providers/theme-provider/colors";
import {
  AnnualLeaveIcon,
  CasualLeaveIcon,
  DeleteIcon,
  NoTimeOffIcon,
  SickLeaveIcon,
  ThreeDotsIcon,
  PopoverTipIcon,
} from "@/icons";
import { type Employee, TimeOffStatus, type TimeOff } from "@/types";

const variantMap = {
  Annual: {
    label: "Annual Leave",
    iconColor: "primary.700",
    iconBgColor: "primary.50",
    icon: <AnnualLeaveIcon width={16} height={16} />,
  },
  Sick: {
    label: "Sick Leave",
    iconColor: "#C2410C",
    iconBgColor: "#FFF7ED",
    icon: <SickLeaveIcon width={16} height={16} />,
  },
  Casual: {
    label: "Casual Leave",
    iconColor: "grey.700",
    iconBgColor: "grey.50",
    icon: <CasualLeaveIcon width={16} height={16} />,
  },
} as const;

type Props = {
  type: "upcoming" | "history" | "inReview";
};

export const TimeOffList = (props: Props) => {
  const { data: employee } = useGetIdentity<Employee>();

  const { data, isLoading, hasNextPage, fetchNextPage } =
    useInfiniteList<TimeOff>({
      resource: "time-offs",
      sorters: sorters[props.type],
      filters: [
        ...filters[props.type],
        {
          field: "employeeId",
          operator: "eq",
          value: employee?.id,
        },
      ],
      queryOptions: {
        enabled: !!employee?.id,
      },
    });

  const timeOffHistory = data?.pages.flatMap((page) => page.data) || [];
  const hasData = isLoading || timeOffHistory.length !== 0;

  if (props.type === "inReview" && !hasData) {
    return null;
  }

  return (
    <Frame
      sx={(theme) => ({
        maxHeight: "362px",
        paddingBottom: 0,
        position: "relative",
        "&::after": {
          pointerEvents: "none",
          content: '""',
          position: "absolute",
          bottom: 0,
          left: "24px",
          right: "24px",
          width: "80%",
          height: "32px",
          background:
            "linear-gradient(180deg, rgba(255, 255, 255, 0), #FFFFFF)",
        },
        display: "flex",
        flexDirection: "column",
      })}
      sxChildren={{
        paddingRight: 0,
        paddingLeft: 0,
        flex: 1,
        overflow: "hidden",
      }}
      title={title[props.type]}
    >
      <LoadingOverlay loading={isLoading} sx={{ height: "100%" }}>
        {!hasData ? (
          <Box
            sx={{
              display: "flex",
              flexDirection: "column",
              alignItems: "center",
              justifyContent: "center",
              gap: "24px",
              height: "180px",
            }}
          >
            <NoTimeOffIcon />
            <Typography variant="body2" color="text.secondary">
              {props.type === "history"
                ? "No time off used yet."
                : "No upcoming time offs scheduled."}
            </Typography>
          </Box>
        ) : (
          <Box
            id="scrollableDiv-timeOffHistory"
            sx={(theme) => ({
              maxHeight: "312px",
              height: "auto",
              [theme.breakpoints.up("lg")]: {
                height: "312px",
              },
              overflow: "auto",
              paddingLeft: "12px",
              paddingRight: "12px",
            })}
          >
            <InfiniteScroll
              dataLength={timeOffHistory.length}
              next={() => fetchNextPage()}
              hasMore={hasNextPage || false}
              endMessage={
                !isLoading &&
                hasData && (
                  <Box
                    sx={{
                      pt: timeOffHistory.length > 3 ? "40px" : "16px",
                    }}
                  />
                )
              }
              scrollableTarget="scrollableDiv-timeOffHistory"
              loader={
                <Box
                  sx={{
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                    width: "100%",
                    height: "100px",
                  }}
                >
                  <CircularProgress size={24} />
                </Box>
              }
            >
              <Box
                sx={{
                  display: "flex",
                  flexDirection: "column",
                  gap: "12px",
                }}
              >
                {timeOffHistory.map((timeOff) => {
                  return (
                    <ListItem
                      timeOff={timeOff}
                      key={timeOff.id}
                      type={props.type}
                    />
                  );
                })}
              </Box>
            </InfiniteScroll>
          </Box>
        )}
      </LoadingOverlay>
    </Frame>
  );
};

const ListItem = ({
  timeOff,
  type,
}: { timeOff: TimeOff; type: Props["type"] }) => {
  const { mutateAsync: timeOffCancel } = useDelete<TimeOff>();

  const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
  const [hovered, setHovered] = useState(false);

  const diffrenceOfDays =
    dayjs(timeOff.endsAt).diff(dayjs(timeOff.startsAt), "day") + 1;

  const isSameDay = dayjs(timeOff.startsAt).isSame(
    dayjs(timeOff.endsAt),
    "day",
  );

  return (
    <Box
      key={timeOff.id}
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      sx={{
        display: "flex",
        alignItems: "center",
        gap: "16px",
        height: "64px",
        paddingLeft: "12px",
        paddingRight: "12px",
        borderRadius: "64px",
        backgroundColor: hovered ? "grey.50" : "transparent",
        transition: "background-color 0.2s",
      }}
    >
      <Box
        sx={{
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          color: variantMap[timeOff.timeOffType].iconColor,
          backgroundColor: variantMap[timeOff.timeOffType].iconBgColor,
          width: "40px",
          height: "40px",
          borderRadius: "100%",
        }}
      >
        {variantMap[timeOff.timeOffType].icon}
      </Box>
      <Box
        sx={{
          display: "flex",
          flexDirection: "column",
          gap: "4px",
        }}
      >
        <Box
          sx={{
            display: "flex",
            alignItems: "center",
            gap: "4px",
          }}
        >
          {isSameDay ? (
            <DateField
              value={timeOff.startsAt}
              color="text.secondary"
              variant="caption"
              format="MMMM DD"
            />
          ) : (
            <>
              <DateField
                value={timeOff.startsAt}
                color="text.secondary"
                variant="caption"
                format="MMMM DD"
              />
              <Typography variant="caption" color="text.secondary">
                -
              </Typography>
              <DateField
                value={timeOff.endsAt}
                color="text.secondary"
                variant="caption"
                format="MMMM DD"
              />
            </>
          )}
        </Box>
        <Typography variant="body2">
          <span
            style={{
              fontWeight: 500,
            }}
          >
            {diffrenceOfDays} {diffrenceOfDays > 1 ? "days" : "day"} of{" "}
          </span>
          {variantMap[timeOff.timeOffType].label}
        </Typography>
      </Box>

      {hovered && (type === "inReview" || type === "upcoming") && (
        <IconButton
          onClick={(e) => setAnchorEl(e.currentTarget)}
          sx={{
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            width: "40px",
            height: "40px",
            marginLeft: "auto",
            backgroundColor: "white",
            borderRadius: "100%",
            color: "grey.400",
            border: (theme) => `1px solid ${theme.palette.grey[400]}`,
            flexShrink: 0,
          }}
        >
          <ThreeDotsIcon />
        </IconButton>
      )}

      <Popover
        id={timeOff.id.toString()}
        open={Boolean(anchorEl)}
        anchorEl={anchorEl}
        onClose={() => {
          setAnchorEl(null);
          setHovered(false);
        }}
        anchorOrigin={{
          vertical: "bottom",
          horizontal: "center",
        }}
        transformOrigin={{
          vertical: "top",
          horizontal: "center",
        }}
        sx={{
          "& .MuiPaper-root": {
            overflow: "visible",
            borderRadius: "12px",
            border: (theme) => `1px solid ${theme.palette.grey[400]}`,
            boxShadow: "0px 0px 0px 4px rgba(222, 229, 237, 0.25)",
          },
        }}
      >
        <Button
          variant="text"
          onClick={async () => {
            await timeOffCancel({
              resource: "time-offs",
              id: timeOff.id,
              invalidates: ["all"],
              successNotification: () => {
                return {
                  type: "success",
                  message: "Time off request cancelled successfully.",
                };
              },
            });
          }}
          sx={{
            position: "relative",
            width: "200px",
            height: "56px",
            paddingLeft: "16px",
            color: red[900],
            display: "flex",
            gap: "12px",
            justifyContent: "flex-start",
            "&:hover": {
              backgroundColor: "transparent",
            },
          }}
        >
          <DeleteIcon />
          <Typography variant="body2">Cancel Request</Typography>

          <Box
            sx={{
              width: "40px",
              height: "16px",
              position: "absolute",
              top: "-2px",
              left: "calc(50% - 1px)",
              transform: "translate(-50%, -50%)",
            }}
          >
            <PopoverTipIcon />
          </Box>
        </Button>
      </Popover>
    </Box>
  );
};

const today = dayjs().toISOString();

const title: Record<Props["type"], string> = {
  history: "Time Off History",
  upcoming: "Upcoming Time Off",
  inReview: "In Review",
};

const filters: Record<Props["type"], CrudFilters> = {
  history: [
    {
      field: "status",
      operator: "eq",
      value: TimeOffStatus.APPROVED,
    },
    {
      field: "endsAt",
      operator: "lt",
      value: today,
    },
  ],
  upcoming: [
    {
      field: "status",
      operator: "eq",
      value: TimeOffStatus.APPROVED,
    },
    {
      field: "endsAt",
      operator: "gte",
      value: today,
    },
  ],
  inReview: [
    {
      field: "status",
      operator: "eq",
      value: TimeOffStatus.PENDING,
    },
  ],
};

const sorters: Record<Props["type"], CrudSort[]> = {
  history: [{ field: "startsAt", order: "desc" }],
  upcoming: [{ field: "endsAt", order: "asc" }],
  inReview: [{ field: "startsAt", order: "asc" }],
};


Il file list.tsx è lungo, ma la maggior parte riguarda lo styling e la presentazione dell’interfaccia utente.

<TimeOffList />

Utilizzeremo questo <TimeOffList /> componente in tre contesti diversi:

  <TimeOffList type="inReview" />
  <TimeOffList type="upcoming" />
  <TimeOffList type="history" />

Il prop type determina che tipo di lista di assenze mostrare:

  • inReview: Mostra le richieste di assenza in attesa di approvazione.
  • upcoming: Visualizza le assenze future che sono state approvate ma non ancora verificatesi.
  • history: Elenca le assenze che sono state approvate e che si sono già verificate.

All’interno del componente, creeremo filtri e ordinamenti basati sul prop type. Useremo questi filtri e ordinamenti per recuperare i dati sulle assenze dall’API.

Analizziamo le parti chiave del componente:

1. Ottenere l’Utente Corrente
const { data: employee } = useGetIdentity<Employee>();
  • useGetIdentity<Employee>(): Recupera le informazioni dell’utente corrente.
    • Utilizziamo l’ID del dipendente per filtrare le assenze in modo che ogni utente veda solo le proprie richieste.
2. Recuperare Dati sulle Assenze con Scorrimento Infinito
const { data, isLoading, hasNextPage, fetchNextPage } =
  useInfiniteList <
  TimeOff >
  {
    resource: "time-offs",
    sorters: sorters[props.type],
    filters: [
      ...filters[props.type],
      { field: "employeeId", operator: "eq", value: employee?.id },
    ],
    queryOptions: { enabled: !!employee?.id },
  };

// ...

<InfiniteScroll
  dataLength={timeOffHistory.length}
  next={() => fetchNextPage()}
  hasMore={hasNextPage || false}
  // ... altri props
>
  {/* Renderizza gli elementi della lista qui */}
</InfiniteScroll>;
  • useInfiniteList<TimeOff>(): Recupera i dati delle ferie con scrolling infinito.

    • resource: Specifica il punto di accesso API.
    • sorters e filters: Regolati in base al type per recuperare i dati corretti.
    • employeeId filtro: Assicura che vengano recuperate solo le ferie dell’utente attuale.
    • queryOptions.enabled: Esegue la query solo quando i dati dell’utente sono disponibili.
  • <InfiniteScroll />: Consente di caricare più dati man mano che l’utente scorre verso il basso.

    • next: Funzione per recuperare la pagina successiva di dati.
    • hasMore: Indica se sono disponibili ulteriori dati.
3. Annullamento di una Richiesta di Ferie
const { mutateAsync: timeOffCancel } = useDelete<TimeOff>();

// All'interno del componente ListItem
await timeOffCancel({
  resource: "time-offs",
  id: timeOff.id,
  invalidates: ["all"],
  successNotification: () => ({
    type: "success",
    message: "Time off request cancelled successfully.",
  }),
});
  • useDelete: Fornisce la funzione timeOffCancel per eliminare una richiesta di ferie.
    • Utilizzato quando un utente annulla le proprie ferie.
    • Mostra un messaggio di successo al termine.
4. Visualizzazione delle Date con <DateField />
<DateField
  value={timeOff.startsAt}
  color="text.secondary"
  variant="caption"
  format="MMMM DD"
/>
  • <DateField />: Formatta e visualizza le date in modo user-friendly.
    • value: La data da visualizzare.
    • format: Specifica il formato della data (ad es., “5 Gennaio”).
5. Creazione di Filtri e Ordinatori basati su type

Filtri:

const filters: Record<Props["type"], CrudFilters> = {
  history: [
    {
      field: "status",
      operator: "eq",
      value: TimeOffStatus.APPROVED,
    },
    {
      field: "endsAt",
      operator: "lt",
      value: today,
    },
  ],
  // ... altri tipi
};
  • Definisce i criteri per recuperare le assenze in base allo stato e alle date.
    • history: Recupera le assenze approvate che sono già terminate.
    • upcoming: Recupera le assenze approvate che sono in arrivo.

Ordinatori:

const sorters: Record<Props["type"], CrudSort[]> = {
  history: [{ field: "startsAt", order: "desc" }],
  // ... altri tipi
};
  • Determina l’ordine dei dati recuperati.
    • history: Ordina per data di inizio in ordine decrescente.

Creazione del componente <TimeOffLeaveCards /> per visualizzare le statistiche delle assenze utilizzate.

Crea un nuovo file chiamato leave-cards.tsx nella cartella src/components/time-offs e aggiungi il seguente codice:

src/components/time-offs/leave-cards.tsx

import { useGetIdentity, useList } from "@refinedev/core";
import { Box, Grid, Skeleton, Typography } from "@mui/material";
import { AnnualLeaveIcon, CasualLeaveIcon, SickLeaveIcon } from "@/icons";
import {
  type Employee,
  TimeOffStatus,
  TimeOffType,
  type TimeOff,
} from "@/types";

type Props = {
  employeeId?: number;
};

export const TimeOffLeaveCards = (props: Props) => {
  const { data: employee, isLoading: isLoadingEmployee } =
    useGetIdentity<Employee>({
      queryOptions: {
        enabled: !props.employeeId,
      },
    });

  const { data: timeOffsSick, isLoading: isLoadingTimeOffsSick } =
    useList<TimeOff>({
      resource: "time-offs",
      // abbiamo bisogno solo del numero totale di giorni di malattia, quindi possiamo impostare pageSize a 1 per ridurre il carico
      pagination: { pageSize: 1 },
      filters: [
        {
          field: "status",
          operator: "eq",
          value: TimeOffStatus.APPROVED,
        },
        {
          field: "timeOffType",
          operator: "eq",
          value: TimeOffType.SICK,
        },
        {
          field: "employeeId",
          operator: "eq",
          value: employee?.id,
        },
      ],
      queryOptions: {
        enabled: !!employee?.id,
      },
    });

  const { data: timeOffsCasual, isLoading: isLoadingTimeOffsCasual } =
    useList<TimeOff>({
      resource: "time-offs",
      // abbiamo bisogno solo del numero totale di giorni di malattia, quindi possiamo impostare pageSize a 1 per ridurre il carico
      pagination: { pageSize: 1 },
      filters: [
        {
          field: "status",
          operator: "eq",
          value: TimeOffStatus.APPROVED,
        },
        {
          field: "timeOffType",
          operator: "eq",
          value: TimeOffType.CASUAL,
        },
        {
          field: "employeeId",
          operator: "eq",
          value: employee?.id,
        },
      ],
      queryOptions: {
        enabled: !!employee?.id,
      },
    });

  const loading =
    isLoadingEmployee || isLoadingTimeOffsSick || isLoadingTimeOffsCasual;

  return (
    <Grid container spacing="24px">
      <Grid item xs={12} sm={4}>
        <Card
          loading={loading}
          type="annual"
          value={employee?.availableAnnualLeaveDays || 0}
        />
      </Grid>
      <Grid item xs={12} sm={4}>
        <Card loading={loading} type="sick" value={timeOffsSick?.total || 0} />
      </Grid>
      <Grid item xs={12} sm={4}>
        <Card
          loading={loading}
          type="casual"
          value={timeOffsCasual?.total || 0}
        />
      </Grid>
    </Grid>
  );
};

const variantMap = {
  annual: {
    label: "Annual Leave",
    description: "Days available",
    bgColor: "primary.50",
    titleColor: "primary.900",
    descriptionColor: "primary.700",
    iconColor: "primary.700",
    icon: <AnnualLeaveIcon />,
  },
  sick: {
    label: "Sick Leave",
    description: "Days used",
    bgColor: "#FFF7ED",
    titleColor: "#7C2D12",
    descriptionColor: "#C2410C",
    iconColor: "#C2410C",
    icon: <SickLeaveIcon />,
  },
  casual: {
    label: "Casual Leave",
    description: "Days used",
    bgColor: "grey.50",
    titleColor: "grey.900",
    descriptionColor: "grey.700",
    iconColor: "grey.700",
    icon: <CasualLeaveIcon />,
  },
};

const Card = (props: {
  type: "annual" | "sick" | "casual";
  value: number;
  loading?: boolean;
}) => {
  return (
    <Box
      sx={{
        backgroundColor: variantMap[props.type].bgColor,
        padding: "24px",
        borderRadius: "12px",
      }}
    >
      <Box
        sx={{
          display: "flex",
          alignItems: "center",
          justifyContent: "space-between",
        }}
      >
        <Typography
          variant="h6"
          sx={{
            color: variantMap[props.type].titleColor,
            fontSize: "16px",
            fontWeight: 500,
            lineHeight: "24px",
          }}
        >
          {variantMap[props.type].label}
        </Typography>
        <Box
          sx={{
            color: variantMap[props.type].iconColor,
          }}
        >
          {variantMap[props.type].icon}
        </Box>
      </Box>

      <Box sx={{ marginTop: "8px", display: "flex", flexDirection: "column" }}>
        {props.loading ? (
          <Box
            sx={{
              width: "40%",
              height: "32px",
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
            }}
          >
            <Skeleton
              variant="rounded"
              sx={{
                width: "100%",
                height: "20px",
              }}
            />
          </Box>
        ) : (
          <Typography
            variant="caption"
            sx={{
              color: variantMap[props.type].descriptionColor,
              fontSize: "24px",
              lineHeight: "32px",
              fontWeight: 600,
            }}
          >
            {props.value}
          </Typography>
        )}
        <Typography
          variant="body1"
          sx={{
            color: variantMap[props.type].descriptionColor,
            fontSize: "12px",
            lineHeight: "16px",
          }}
        >
          {variantMap[props.type].description}
        </Typography>
      </Box>
    </Box>
  );
};


<TimeOffLeaveCards />

Il componente <TimeOffLeaveCards /> visualizza statistiche sul tempo libero di un dipendente. Mostra tre schede per il Permesso Annuale, il Permesso di Malattia e il Permesso Personale, indicando quanti giorni sono disponibili o utilizzati.

Analizziamo le parti chiave del componente:

1. Recupero dei Dati
  • Dati del Dipendente: Utilizza useGetIdentity per ottenere le informazioni dell’attuale dipendente, come i giorni di permesso annuale disponibili.
  • Conteggi del Tempo Libero: Utilizza useList per recuperare il numero totale di giorni di malattia e di permesso personale utilizzati dal dipendente. Imposta pageSize a 1 perché abbiamo bisogno solo del conteggio totale, non di tutti i dettagli.
2. Visualizzazione delle Schede
  • Il componente rende tre componenti scheda, uno per ciascun tipo di permesso.
  • Ogni scheda mostra:
    • Il tipo di permesso (ad es. Permesso Annuale).
    • Il numero di giorni disponibili o utilizzati.
    • Un’icona che rappresenta il tipo di permesso.
3. Gestione degli Stati di Caricamento
  • Se i dati sono ancora in fase di caricamento, viene visualizzato un segnaposto scheletro invece dei numeri effettivi.
  • La prop loading viene passata alle card per gestire questo stato.
4. Il Componente Card
  • Riceve type, value e loading come prop.
  • Utilizza un variantMap per ottenere le etichette corrette, i colori e le icone in base al tipo di permesso.
  • Visualizza le informazioni sul permesso con uno stile appropriato.

Costruzione di <PageEmployeeTimeOffsList />

Ora che abbiamo i componenti per l’elenco dei permessi e la visualizzazione delle card dei permessi, creiamo il nuovo file nella cartella src/pages/employee/time-offs/ chiamato list.tsx e aggiungiamo il seguente codice:

src/pages/time-off.tsx
import { CanAccess, useCan } from "@refinedev/core";
import { CreateButton } from "@refinedev/mui";
import { Box, Grid } from "@mui/material";
import { PageHeader } from "@/components/layout/page-header";
import { TimeOffList } from "@/components/time-offs/list";
import { TimeOffLeaveCards } from "@/components/time-offs/leave-cards";
import { TimeOffIcon } from "@/icons";
import { ThemeProvider } from "@/providers/theme-provider";
import { Role } from "@/types";

export const PageEmployeeTimeOffsList = () => {
  const { data: useCanData } = useCan({
    action: "manager",
    params: {
      resource: {
        name: "time-offs",
        meta: {
          scope: "manager",
        },
      },
    },
  });
  const isManager = useCanData?.can;

  return (
    <ThemeProvider role={isManager ? Role.MANAGER : Role.EMPLOYEE}>
      <Box>
        <PageHeader
          title="Time Off"
          rightSlot={
            <CreateButton
              size="large"
              variant="contained"
              startIcon={<TimeOffIcon />}
            >
              <CanAccess action="manager" fallback="Request Time Off">
                Assign Time Off
              </CanAccess>
            </CreateButton>
          }
        />

        <TimeOffLeaveCards />

        <Grid
          container
          spacing="24px"
          sx={{
            marginTop: "24px",
          }}
        >
          <Grid item xs={12} md={6}>
            <Box
              sx={{
                display: "flex",
                flexDirection: "column",
                gap: "24px",
              }}
            >
              <TimeOffList type="inReview" />
              <TimeOffList type="upcoming" />
            </Box>
          </Grid>
          <Grid item xs={12} md={6}>
            <TimeOffList type="history" />
          </Grid>
        </Grid>
      </Box>
    </ThemeProvider>
  );
};

<PageEmployeeTimeOffsList /> è il componente principale per la pagina dei permessi, useremo questo componente per visualizzare gli elenchi dei permessi e le card dei permessi quando gli utenti navigano sulla rotta /employee/time-offs.

<PageEmployeeTimeOffsList />

Suddividiamo le parti chiave del componente:

1. Verifica dei Ruoli Utente
  • Utilizza il hook useCan per determinare se l’utente attuale è un manager.
  • Imposta isManager su true se l’utente ha le autorizzazioni di manager.
2. Applicazione del Tema in Base al Ruolo
  • Avvolge il contenuto all’interno di un <ThemeProvider />.
  • Il tema cambia in base a se l’utente è un manager o un dipendente.
3. Intestazione Pagina con Pulsante Condizionale
  • Visualizza un <PageHeader /> con il titolo “Richiesta di Permesso”.
  • Includi un <CreateButton /> che cambia in base al ruolo dell’utente:
    • Se l’utente è un manager, il pulsante dice “Assegna Permesso”.
    • Se l’utente non è un manager, dice “Richiedi Permesso”.
  • Questo è gestito utilizzando il componente <CanAccess />, che controlla i permessi.
4. Visualizzazione delle Statistiche dei Permessi
  • Includi il componente <TimeOffLeaveCards /> per mostrare i saldi e l’utilizzo dei permessi.
  • Questo fornisce un riepilogo dei permessi annuali, malattia e casuali.
5. Elenco delle Richieste di Permesso
  • Utilizza un layout <Grid /> per organizzare il contenuto.
  • Nella parte sinistra (md={6}), visualizza:
    • TimeOffList con type="inReview": Mostra le richieste di permesso in sospeso.
    • TimeOffList con type="upcoming": Mostra i permessi approvati imminenti.
  • Sul lato destro (md={6}), viene visualizzato:
    • TimeOffList con type="history": Mostra i permessi passati che si sono già verificati.

Aggiungendo il percorso “/employee/time-offs”

Siamo pronti a visualizzare il componente <PageEmployeeTimeOffsList /> nel percorso /employee/time-offs. Aggiorniamo il file App.tsx per includere questo percorso:

src/App.tsx
import { Authenticated, ErrorComponent, Refine } from '@refinedev/core'
import { DevtoolsProvider, DevtoolsPanel } from '@refinedev/devtools'
import dataProvider from '@refinedev/nestjsx-crud'
import routerProvider, {
  UnsavedChangesNotifier,
  DocumentTitleHandler,
  NavigateToResource,
} from '@refinedev/react-router-v6'
import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom'
import { Toaster } from 'react-hot-toast'

import { PageEmployeeTimeOffsList } from '@/pages/employee/time-offs/list'
import { PageLogin } from '@/pages/login'

import { Layout } from '@/components/layout'

import { ThemeProvider } from '@/providers/theme-provider'
import { authProvider } from '@/providers/auth-provider'
import { accessControlProvider } from '@/providers/access-control'
import { useNotificationProvider } from '@/providers/notification-provider'
import { queryClient } from '@/providers/query-client'

import { BASE_URL } from '@/utilities/constants'
import { axiosInstance } from '@/utilities/axios'

import { TimeOffIcon } from '@/icons'

import { Role } from '@/types'

import '@/utilities/init-dayjs'

function App() {
  return (
    <BrowserRouter>
      <ThemeProvider>
        <DevtoolsProvider>
          <Refine
            authProvider={authProvider}
            routerProvider={routerProvider}
            dataProvider={dataProvider(BASE_URL, axiosInstance)}
            notificationProvider={useNotificationProvider}
            resources={[
              {
                name: 'employee',
                meta: {
                  scope: Role.EMPLOYEE,
                },
              },
              {
                name: 'manager',
                meta: {
                  scope: Role.MANAGER,
                },
              },
              {
                name: 'time-offs',
                list: '/employee/time-offs',
                meta: {
                  parent: 'employee',
                  scope: Role.EMPLOYEE,
                  label: 'Time Off',
                  icon: <TimeOffIcon />,
                },
              },
            ]}
            accessControlProvider={accessControlProvider}
            options={{
              reactQuery: {
                clientConfig: queryClient,
              },
              syncWithLocation: true,
              warnWhenUnsavedChanges: true,
              useNewQueryKeys: true,
            }}>
            <Routes>
              <Route
                element={
                  <Authenticated key='authenticated-routes' redirectOnFail='/login'>
                    <Outlet />
                  </Authenticated>
                }>
                <Route index element={<NavigateToResource resource='time-offs' />} />

                <Route
                  path='employee'
                  element={
                    <ThemeProvider role={Role.EMPLOYEE}>
                      <Layout>
                        <Outlet />
                      </Layout>
                    </ThemeProvider>
                  }>
                  <Route path='time-offs' element={<Outlet />}>
                    <Route index element={<PageEmployeeTimeOffsList />} />
                  </Route>
                </Route>
              </Route>

              <Route
                element={
                  <Authenticated key='auth-pages' fallback={<Outlet />}>
                    <NavigateToResource resource='time-offs' />
                  </Authenticated>
                }>
                <Route path='/login' element={<PageLogin />} />
              </Route>

              <Route
                element={
                  <Authenticated key="catch-all">
                    <Layout>
                      <Outlet />
                    </Layout>
                  </Authenticated>
                }
              >
                <Route path="*" element={<ErrorComponent />} />
              </Route>
            </Routes>

            <UnsavedChangesNotifier />
            <DocumentTitleHandler />
            <Toaster position='bottom-right' reverseOrder={false} />
            <DevtoolsPanel />
          </Refine>
        </DevtoolsProvider>
      </ThemeProvider>
    </BrowserRouter>
  )
}

export default App

Analizziamo le parti chiave del file App.tsx aggiornato:

1. Definizione della Risorsa Tempo Libero
{
  name: 'time-offs',
  list: '/employee/time-offs',
  meta: {
    parent: 'employee',
    scope: Role.EMPLOYEE,
    label: 'Time Off',
    icon: <TimeOffIcon />,
  },
}

Abbiamo aggiunto una nuova risorsa per i permessi come figlio della risorsa employee risorsa. Questo indica che i permessi sono correlati agli impiegati e sono accessibili dagli impiegati.

  • name: 'time-offs': Questo è l’identificatore per la risorsa, utilizzato internamente da Refine.
  • list: '/employee/time-offs': Specifica il percorso che visualizza la vista elenco della risorsa.
  • meta: Un oggetto che contiene metadati aggiuntivi sulla risorsa.
    • parent: 'employee': Raggruppa questa risorsa nell’ambito di employee, che può essere utilizzato per organizzare le risorse nell’interfaccia utente (come in un menu a barra laterale) o per il controllo degli accessi.
    • scope: Role.EMPLOYEE: Indica che questa risorsa è accessibile agli utenti con il ruolo EMPLOYEE. Utilizziamo questo nell’accessControlProvider per gestire le autorizzazioni.
    • label: 'Time Off': Il nome visualizzato per la risorsa nell’interfaccia utente.
    • icon: <TimeOffIcon />: Associa l’icona TimeOffIcon a questa risorsa per l’identificazione visiva.
2. Reindirizzamento alla risorsa “time-offs” quando gli utenti navigano nella route /
<Route index element={<NavigateToResource resource="time-offs" />} />

Utilizziamo il <NavigateToResource /> componente per reindirizzare gli utenti alla risorsa time-offs quando navigano alla rotta /. Questo assicura che gli utenti vedano l’elenco delle assenze per default.

3. Reindirizzamento alla risorsa “time-offs” quando gli utenti sono autenticati
<Route
  element={
    <Authenticated key="auth-pages" fallback={<Outlet />}>
      <NavigateToResource resource="time-offs" />
    </Authenticated>
  }
>
  <Route path="/login" element={<PageLogin />} />
</Route>

Quando gli utenti sono autenticati, li reindirizziamo alla risorsa time-offs. Se non sono autenticati, vedono la pagina di accesso.

4. Aggiunta della rotta /employee/time-offs
<Route
  path="employee"
  element={
    <ThemeProvider role={Role.EMPLOYEE}>
      <Layout>
        <Outlet />
      </Layout>
    </ThemeProvider>
  }
>
  <Route path="time-offs" element={<Outlet />}>
    <Route index element={<PageEmployeeTimeOffsList />} />
  </Route>
</Route>

Organizziamo le pagine degli impiegati utilizzando rotte annidate. Prima, creiamo una rotta principale con path='employee' che racchiude il contenuto in un tema e layout specifici per gli impiegati. All’interno di questa rotta, aggiungiamo path='time-offs', che visualizza il componente PageEmployeeTimeOffsList. Questa struttura raggruppa tutte le funzionalità degli impiegati sotto un’unica rotta e mantiene lo stile coerente.

Dopo aver apportato queste modifiche, puoi navigare alla rotta /employee/time-offs per vedere la pagina dell’elenco delle assenze in azione.

/employee/time-offs

Al momento, la pagina dell’elenco delle assenze è funzionale, ma manca la possibilità di creare nuove richieste di assenza. Aggiungiamo la possibilità di creare nuove richieste di assenza.

Creazione della pagina per le richieste di assenza

Creeremo una nuova pagina per richiedere o assegnare giorni di assenza. Questa pagina includerà un modulo in cui gli utenti possono specificare il tipo di assenza, le date di inizio e fine e eventuali note aggiuntive.

Prima di iniziare, dobbiamo creare nuovi componenti da utilizzare nel modulo:

Creazione del componente <TimeOffFormSummary />

Crea un nuovo file chiamato form-summary.tsx nella cartella src/components/time-offs/ e aggiungi il seguente codice:

src/components/time-offs/form-summary.tsx

import { Box, Divider, Typography } from "@mui/material";

type Props = {
  availableAnnualDays: number;
  requestedDays: number;
};

export const TimeOffFormSummary = (props: Props) => {
  const remainingDays = props.availableAnnualDays - props.requestedDays;

  return (
    <Box
      sx={{
        display: "flex",
        flexDirection: "column",
        alignItems: "flex-end",
        gap: "16px",
        whiteSpace: "nowrap",
      }}
    >
      <Box
        sx={{
          display: "flex",
          gap: "16px",
        }}
      >
        <Typography variant="body2" color="text.secondary">
          Available Annual Leave Days:
        </Typography>
        <Typography variant="body2">{props.availableAnnualDays}</Typography>
      </Box>

      <Box
        sx={{
          display: "flex",
          gap: "16px",
        }}
      >
        <Typography variant="body2" color="text.secondary">
          Requested Days:
        </Typography>
        <Typography variant="body2">{props.requestedDays}</Typography>
      </Box>

      <Divider
        sx={{
          width: "100%",
        }}
      />
      <Box
        sx={{
          display: "flex",
          gap: "16px",
          height: "40px",
        }}
      >
        <Typography variant="body2" color="text.secondary">
          Remaining Days:
        </Typography>
        <Typography variant="body2" fontWeight={500}>
          {remainingDays}
        </Typography>
      </Box>
    </Box>
  );
};

<TimeOffFormSummary />

Il componente <TimeOffFormSummary /> visualizza un riepilogo della richiesta di assenza. Mostra i giorni di ferie annuali disponibili, il numero di giorni richiesti e i giorni rimanenti. Utilizzeremo questo componente nel modulo di assenza per fornire agli utenti una chiara panoramica della loro richiesta.

Creazione del componente <PageEmployeeTimeOffsCreate />

Crea un nuovo file chiamato create.tsx nella cartella src/pages/employee/time-offs/ e aggiungi il seguente codice:

src/pages/time-offs/create.tsx
import { useCan, useGetIdentity, type HttpError } from "@refinedev/core";
import { useForm } from "@refinedev/react-hook-form";
import { Controller } from "react-hook-form";
import type { DateRange } from "@mui/x-date-pickers-pro/models";
import { Box, Button, MenuItem, Select, Typography } from "@mui/material";
import dayjs from "dayjs";
import { PageHeader } from "@/components/layout/page-header";
import { InputText } from "@/components/input/text";
import { LoadingOverlay } from "@/components/loading-overlay";
import { InputDateStartsEnds } from "@/components/input/date-starts-ends";
import { TimeOffFormSummary } from "@/components/time-offs/form-summary";
import { ThemeProvider } from "@/providers/theme-provider";
import {
  type Employee,
  type TimeOff,
  TimeOffType,
  TimeOffStatus,
  Role,
} from "@/types";
import { CheckRectangleIcon } from "@/icons";

type FormValues = Omit<TimeOff, "id" | "notes"> & {
  notes: string;
  dates: DateRange<dayjs.Dayjs>;
};

export const PageEmployeeTimeOffsCreate = () => {
  const { data: useCanData } = useCan({
    action: "manager",
    params: {
      resource: {
        name: "time-offs",
        meta: {
          scope: "manager",
        },
      },
    },
  });
  const isManager = useCanData?.can;

  const { data: employee } =
    useGetIdentity<Employee>();

  const {
    refineCore: { formLoading, onFinish },
    ...formMethods
  } = useForm<TimeOff, HttpError, FormValues>({
    defaultValues: {
      timeOffType: TimeOffType.ANNUAL,
      notes: "",
      dates: [null, null],
    },
    refineCoreProps: {
      successNotification: () => {
        return {
          message: isManager
            ? "Time off assigned"
            : "Your time off request is submitted for review.",
          type: "success",
        };
      },
    },
  });
  const { control, handleSubmit, formState, watch } = formMethods;

  const onFinishHandler = async (values: FormValues) => {
    const payload: FormValues = {
      ...values,
      startsAt: dayjs(values.dates[0]).format("YYYY-MM-DD"),
      endsAt: dayjs(values.dates[1]).format("YYYY-MM-DD"),
      ...(isManager && {
        status: TimeOffStatus.APPROVED,
      }),
    };
    await onFinish(payload);
  };

  const timeOffType = watch("timeOffType");
  const selectedDays = watch("dates");
  const startsAt = selectedDays[0];
  const endsAt = selectedDays[1];
  const availableAnnualDays = employee?.availableAnnualLeaveDays ?? 0;
  const requestedDays =
    startsAt && endsAt ? endsAt.diff(startsAt, "day") + 1 : 0;

  return (
    <ThemeProvider role={isManager ? Role.MANAGER : Role.EMPLOYEE}>
      <LoadingOverlay loading={formLoading}>
        <Box>
          <PageHeader
            title={isManager ? "Assign Time Off" : "Request Time Off"}
            showListButton
            showDivider
          />

          <Box
            component="form"
            onSubmit={handleSubmit(onFinishHandler)}
            sx={{
              display: "flex",
              flexDirection: "column",
              gap: "24px",
              marginTop: "24px",
            }}
          >
            <Box>
              <Typography
                variant="body2"
                sx={{
                  mb: "8px",
                }}
              >
                Time Off Type
              </Typography>
              <Controller
                name="timeOffType"
                control={control}
                render={({ field }) => (
                  <Select
                    {...field}
                    size="small"
                    sx={{
                      minWidth: "240px",
                      height: "40px",
                      "& .MuiSelect-select": {
                        paddingBlock: "10px",
                      },
                    }}
                  >
                    <MenuItem value={TimeOffType.ANNUAL}>Annual Leave</MenuItem>
                    <MenuItem value={TimeOffType.CASUAL}>Casual Leave</MenuItem>
                    <MenuItem value={TimeOffType.SICK}>Sick Leave</MenuItem>
                  </Select>
                )}
              />
            </Box>

            <Box>
              <Typography
                variant="body2"
                sx={{
                  mb: "16px",
                }}
              >
                Requested Dates
              </Typography>
              <Controller
                name="dates"
                control={control}
                rules={{
                  validate: (value) => {
                    if (!value[0] || !value[1]) {
                      return "Please select both start and end dates";
                    }

                    return true;
                  },
                }}
                render={({ field }) => {
                  return (
                    <Box
                      sx={{
                        display: "grid",
                        gridTemplateColumns: () => {
                          return {
                            sm: "1fr",
                            lg: "628px 1fr",
                          };
                        },
                        gap: "40px",
                      }}
                    >
                      <InputDateStartsEnds
                        {...field}
                        error={formState.errors.dates?.message}
                        availableAnnualDays={availableAnnualDays}
                        requestedDays={requestedDays}
                      />
                      {timeOffType === TimeOffType.ANNUAL && (
                        <Box
                          sx={{
                            display: "flex",
                            maxWidth: "628px",
                            alignItems: () => {
                              return {
                                lg: "flex-end",
                              };
                            },
                            justifyContent: () => {
                              return {
                                xs: "flex-end",
                                lg: "flex-start",
                              };
                            },
                          }}
                        >
                          <TimeOffFormSummary
                            availableAnnualDays={availableAnnualDays}
                            requestedDays={requestedDays}
                          />
                        </Box>
                      )}
                    </Box>
                  );
                }}
              />
            </Box>

            <Box
              sx={{
                maxWidth: "628px",
              }}
            >
              <Controller
                name="notes"
                control={control}
                render={({ field, fieldState }) => {
                  return (
                    <InputText
                      {...field}
                      label="Notes"
                      error={fieldState.error?.message}
                      placeholder="Place enter your notes"
                      multiline
                      rows={3}
                    />
                  );
                }}
              />
            </Box>

            <Button
              variant="contained"
              size="large"
              type="submit"
              startIcon={isManager ? <CheckRectangleIcon /> : undefined}
            >
              {isManager ? "Assign" : "Send Request"}
            </Button>
          </Box>
        </Box>
      </LoadingOverlay>
    </ThemeProvider>
  );
};

<PageEmployeeTimeOffsCreate />

Il componente <PageEmployeeTimeOffsCreate /> visualizza un modulo per creare nuove richieste di assenza in un’app di gestione HR. Sia i dipendenti che i manager possono utilizzarlo per richiedere o assegnare assenze. Il modulo include opzioni per selezionare il tipo di assenza, scegliere le date di inizio e fine, aggiungere note e mostra un riepilogo dell’assenza richiesta.

Analizziamo le parti chiave del componente:

1. Controllo del ruolo utente

const { data: useCanData } = useCan({
  action: "manager",
  params: {
    resource: {
      name: "time-offs",
      meta: {
        scope: "manager",
      },
    },
  },
});
const isManager = useCanData?.can;

Con l’useCan hook, controlliamo se l’utente attuale ha i permessi di manager. Questo determina se l’utente può assegnare un permesso o solo richiederlo. Gestiremo l’invio del modulo in modo diverso in onFinishHandler in base al ruolo dell’utente.

2. Stato del Modulo e Invio


 const {
  refineCore: { formLoading, onFinish },
  ...formMethods
} = useForm<TimeOff, HttpError, FormValues>({
  defaultValues: {
    timeOffType: TimeOffType.ANNUAL,
    notes: "",
    dates: [null, null],
  },
  refineCoreProps: {
    successNotification: () => {
      return {
        message: isManager
          ? "Time off assigned"
          : "Your time off request is submitted for review.",
        type: "success",
      };
    },
  },
});
const { control, handleSubmit, formState, watch } = formMethods;

  const onFinishHandler = async (values: FormValues) => {
    const payload: FormValues = {
      ...values,
      startsAt: dayjs(values.dates[0]).format("YYYY-MM-DD"),
      endsAt: dayjs(values.dates[1]).format("YYYY-MM-DD"),
      ...(isManager && {
        status: TimeOffStatus.APPROVED,
      }),
    };
    await onFinish(payload);
  };

useForm inizializza il modulo con valori predefiniti e imposta le notifiche di successo in base al ruolo dell’utente. La funzione onFinishHandler elabora i dati del modulo prima di inviarli. Per i manager, lo stato viene impostato su APPROVED immediatamente, mentre le richieste dei dipendenti vengono inviate per revisione.

3. Stile

<ThemeProvider role={isManager ? Role.MANAGER : Role.EMPLOYEE}>
  {/* ... */}
</ThemeProvider>

<Button
  variant="contained"
  size="large"
  type="submit"
  startIcon={isManager ? <CheckRectangleIcon /> : undefined}
>
  {isManager ? "Assign" : "Send Request"}
</Button>

Nel nostro design, il colore principale cambia in base al ruolo dell’utente. Utilizziamo il <ThemeProvider /> per applicare il tema corretto di conseguenza. Il testo e l’icona del pulsante di invio cambiano anche a seconda che l’utente sia un manager o un dipendente.

4. Aggiunta della Route “/employee/time-offs/create”

Dobbiamo aggiungere la nuova route per la pagina di creazione del permesso. Aggiorniamo il file App.tsx per includere questa route:

src/App.tsx
import { Authenticated, ErrorComponent, Refine } from '@refinedev/core'
import { DevtoolsProvider, DevtoolsPanel } from '@refinedev/devtools'
import dataProvider from '@refinedev/nestjsx-crud'
import routerProvider, {
  UnsavedChangesNotifier,
  DocumentTitleHandler,
  NavigateToResource,
} from '@refinedev/react-router-v6'
import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom'
import { Toaster } from 'react-hot-toast'

import { PageEmployeeTimeOffsList } from '@/pages/employee/time-offs/list'
import { PageEmployeeTimeOffsCreate } from '@/pages/employee/time-offs/create'
import { PageLogin } from '@/pages/login'

import { Layout } from '@/components/layout'

import { ThemeProvider } from '@/providers/theme-provider'
import { authProvider } from '@/providers/auth-provider'
import { accessControlProvider } from '@/providers/access-control'
import { useNotificationProvider } from '@/providers/notification-provider'
import { queryClient } from '@/providers/query-client'

import { BASE_URL } from '@/utilities/constants'
import { axiosInstance } from '@/utilities/axios'

import { TimeOffIcon } from '@/icons'

import { Role } from '@/types'

import '@/utilities/init-dayjs'

function App() {
  return (
    <BrowserRouter>
      <ThemeProvider>
        <DevtoolsProvider>
          <Refine
            authProvider={authProvider}
            routerProvider={routerProvider}
            dataProvider={dataProvider(BASE_URL, axiosInstance)}
            notificationProvider={useNotificationProvider}
            resources={[
              {
                name: 'employee',
                meta: {
                  scope: Role.EMPLOYEE,
                  order: 2,
                },
              },
              {
                name: 'manager',
                meta: {
                  scope: Role.MANAGER,
                  order: 1,
                },
              },
              {
                name: 'time-offs',
                list: '/employee/time-offs',
                create: '/employee/time-offs/new',
                meta: {
                  parent: 'employee',
                  scope: Role.EMPLOYEE,
                  label: 'Time Off',
                  icon: <TimeOffIcon />,
                },
              },
            ]}
            accessControlProvider={accessControlProvider}
            options={{
              reactQuery: {
                clientConfig: queryClient,
              },
              syncWithLocation: true,
              warnWhenUnsavedChanges: true,
              useNewQueryKeys: true,
            }}>
            <Routes>
              <Route
                element={
                  <Authenticated key='authenticated-routes' redirectOnFail='/login'>
                    <Outlet />
                  </Authenticated>
                }>
                <Route index element={<NavigateToResource resource='time-offs' />} />

                <Route
                  path='employee'
                  element={
                    <ThemeProvider role={Role.EMPLOYEE}>
                      <Layout>
                        <Outlet />
                      </Layout>
                    </ThemeProvider>
                  }>
                  <Route path='time-offs' element={<Outlet />}>
                    <Route index element={<PageEmployeeTimeOffsList />} />
                    <Route path='new' element={<PageEmployeeTimeOffsCreate />} />
                  </Route>
                </Route>
              </Route>

              <Route
                element={
                  <Authenticated key='auth-pages' fallback={<Outlet />}>
                    <NavigateToResource resource='time-offs' />
                  </Authenticated>
                }>
                <Route path='/login' element={<PageLogin />} />
              </Route>

              <Route
                element={
                  <Authenticated key='catch-all'>
                    <Layout>
                      <Outlet />
                    </Layout>
                  </Authenticated>
                }>
                <Route path='*' element={<ErrorComponent />} />
              </Route>
            </Routes>

            <UnsavedChangesNotifier />
            <DocumentTitleHandler />
            <Toaster position='bottom-right' reverseOrder={false} />
            <DevtoolsPanel />
          </Refine>
        </DevtoolsProvider>
      </ThemeProvider>
    </BrowserRouter>
  )
}

export default App


Dopo aver aggiunto queste modifiche, puoi navigare alla route /employee/time-offs/create o fare clic sul pulsante “Assegna Permesso” nella pagina dell’elenco dei permessi per accedere al modulo di creazione del permesso.

/employee/time-offs/create

Passo 5 — Costruzione Pagina di Gestione delle Richieste di Permesso

In questo passo, creeremo una nuova pagina per gestire le richieste di permesso. Questa pagina permetterà ai manager di esaminare e approvare o respingere le richieste di permesso inviate dagli dipendenti.

/manager/requests

Costruzione Pagina Elenco Richieste di Permesso

Ci occuperemo di creare una nuova pagina per la gestione delle richieste di permesso. Questa pagina includerà un elenco delle richieste di permesso, mostrando dettagli come il nome del dipendente, il tipo di permesso richiesto, le date richieste e lo stato attuale.

Prima di iniziare, dobbiamo creare nuovi componenti da utilizzare nell’elenco:

Costruzione <RequestsList /> Componente

Crea un nuovo file chiamato list.tsx nella cartella src/components/requests/ e aggiungi il seguente codice:

src/components/requests/list.tsx
import type { ReactNode } from "react";
import InfiniteScroll from "react-infinite-scroll-component";
import {
  Box,
  Button,
  CircularProgress,
  Skeleton,
  Typography,
} from "@mui/material";

type Props = {
  dataLength: number;
  hasMore: boolean;
  scrollableTarget: string;
  loading: boolean;
  noDataText: string;
  noDataIcon: ReactNode;
  children: ReactNode;
  next: () => void;
};

export const RequestsList = (props: Props) => {
  const hasData = props.dataLength > 0 || props.loading;
  if (!hasData) {
    return (
      <Box
        sx={{
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          flexDirection: "column",
          gap: "24px",
        }}
      >
        {props.noDataIcon}
        <Typography variant="body2" color="text.secondary">
          {props.noDataText || "No data."}
        </Typography>
      </Box>
    );
  }

  return (
    <Box
      sx={{
        position: "relative",
      }}
    >
      <Box
        id={props.scrollableTarget}
        sx={(theme) => ({
          maxHeight: "600px",
          [theme.breakpoints.up("lg")]: {
            height: "600px",
          },
          overflow: "auto",
          ...((props.dataLength > 6 || props.loading) && {
            "&::after": {
              pointerEvents: "none",
              content: '""',
              zIndex: 1,
              position: "absolute",
              bottom: "0",
              left: "12px",
              right: "12px",
              width: "calc(100% - 24px)",
              height: "60px",
              background:
                "linear-gradient(180deg, rgba(255, 255, 255, 0), #FFFFFF)",
            },
          }),
        })}
      >
        <InfiniteScroll
          dataLength={props.dataLength}
          hasMore={props.hasMore}
          next={props.next}
          scrollableTarget={props.scrollableTarget}
          endMessage={
            !props.loading &&
            props.dataLength > 6 && (
              <Box
                sx={{
                  pt: "40px",
                }}
              />
            )
          }
          loader={
            <Box
              sx={{
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                width: "100%",
                height: "100px",
              }}
            >
              <CircularProgress size={24} />
            </Box>
          }
        >
          <Box
            sx={{
              display: "flex",
              flexDirection: "column",
            }}
          >
            {props.loading ? <SkeletonList /> : props.children}
          </Box>
        </InfiniteScroll>
      </Box>
    </Box>
  );
};

const SkeletonList = () => {
  return (
    <>
      {[...Array(6)].map((_, index) => (
        <Box
          key={index}
          sx={(theme) => ({
            paddingRight: "24px",
            paddingLeft: "24px",
            display: "flex",
            flexDirection: "column",
            justifyContent: "flex-end",
            gap: "12px",
            paddingTop: "12px",
            paddingBottom: "4px",

            [theme.breakpoints.up("sm")]: {
              paddingTop: "20px",
              paddingBottom: "12px",
            },

            "& .MuiSkeleton-rectangular": {
              borderRadius: "2px",
            },
          })}
        >
          <Skeleton variant="rectangular" width="64px" height="12px" />
          <Box
            sx={{
              display: "flex",
              alignItems: "center",
              gap: "24px",
            }}
          >
            <Skeleton
              variant="circular"
              width={48}
              height={48}
              sx={{
                flexShrink: 0,
              }}
            />
            <Box
              sx={(theme) => ({
                height: "auto",
                width: "100%",
                [theme.breakpoints.up("md")]: {
                  height: "48px",
                },
                display: "flex",
                flex: 1,
                flexDirection: "column",
                justifyContent: "center",
                gap: "8px",
              })}
            >
              <Skeleton
                variant="rectangular"
                sx={(theme) => ({
                  width: "100%",
                  [theme.breakpoints.up("sm")]: {
                    width: "120px",
                  },
                })}
                height="16px"
              />
              <Skeleton
                variant="rectangular"
                sx={(theme) => ({
                  width: "100%",
                  [theme.breakpoints.up("sm")]: {
                    width: "230px",
                  },
                })}
                height="12px"
              />
            </Box>
            <Button
              size="small"
              color="inherit"
              sx={(theme) => ({
                display: "none",
                [theme.breakpoints.up("sm")]: {
                  display: "block",
                },

                alignSelf: "flex-start",
                flexShrink: 0,
                marginLeft: "auto",
              })}
            >
              View Request
            </Button>
          </Box>
        </Box>
      ))}
    </>
  );
};

Il componente <RequestsList /> visualizza un elenco di richieste di permesso con scorrimento infinito. Include un indicatore di caricamento, segnaposto scheletro e un messaggio quando non ci sono dati. Questo componente è progettato per gestire grandi set di dati in modo efficiente e offrire un’esperienza utente fluida.

Costruzione del componente <RequestsListItem />

Crea un nuovo file chiamato list-item.tsx nella cartella src/components/requests/ e aggiungi il seguente codice:

src/components/requests/list-item.tsx
import { Box, Typography, Avatar, Button } from "@mui/material";
import type { ReactNode } from "react";

type Props = {
  date: string;
  avatarURL: string;
  title: string;
  descriptionIcon?: ReactNode;
  description: string;
  onClick?: () => void;
  showTimeSince?: boolean;
};

export const RequestsListItem = ({
  date,
  avatarURL,
  title,
  descriptionIcon,
  description,
  onClick,
  showTimeSince,
}: Props) => {
  return (
    <Box
      role="button"
      onClick={onClick}
      sx={(theme) => ({
        cursor: "pointer",
        paddingRight: "24px",
        paddingLeft: "24px",

        paddingTop: "4px",
        paddingBottom: "4px",
        [theme.breakpoints.up("sm")]: {
          paddingTop: "12px",
          paddingBottom: "12px",
        },

        "&:hover": {
          backgroundColor: theme.palette.action.hover,
        },
      })}
    >
      {showTimeSince && (
        <Box
          sx={{
            marginBottom: "8px",
          }}
        >
          <Typography variant="caption" color="textSecondary">
            {date}
          </Typography>
        </Box>
      )}
      <Box
        sx={{
          display: "flex",
        }}
      >
        <Avatar
          src={avatarURL}
          alt={title}
          sx={{ width: "48px", height: "48px" }}
        />
        <Box
          sx={(theme) => ({
            height: "auto",
            [theme.breakpoints.up("md")]: {
              height: "48px",
            },
            width: "100%",
            display: "flex",
            flexWrap: "wrap",
            justifyContent: "space-between",
            gap: "4px",
            marginLeft: "16px",
          })}
        >
          <Box>
            <Typography variant="body2" fontWeight={500} lineHeight="24px">
              {title}
            </Typography>
            <Box
              sx={{
                display: "flex",
                alignItems: "center",
                gap: "8px",
                minWidth: "260px",
              }}
            >
              {descriptionIcon}
              <Typography variant="caption" color="textSecondary">
                {description}
              </Typography>
            </Box>
          </Box>

          {onClick && (
            <Button
              size="small"
              color="inherit"
              onClick={onClick}
              sx={{
                alignSelf: "flex-start",
                flexShrink: 0,
                marginLeft: "auto",
              }}
            >
              View Request
            </Button>
          )}
        </Box>
      </Box>
    </Box>
  );
};

Il componente <RequestsListItem /> visualizza una singola richiesta di permesso nella lista. Include l’avatar dell’impiegato, il nome, la descrizione e un pulsante per visualizzare i dettagli della richiesta. Questo componente è riutilizzabile e può essere utilizzato per rendere ogni elemento nell’elenco delle richieste di permesso.

Costruzione del componente <PageManagerRequestsList />

Crea un nuovo file chiamato list.tsx nella cartella src/pages/manager/requests/ e aggiungi il seguente codice:

import type { PropsWithChildren } from "react";
import { useGo, useInfiniteList } from "@refinedev/core";
import { Box, Typography } from "@mui/material";
import dayjs from "dayjs";
import { Frame } from "@/components/frame";
import { PageHeader } from "@/components/layout/page-header";
import { RequestsListItem } from "@/components/requests/list-item";
import { RequestsList } from "@/components/requests/list";
import { indigo } from "@/providers/theme-provider/colors";
import { TimeOffIcon, RequestTypeIcon, NoTimeOffIcon } from "@/icons";
import { TimeOffStatus, type Employee, type TimeOff } from "@/types";

export const PageManagerRequestsList = ({ children }: PropsWithChildren) => {
  return (
    <>
      <Box>
        <PageHeader title="Awaiting Requests" />
        <TimeOffsList />
      </Box>
      {children}
    </>
  );
};

const TimeOffsList = () => {
  const go = useGo();

  const {
    data: timeOffsData,
    isLoading: timeOffsLoading,
    fetchNextPage: timeOffsFetchNextPage,
    hasNextPage: timeOffsHasNextPage,
  } = useInfiniteList<
    TimeOff & {
      employee: Employee;
    }
  >({
    resource: "time-offs",
    filters: [
      { field: "status", operator: "eq", value: TimeOffStatus.PENDING },
    ],
    sorters: [{ field: "createdAt", order: "desc" }],
    meta: {
      join: ["employee"],
    },
  });

  const timeOffs = timeOffsData?.pages.flatMap((page) => page.data) || [];
  const totalCount = timeOffsData?.pages[0].total;

  return (
    <Frame
      title="Time off Requests"
      titleSuffix={
        !!totalCount &&
        totalCount > 0 && (
          <Box
            sx={{
              padding: "4px",
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              minWidth: "24px",
              height: "24px",
              borderRadius: "4px",
              backgroundColor: indigo[100],
            }}
          >
            <Typography
              variant="caption"
              sx={{
                color: indigo[500],
                fontSize: "12px",
                lineHeight: "16px",
              }}
            >
              {totalCount}
            </Typography>
          </Box>
        )
      }
      icon={<TimeOffIcon width={24} height={24} />}
      sx={{
        flex: 1,
        paddingBottom: "0px",
      }}
      sxChildren={{
        padding: 0,
      }}
    >
      <RequestsList
        loading={timeOffsLoading}
        dataLength={timeOffs.length}
        hasMore={timeOffsHasNextPage || false}
        next={timeOffsFetchNextPage}
        scrollableTarget="scrollableDiv-timeOffs"
        noDataText="No time off requests right now."
        noDataIcon={<NoTimeOffIcon />}
      >
        {timeOffs.map((timeOff) => {
          const date = dayjs(timeOff.createdAt).fromNow();
          const fullName = `${timeOff.employee.firstName} ${timeOff.employee.lastName}`;
          const avatarURL = timeOff.employee.avatarUrl;
          const requestedDay =
            dayjs(timeOff.endsAt).diff(dayjs(timeOff.startsAt), "day") + 1;
          const description = `Requested ${requestedDay} ${
            requestedDay > 1 ? "days" : "day"
          } of time  ${timeOff.timeOffType.toLowerCase()} leave.`;

          return (
            <RequestsListItem
              key={timeOff.id}
              date={date}
              avatarURL={avatarURL}
              title={fullName}
              showTimeSince
              descriptionIcon={<RequestTypeIcon type={timeOff.timeOffType} />}
              description={description}
              onClick={() => {
                go({
                  type: "replace",
                  to: {
                    resource: "requests",
                    id: timeOff.id,
                    action: "edit",
                  },
                });
              }}
            />
          );
        })}
      </RequestsList>
    </Frame>
  );
};

Il componente <PageManagerRequestsList /> visualizza le richieste di permesso in sospeso che i manager devono approvare. Mostra dettagli come il nome dell’impiegato, il tipo di permesso, le date richieste e da quanto tempo è stata fatta la richiesta. I manager possono fare clic su una richiesta per vedere ulteriori dettagli. Utilizza <RequestsList /> e <RequestsListItem /> per rendere l’elenco.

Questo componente accetta anche children come prop. Successivamente, implementeremo un percorso modale utilizzando <Outlet /> per visualizzare i dettagli della richiesta, rendendo il percorso /manager/requests/:id all’interno del componente.

Aggiunta del percorso “/manager/requests”

Dobbiamo aggiungere la nuova route per la pagina di gestione delle richieste di permesso. Aggiorniamo il file App.tsx per includere questa route:

src/App.tsx
import { Authenticated, ErrorComponent, Refine } from '@refinedev/core'
import { DevtoolsProvider, DevtoolsPanel } from '@refinedev/devtools'
import dataProvider from '@refinedev/nestjsx-crud'
import routerProvider, {
  UnsavedChangesNotifier,
  DocumentTitleHandler,
  NavigateToResource,
} from '@refinedev/react-router-v6'
import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom'
import { Toaster } from 'react-hot-toast'

import { PageEmployeeTimeOffsList } from '@/pages/employee/time-offs/list'
import { PageEmployeeTimeOffsCreate } from '@/pages/employee/time-offs/create'
import { PageLogin } from '@/pages/login'

import { Layout } from '@/components/layout'

import { ThemeProvider } from '@/providers/theme-provider'
import { authProvider } from '@/providers/auth-provider'
import { accessControlProvider } from '@/providers/access-control'
import { useNotificationProvider } from '@/providers/notification-provider'
import { queryClient } from '@/providers/query-client'

import { BASE_URL } from '@/utilities/constants'
import { axiosInstance } from '@/utilities/axios'

import { RequestsIcon, TimeOffIcon } from '@/icons'

import { Role } from '@/types'

import '@/utilities/init-dayjs'
import { PageManagerRequestsList } from './pages/manager/requests/list'

function App() {
  return (
    <BrowserRouter>
      <ThemeProvider>
        <DevtoolsProvider>
          <Refine
            authProvider={authProvider}
            routerProvider={routerProvider}
            dataProvider={dataProvider(BASE_URL, axiosInstance)}
            notificationProvider={useNotificationProvider}
            resources={[
              {
                name: 'employee',
                meta: {
                  scope: Role.EMPLOYEE,
                  order: 2,
                },
              },
              {
                name: 'manager',
                meta: {
                  scope: Role.MANAGER,
                  order: 1,
                },
              },
              {
                name: 'time-offs',
                list: '/employee/time-offs',
                create: '/employee/time-offs/new',
                meta: {
                  parent: 'employee',
                  scope: Role.EMPLOYEE,
                  label: 'Time Off',
                  icon: <TimeOffIcon />,
                },
              },
              {
                name: 'time-offs',
                list: '/manager/requests',
                identifier: 'requests',
                meta: {
                  parent: 'manager',
                  scope: Role.MANAGER,
                  label: 'Requests',
                  icon: <RequestsIcon />,
                },
              },
            ]}
            accessControlProvider={accessControlProvider}
            options={{
              reactQuery: {
                clientConfig: queryClient,
              },
              syncWithLocation: true,
              warnWhenUnsavedChanges: true,
              useNewQueryKeys: true,
            }}>
            <Routes>
              <Route
                element={
                  <Authenticated key='authenticated-routes' redirectOnFail='/login'>
                    <Outlet />
                  </Authenticated>
                }>
                <Route index element={<NavigateToResource resource='time-offs' />} />

                <Route
                  path='employee'
                  element={
                    <ThemeProvider role={Role.EMPLOYEE}>
                      <Layout>
                        <Outlet />
                      </Layout>
                    </ThemeProvider>
                  }>
                  <Route path='time-offs' element={<Outlet />}>
                    <Route index element={<PageEmployeeTimeOffsList />} />
                    <Route path='new' element={<PageEmployeeTimeOffsCreate />} />
                  </Route>
                </Route>
              </Route>

              <Route
                path='manager'
                element={
                  <ThemeProvider role={Role.MANAGER}>
                    <Layout>
                      <Outlet />
                    </Layout>
                  </ThemeProvider>
                }>
                <Route path='requests' element={<Outlet />}>
                  <Route index element={<PageManagerRequestsList />} />
                </Route>
              </Route>

              <Route
                element={
                  <Authenticated key='auth-pages' fallback={<Outlet />}>
                    <NavigateToResource resource='time-offs' />
                  </Authenticated>
                }>
                <Route path='/login' element={<PageLogin />} />
              </Route>

              <Route
                element={
                  <Authenticated key='catch-all'>
                    <Layout>
                      <Outlet />
                    </Layout>
                  </Authenticated>
                }>
                <Route path='*' element={<ErrorComponent />} />
              </Route>
            </Routes>

            <UnsavedChangesNotifier />
            <DocumentTitleHandler />
            <Toaster position='bottom-right' reverseOrder={false} />
            <DevtoolsPanel />
          </Refine>
        </DevtoolsProvider>
      </ThemeProvider>
    </BrowserRouter>
  )
}

export default App

Dopo aver apportato queste modifiche, è possibile navigare alla route /manager/requests per visualizzare la pagina di gestione delle richieste di permesso in azione

/manager/requests

Costruzione della Pagina dei Dettagli della Richiesta di Permesso

In questo passaggio, creeremo una nuova pagina per visualizzare i dettagli di una richiesta di permesso. Questa pagina mostrerà il nome dell’impiegato, il tipo di permesso richiesto, le date richieste e lo stato attuale. I manager possono approvare o rifiutare la richiesta da questa pagina.

Costruzione del Componente <TimeOffRequestModal />

Per prima cosa, crea un file chiamato use-get-employee-time-off-usage nella cartella src/hooks/ e aggiungi il seguente codice:

src/hooks/use-get-employee-time-off-usage.ts
import { useList } from "@refinedev/core";
import { type TimeOff, TimeOffStatus, TimeOffType } from "@/types";
import { useMemo } from "react";
import dayjs from "dayjs";

export const useGetEmployeeTimeOffUsage = ({
  employeeId,
}: { employeeId?: number }) => {
  const query = useList<TimeOff>({
    resource: "time-offs",
    pagination: { pageSize: 999 },
    filters: [
      {
        field: "status",
        operator: "eq",
        value: TimeOffStatus.APPROVED,
      },
      {
        field: "employeeId",
        operator: "eq",
        value: employeeId,
      },
    ],
    queryOptions: {
      enabled: !!employeeId,
    },
  });
  const data = query?.data?.data;

  const { sick, casual, annual, sickCount, casualCount, annualCount } =
    useMemo(() => {
      const sick: TimeOff[] = [];
      const casual: TimeOff[] = [];
      const annual: TimeOff[] = [];
      let sickCount = 0;
      let casualCount = 0;
      let annualCount = 0;

      data?.forEach((timeOff) => {
        const duration =
          dayjs(timeOff.endsAt).diff(dayjs(timeOff.startsAt), "days") + 1;

        if (timeOff.timeOffType === TimeOffType.SICK) {
          sick.push(timeOff);
          sickCount += duration;
        } else if (timeOff.timeOffType === TimeOffType.CASUAL) {
          casual.push(timeOff);
          casualCount += duration;
        } else if (timeOff.timeOffType === TimeOffType.ANNUAL) {
          annual.push(timeOff);
          annualCount += duration;
        }
      });

      return {
        sick,
        casual,
        annual,
        sickCount,
        casualCount,
        annualCount,
      };
    }, [data]);

  return {
    query,
    sick,
    casual,
    annual,
    sickCount,
    casualCount,
    annualCount,
  };
};

Useremo il hook useGetEmployeeTimeOffUsage per calcolare il numero totale di giorni che un impiegato ha preso per ogni tipo di permesso. Queste informazioni verranno mostrate nella pagina dei dettagli della richiesta di permesso.

Dopo di che, crea un nuovo file chiamato time-off-request-modal.tsx nella cartella src/components/requests/ e aggiungi il seguente codice:

src/components/requests/time-off-request-modal.tsx
import type { ReactNode } from "react";
import { useInvalidate, useList, useUpdate } from "@refinedev/core";
import {
  Avatar,
  Box,
  Button,
  Divider,
  Tooltip,
  Typography,
} from "@mui/material";
import dayjs from "dayjs";
import { Modal } from "@/components/modal";
import {
  TimeOffStatus,
  TimeOffType,
  type Employee,
  type TimeOff,
} from "@/types";
import { RequestTypeIcon, ThumbsDownIcon, ThumbsUpIcon } from "@/icons";
import { useGetEmployeeTimeOffUsage } from "@/hooks/use-get-employee-time-off-usage";

type Props = {
  open: boolean;
  onClose: () => void;
  loading: boolean;
  onSuccess?: () => void;
  timeOff:
    | (TimeOff & {
        employee: Employee;
      })
    | null
    | undefined;
};

export const TimeOffRequestModal = ({
  open,
  timeOff,
  loading: loadingFromProps,
  onClose,
  onSuccess,
}: Props) => {
  const employeeUsedTimeOffs = useGetEmployeeTimeOffUsage({
    employeeId: timeOff?.employee.id,
  });

  const invalidate = useInvalidate();

  const { mutateAsync } = useUpdate<TimeOff>();

  const employee = timeOff?.employee;
  const duration =
    dayjs(timeOff?.endsAt).diff(dayjs(timeOff?.startsAt), "days") + 1;
  const remainingAnnualLeaveDays =
    (employee?.availableAnnualLeaveDays ?? 0) - duration;

  const { data: timeOffsData, isLoading: timeOffsLoading } = useList<
    TimeOff & { employee: Employee }
  >({
    resource: "time-offs",
    pagination: {
      pageSize: 999,
    },
    filters: [
      {
        field: "status",
        operator: "eq",
        value: TimeOffStatus.APPROVED,
      },
      {
        operator: "and",
        value: [
          {
            field: "startsAt",
            operator: "lte",
            value: timeOff?.endsAt,
          },
          {
            field: "endsAt",
            operator: "gte",
            value: timeOff?.startsAt,
          },
        ],
      },
    ],
    queryOptions: {
      enabled: !!timeOff,
    },
    meta: {
      join: ["employee"],
    },
  });
  const whoIsOutList = timeOffsData?.data || [];

  const handleSubmit = async (status: TimeOffStatus) => {
    await mutateAsync({
      resource: "time-offs",
      id: timeOff?.id,
      invalidates: ["resourceAll"],
      values: {
        status,
      },
    });

    onSuccess?.();
    invalidate({
      resource: "employees",
      invalidates: ["all"],
    });
  };

  const loading = timeOffsLoading || loadingFromProps;

  return (
    <Modal
      open={open}
      title="Time Off Request"
      loading={loading}
      sx={{
        maxWidth: "520px",
      }}
      onClose={onClose}
      footer={
        <>
          <Divider />
          <Box
            sx={{
              display: "flex",
              alignItems: "center",
              justifyContent: "space-between",
              gap: "8px",
              padding: "24px",
            }}
          >
            <Button
              sx={{
                backgroundColor: (theme) => theme.palette.error.light,
              }}
              startIcon={<ThumbsDownIcon />}
              onClick={() => handleSubmit(TimeOffStatus.REJECTED)}
            >
              Decline
            </Button>
            <Button
              sx={{
                backgroundColor: (theme) => theme.palette.success.light,
              }}
              onClick={() => handleSubmit(TimeOffStatus.APPROVED)}
              startIcon={<ThumbsUpIcon />}
            >
              Accept
            </Button>
          </Box>
        </>
      }
    >
      <Box
        sx={{
          display: "flex",
          alignItems: "center",
          padding: "24px",
          backgroundColor: (theme) => theme.palette.grey[50],
          borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
        }}
      >
        <Avatar
          src={employee?.avatarUrl}
          alt={employee?.firstName}
          sx={{
            width: "80px",
            height: "80px",
            marginRight: "24px",
          }}
        />
        <Box
          sx={{
            display: "flex",
            flexDirection: "column",
          }}
        >
          <Typography
            variant="h2"
            fontSize="18px"
            lineHeight="28px"
            fontWeight="500"
          >
            {employee?.firstName} {employee?.lastName}
          </Typography>
          <Typography variant="caption">{employee?.jobTitle}</Typography>
          <Typography variant="caption">{employee?.role}</Typography>
        </Box>
      </Box>

      <Box
        sx={{
          padding: "24px",
        }}
      >
        <InfoRow
          loading={loading}
          label="Request Type"
          value={
            <Box
              component="span"
              sx={{
                display: "flex",
                alignItems: "center",
                gap: "8px",
              }}
            >
              <RequestTypeIcon type={timeOff?.timeOffType} />
              <Typography variant="body2" component="span">
                {timeOff?.timeOffType} Leave
              </Typography>
            </Box>
          }
        />
        <Divider />
        <InfoRow
          loading={loading}
          label="Duration"
          value={`${duration > 1 ? `${duration} days` : `${duration} day`}`}
        />
        <Divider />
        <InfoRow
          loading={loading}
          label={
            {
              [TimeOffType.ANNUAL]: "Remaining Annual Leave Days",
              [TimeOffType.SICK]: "Previously Used Sick Leave Days",
              [TimeOffType.CASUAL]: "Previously Used Casual Leave Days",
            }[timeOff?.timeOffType ?? TimeOffType.ANNUAL]
          }
          value={
            {
              [TimeOffType.ANNUAL]: remainingAnnualLeaveDays,
              [TimeOffType.SICK]: employeeUsedTimeOffs.sickCount,
              [TimeOffType.CASUAL]: employeeUsedTimeOffs.casualCount,
            }[timeOff?.timeOffType ?? TimeOffType.ANNUAL]
          }
        />
        <Divider />
        <InfoRow
          loading={loading}
          label="Start Date"
          value={dayjs(timeOff?.startsAt).format("MMMM DD")}
        />
        <Divider />
        <InfoRow
          loading={loading}
          label="End Date"
          value={dayjs(timeOff?.endsAt).format("MMMM DD")}
        />

        <Divider />
        <Box
          sx={{
            display: "flex",
            flexDirection: "column",
            gap: "8px",
            paddingY: "24px",
          }}
        >
          <Typography variant="body2" fontWeight={600}>
            Notes
          </Typography>
          <Typography
            variant="body2"
            sx={{
              height: "20px",
              fontStyle: timeOff?.notes ? "normal" : "italic",
            }}
          >
            {!loading && (timeOff?.notes || "No notes provided.")}
          </Typography>
        </Box>

        <Divider />
        <Box
          sx={{
            display: "flex",
            flexDirection: "column",
            gap: "8px",
            paddingY: "24px",
          }}
        >
          <Typography variant="body2" fontWeight={600}>
            Who's out between these days?
          </Typography>
          <Box
            sx={{
              display: "flex",
              alignItems: "center",
              flexWrap: "wrap",
              gap: "8px",
            }}
          >
            {whoIsOutList.length ? (
              whoIsOutList.map((whoIsOut) => (
                <Tooltip
                  key={whoIsOut.id}
                  sx={{
                    "& .MuiTooltip-tooltip": {
                      background: "red",
                    },
                  }}
                  title={
                    <Box
                      sx={{
                        display: "flex",
                        flexDirection: "column",
                        gap: "2px",
                      }}
                    >
                      <Typography variant="body2">
                        {whoIsOut.employee.firstName}{" "}
                        {whoIsOut.employee.lastName}
                      </Typography>
                      <Typography variant="caption">
                        {whoIsOut.timeOffType} Leave
                      </Typography>
                      <Typography variant="caption">
                        {dayjs(whoIsOut.startsAt).format("MMMM DD")} -{" "}
                        {dayjs(whoIsOut.endsAt).format("MMMM DD")}
                      </Typography>
                    </Box>
                  }
                  placement="top"
                >
                  <Avatar
                    src={whoIsOut.employee.avatarUrl}
                    alt={whoIsOut.employee.firstName}
                    sx={{
                      width: "32px",
                      height: "32px",
                    }}
                  />
                </Tooltip>
              ))
            ) : (
              <Typography
                variant="body2"
                sx={{
                  height: "32px",
                  fontStyle: "italic",
                }}
              >
                {loading ? "" : "No one is out between these days."}
              </Typography>
            )}
          </Box>
        </Box>
      </Box>
    </Modal>
  );
};

const InfoRow = ({
  label,
  value,
  loading,
}: { label: ReactNode; value: ReactNode; loading: boolean }) => {
  return (
    <Box
      sx={{
        display: "flex",
        justifyContent: "space-between",
        paddingY: "24px",
        height: "72px",
      }}
    >
      <Typography variant="body2">{label}</Typography>
      <Typography variant="body2">{loading ? "" : value}</Typography>
    </Box>
  );
};

Suddividiamo il componente <TimeOffRequestModal />:

1. Recupero dell’Utilizzo del Permesso dell’Impiegato

Il useGetEmployeeTimeOffUsage hook viene utilizzato per recuperare l’uso dei giorni di ferie da parte dell’impiegato. Questo hook calcola i giorni di ferie annuali rimanenti e i giorni di malattia e di ferie non retribuite precedentemente utilizzati in base alla storia delle assenze dell’impiegato.

2. Recupero delle Assenze Approvate Sovrapposte
filters: [
  {
    field: "status",
    operator: "eq",
    value: TimeOffStatus.APPROVED,
  },
  {
    operator: "and",
    value: [
      {
        field: "startsAt",
        operator: "lte",
        value: timeOff?.endsAt,
      },
      {
        field: "endsAt",
        operator: "gte",
        value: timeOff?.startsAt,
      },
    ],
  },
];

Il useList hook con i filtri sopra indicati recupera tutte le assenze approvate che si sovrappongono con la richiesta di assenza attuale. Questa lista viene utilizzata per visualizzare gli impiegati che sono assenti tra le date richieste.

3. Gestione dell’Approvazione/Rifiuto della Richiesta di Assenza

La funzione handleSubmit viene chiamata quando il manager approva o rifiuta la richiesta di assenza.

const invalidate = useInvalidate();

// ...

const handleSubmit = async (status: TimeOffStatus) => {
  await mutateAsync({
    resource: "time-offs",
    id: timeOff?.id,
    invalidates: ["resourceAll"],
    values: {
      status,
    },
  });

  onSuccess?.();
  invalidate({
    resource: "employees",
    invalidates: ["all"],
  });
};

Refine invalida automaticamente la cache delle risorse dopo che la risorsa è stata mutata (time-offs in questo caso). Poiché l’uso delle assenze da parte dell’impiegato viene calcolato in base alla storia delle assenze, invalidiamo anche la cache delle risorse employees per aggiornare l’uso delle assenze dell’impiegato.

Aggiunta della Route “/manager/requests/:id”

In questo passaggio, creeremo una nuova route per visualizzare la pagina dei dettagli della richiesta di assenza, dove i manager possono approvare o rifiutare le richieste.

Creiamo un nuovo file chiamato edit.tsx nella cartella src/pages/manager/requests/time-offs/ e aggiungiamo il seguente codice:

src/pages/manager/requests/time-offs/edit.tsx
import { useGo, useShow } from "@refinedev/core";
import { TimeOffRequestModal } from "@/components/requests/time-off-request-modal";
import type { Employee, TimeOff } from "@/types";

export const PageManagerRequestsTimeOffsEdit = () => {
  const go = useGo();

  const { query: timeOffRequestQuery } = useShow<
    TimeOff & { employee: Employee }
  >({
    meta: {
      join: ["employee"],
    },
  });

  const loading = timeOffRequestQuery.isLoading;

  return (
    <TimeOffRequestModal
      open
      loading={loading}
      timeOff={timeOffRequestQuery?.data?.data}
      onClose={() =>
        go({
          to: {
            resource: "requests",
            action: "list",
          },
        })
      }
      onSuccess={() => {
        go({
          to: {
            resource: "requests",
            action: "list",
          },
        });
      }}
    />
  );
};

Ora dobbiamo aggiungere la nuova route per visualizzare la pagina dei dettagli della richiesta di assenza. Aggiorniamo il file App.tsx per includere questa route:

src/App.tsx
import { Authenticated, ErrorComponent, Refine } from '@refinedev/core'
import { DevtoolsProvider, DevtoolsPanel } from '@refinedev/devtools'
import dataProvider from '@refinedev/nestjsx-crud'
import routerProvider, {
  UnsavedChangesNotifier,
  DocumentTitleHandler,
  NavigateToResource,
} from '@refinedev/react-router-v6'
import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom'
import { Toaster } from 'react-hot-toast'

import { PageEmployeeTimeOffsList } from '@/pages/employee/time-offs/list'
import { PageEmployeeTimeOffsCreate } from '@/pages/employee/time-offs/create'
import { PageManagerRequestsList } from '@/pages/manager/requests/list'
import { PageManagerRequestsTimeOffsEdit } from '@/pages/manager/requests/time-offs/edit'
import { PageLogin } from '@/pages/login'

import { Layout } from '@/components/layout'

import { ThemeProvider } from '@/providers/theme-provider'
import { authProvider } from '@/providers/auth-provider'
import { accessControlProvider } from '@/providers/access-control'
import { useNotificationProvider } from '@/providers/notification-provider'
import { queryClient } from '@/providers/query-client'

import { BASE_URL } from '@/utilities/constants'
import { axiosInstance } from '@/utilities/axios'

import { RequestsIcon, TimeOffIcon } from '@/icons'

import { Role } from '@/types'

import '@/utilities/init-dayjs'

function App() {
  return (
    <BrowserRouter>
      <ThemeProvider>
        <DevtoolsProvider>
          <Refine
            authProvider={authProvider}
            routerProvider={routerProvider}
            dataProvider={dataProvider(BASE_URL, axiosInstance)}
            notificationProvider={useNotificationProvider}
            resources={[
              {
                name: 'employee',
                meta: {
                  scope: Role.EMPLOYEE,
                  order: 2,
                },
              },
              {
                name: 'manager',
                meta: {
                  scope: Role.MANAGER,
                  order: 1,
                },
              },
              {
                name: 'time-offs',
                list: '/employee/time-offs',
                create: '/employee/time-offs/new',
                meta: {
                  parent: 'employee',
                  scope: Role.EMPLOYEE,
                  label: 'Time Off',
                  icon: <TimeOffIcon />,
                },
              },
              {
                name: 'time-offs',
                list: '/manager/requests',
                edit: '/manager/requests/:id/edit',
                identifier: 'requests',
                meta: {
                  parent: 'manager',
                  scope: Role.MANAGER,
                  label: 'Requests',
                  icon: <RequestsIcon />,
                },
              },
            ]}
            accessControlProvider={accessControlProvider}
            options={{
              reactQuery: {
                clientConfig: queryClient,
              },
              syncWithLocation: true,
              warnWhenUnsavedChanges: true,
              useNewQueryKeys: true,
            }}>
            <Routes>
              <Route
                element={
                  <Authenticated key='authenticated-routes' redirectOnFail='/login'>
                    <Outlet />
                  </Authenticated>
                }>
                <Route index element={<NavigateToResource resource='time-offs' />} />

                <Route
                  path='employee'
                  element={
                    <ThemeProvider role={Role.EMPLOYEE}>
                      <Layout>
                        <Outlet />
                      </Layout>
                    </ThemeProvider>
                  }>
                  <Route path='time-offs' element={<Outlet />}>
                    <Route index element={<PageEmployeeTimeOffsList />} />
                    <Route path='new' element={<PageEmployeeTimeOffsCreate />} />
                  </Route>
                </Route>
              </Route>

              <Route
                path='manager'
                element={
                  <ThemeProvider role={Role.MANAGER}>
                    <Layout>
                      <Outlet />
                    </Layout>
                  </ThemeProvider>
                }>
                <Route
                  path='requests'
                  element={
                    <PageManagerRequestsList>
                      <Outlet />
                    </PageManagerRequestsList>
                  }>
                  <Route path=':id/edit' element={<PageManagerRequestsTimeOffsEdit />} />
                </Route>
              </Route>

              <Route
                element={
                  <Authenticated key='auth-pages' fallback={<Outlet />}>
                    <NavigateToResource resource='time-offs' />
                  </Authenticated>
                }>
                <Route path='/login' element={<PageLogin />} />
              </Route>

              <Route
                element={
                  <Authenticated key='catch-all'>
                    <Layout>
                      <Outlet />
                    </Layout>
                  </Authenticated>
                }>
                <Route path='*' element={<ErrorComponent />} />
              </Route>
            </Routes>

            <UnsavedChangesNotifier />
            <DocumentTitleHandler />
            <Toaster position='bottom-right' reverseOrder={false} />
            <DevtoolsPanel />
          </Refine>
        </DevtoolsProvider>
      </ThemeProvider>
    </BrowserRouter>
  )
}

export default App



Esaminiamo più da vicino le modifiche:

<Route
  path="requests"
  element={
    <PageManagerRequestsList>
      <Outlet />
    </PageManagerRequestsList>
  }
>
  <Route path=":id/edit" element={<PageManagerRequestsTimeOffsEdit />} />
</Route>

Il codice sopra imposta una struttura di route annidate in cui viene visualizzato un modulo quando si naviga verso una specifica route figlia. Il componente <PageManagerRequestsTimeOffsEdit /> è un modulo e viene renderizzato come un figlio del componente <PageManagerRequestsList />. Questa struttura ci consente di visualizzare il modulo sopra la pagina dell’elenco mantenendo la pagina dell’elenco visibile sullo sfondo.

Quando si naviga verso la route /manager/requests/:id/edit o si fa clic su una richiesta di tempo libero nell’elenco, la pagina dei dettagli della richiesta di tempo libero verrà visualizzata come un modulo sopra la pagina dell’elenco.

/manager/requests/:id/edit

Passo 6 — Implementazione dell’Autorizzazione e del Controllo degli Accessi

L’autorizzazione è un componente critico nelle applicazioni a livello aziendale, giocando un ruolo chiave sia nella sicurezza che nell’efficienza operativa. Garantisce che solo gli utenti autorizzati possano accedere a risorse specifiche, proteggendo dati e funzionalità sensibili. Il sistema di autorizzazione di Refine fornisce l’infrastruttura necessaria per proteggere le tue risorse e garantire che gli utenti interagiscano con la tua applicazione in modo sicuro e controllato. In questo passaggio, implementeremo l’autorizzazione e il controllo degli accessi per la gestione delle richieste di permesso. Restrigiamo l’accesso alle rotte /manager/requests e /manager/requests/:id/edit solo ai manager con l’aiuto del componente <CanAccess />.

Al momento, quando accedi come dipendente, non puoi vedere il link alla pagina Richieste nella barra laterale, ma puoi comunque accedere alla rotta /manager/requests digitando l’URL nel browser. Aggiungeremo una protezione per prevenire l’accesso non autorizzato a queste rotte.

Aggiorniamo il file App.tsx per includere i controlli di autorizzazione:

src/App.tsx
import { Authenticated, CanAccess, ErrorComponent, Refine } from '@refinedev/core'
import { DevtoolsProvider, DevtoolsPanel } from '@refinedev/devtools'
import dataProvider from '@refinedev/nestjsx-crud'
import routerProvider, {
  UnsavedChangesNotifier,
  DocumentTitleHandler,
  NavigateToResource,
} from '@refinedev/react-router-v6'
import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom'
import { Toaster } from 'react-hot-toast'

import { PageEmployeeTimeOffsList } from '@/pages/employee/time-offs/list'
import { PageEmployeeTimeOffsCreate } from '@/pages/employee/time-offs/create'
import { PageManagerRequestsList } from '@/pages/manager/requests/list'
import { PageManagerRequestsTimeOffsEdit } from '@/pages/manager/requests/time-offs/edit'
import { PageLogin } from '@/pages/login'

import { Layout } from '@/components/layout'

import { ThemeProvider } from '@/providers/theme-provider'
import { authProvider } from '@/providers/auth-provider'
import { accessControlProvider } from '@/providers/access-control'
import { useNotificationProvider } from '@/providers/notification-provider'
import { queryClient } from '@/providers/query-client'

import { BASE_URL } from '@/utilities/constants'
import { axiosInstance } from '@/utilities/axios'

import { RequestsIcon, TimeOffIcon } from '@/icons'

import { Role } from '@/types'

import '@/utilities/init-dayjs'

function App() {
  return (
    <BrowserRouter>
      <ThemeProvider>
        <DevtoolsProvider>
          <Refine
            authProvider={authProvider}
            routerProvider={routerProvider}
            dataProvider={dataProvider(BASE_URL, axiosInstance)}
            notificationProvider={useNotificationProvider}
            resources={[
              {
                name: 'employee',
                meta: {
                  scope: Role.EMPLOYEE,
                  order: 2,
                },
              },
              {
                name: 'manager',
                meta: {
                  scope: Role.MANAGER,
                  order: 1,
                },
              },
              {
                name: 'time-offs',
                list: '/employee/time-offs',
                create: '/employee/time-offs/new',
                meta: {
                  parent: 'employee',
                  scope: Role.EMPLOYEE,
                  label: 'Time Off',
                  icon: <TimeOffIcon />,
                },
              },
              {
                name: 'time-offs',
                list: '/manager/requests',
                edit: '/manager/requests/:id/edit',
                identifier: 'requests',
                meta: {
                  parent: 'manager',
                  scope: Role.MANAGER,
                  label: 'Requests',
                  icon: <RequestsIcon />,
                },
              },
            ]}
            accessControlProvider={accessControlProvider}
            options={{
              reactQuery: {
                clientConfig: queryClient,
              },
              syncWithLocation: true,
              warnWhenUnsavedChanges: true,
              useNewQueryKeys: true,
            }}>
            <Routes>
              <Route
                element={
                  <Authenticated key='authenticated-routes' redirectOnFail='/login'>
                    <Outlet />
                  </Authenticated>
                }>
                <Route index element={<NavigateToResource resource='time-offs' />} />

                <Route
                  path='employee'
                  element={
                    <ThemeProvider role={Role.EMPLOYEE}>
                      <Layout>
                        <Outlet />
                      </Layout>
                    </ThemeProvider>
                  }>
                  <Route path='time-offs' element={<Outlet />}>
                    <Route index element={<PageEmployeeTimeOffsList />} />
                    <Route path='new' element={<PageEmployeeTimeOffsCreate />} />
                  </Route>
                </Route>
              </Route>

              <Route
                path='manager'
                element={
                  <ThemeProvider role={Role.MANAGER}>
                    <Layout>
                      <CanAccess action='manager' fallback={<NavigateToResource resource='time-offs' />}>
                        <Outlet />
                      </CanAccess>
                    </Layout>
                  </ThemeProvider>
                }>
                <Route
                  path='requests'
                  element={
                    <PageManagerRequestsList>
                      <Outlet />
                    </PageManagerRequestsList>
                  }>
                  <Route path=':id/edit' element={<PageManagerRequestsTimeOffsEdit />} />
                </Route>
              </Route>

              <Route
                element={
                  <Authenticated key='auth-pages' fallback={<Outlet />}>
                    <NavigateToResource resource='time-offs' />
                  </Authenticated>
                }>
                <Route path='/login' element={<PageLogin />} />
              </Route>

              <Route
                element={
                  <Authenticated key='catch-all'>
                    <Layout>
                      <Outlet />
                    </Layout>
                  </Authenticated>
                }>
                <Route path='*' element={<ErrorComponent />} />
              </Route>
            </Routes>

            <UnsavedChangesNotifier />
            <DocumentTitleHandler />
            <Toaster position='bottom-right' reverseOrder={false} />
            <DevtoolsPanel />
          </Refine>
        </DevtoolsProvider>
      </ThemeProvider>
    </BrowserRouter>
  )
}

export default App

Nel codice sopra, abbiamo aggiunto il componente <CanAccess /> alla rotta “/manager”. Questo componente controlla se l’utente ha il ruolo di “manager” prima di eseguire il rendering delle rotte figlie. Se l’utente non ha il ruolo di “manager”, verrà reindirizzato alla pagina dell’elenco dei permessi per i dipendenti.

Ora, quando accedi come dipendente e cerchi di accedere al percorso /manager/requests, verrai reindirizzato alla pagina dell’elenco delle assenze per i dipendenti.

Passo 7 — Distribuzione sulla piattaforma App di DigitalOcean

In questo passo, distribuiamo l’applicazione sulla piattaforma App di DigitalOcean. Per farlo, ospiteremo il codice sorgente su GitHub e collegheremo il repository GitHub alla piattaforma App.

Invio del codice su GitHub

Accedi al tuo account GitHub e crea un nuovo repository chiamato refine-hr. Puoi rendere il repository pubblico o privato:

Dopo aver creato il repository, naviga nella directory del progetto ed esegui il seguente comando per inizializzare un nuovo repository Git:

git init

Successivamente, aggiungi tutti i file al repository Git con questo comando:

git add .

Poi, esegui il commit dei file con questo comando:

git commit -m "Initial commit"

Successivamente, aggiungi il repository GitHub come repository remoto con questo comando:

git remote add origin <your-github-repository-url>

Infine, specifica che desideri inviare il tuo codice al ramo main con questo comando:

git branch -M main

Infine, spingi il codice nel repository di GitHub con questo comando:

git push -u origin main

Quando richiesto, inserisci le tue credenziali GitHub per spingere il tuo codice.

Riceverai un messaggio di successo dopo che il codice è stato inviato al repository di GitHub.

In questa sezione, hai caricato il tuo progetto su GitHub in modo da poterlo accedere utilizzando le App di DigitalOcean. Il passo successivo è creare una nuova App di DigitalOcean utilizzando il tuo progetto e impostare il deployment automatico.

Distribuzione sulla DigitalOcean App Platform

Durante questo processo, prenderai un’applicazione React e la preparerai per la distribuzione tramite la App Platform di DigitalOcean. Collegherai il tuo repository GitHub a DigitalOcean, configurerai come l’app verrà costruita e poi creerai un’iniziale distribuzione di un progetto. Dopo che il progetto è stato distribuito, le ulteriori modifiche che apporti verranno ricostruite e aggiornate automaticamente.

Entro la fine di questo passaggio, avrai la tua applicazione distribuita su DigitalOcean con consegna continua predisposta.

Accedi al tuo account DigitalOcean e vai alla pagina Apps. Clicca sul pulsante Crea App:

Se non hai collegato il tuo account GitHub a DigitalOcean, ti verrà chiesto di farlo. Clicca sul pulsante Collegati a GitHub. Si aprirà una nuova finestra che ti chiederà di autorizzare DigitalOcean ad accedere al tuo account GitHub.

Dopo aver autorizzato DigitalOcean, verrai reindirizzato alla pagina delle App di DigitalOcean. Il passo successivo è selezionare il tuo repository GitHub. Dopo aver selezionato il tuo repository, ti verrà chiesto di selezionare un branch da distribuire. Seleziona il branch main e clicca sul pulsante Avanti.

Dopo di ciò, vedrai i passaggi di configurazione per la tua applicazione. In questo tutorial, puoi cliccare sul pulsante Avanti per saltare i passaggi di configurazione. Tuttavia, puoi anche configurare la tua applicazione come desideri.

Aspetta che la build sia completata. Dopo che la build è completata, premi App Live per accedere al tuo progetto nel browser. Sarà lo stesso del progetto che hai testato localmente, ma questo sarà attivo sul web con un URL sicuro. Inoltre, puoi seguire questo tutorial disponibile sul sito della community di DigitalOcean per imparare a distribuire applicazioni basate su React su App Platform.

Nota: Nel caso in cui la tua build non riesca a essere distribuita con successo, puoi configurare il tuo comando di build su DigitalOcean per utilizzare npm install --production=false && npm run build && npm prune --production invece di npm run build

Conclusione

In questo tutorial, abbiamo costruito un’applicazione di gestione HR utilizzando Refine da zero e ci siamo familiarizzati con come costruire un’app CRUD completamente funzionale.

Inoltre, dimostreremo come distribuire la tua applicazione sulla Piattaforma App di DigitalOcean.

Se desideri saperne di più su Refine, puoi consultare la documentazione e se hai domande o feedback, puoi unirti al Server Discord di Refine.

Source:
https://www.digitalocean.com/community/developer-center/building-and-deploying-an-hr-app-using-refine