Fermer

juin 28, 2024

Premiers pas avec Appwrite (partie 4)

Premiers pas avec Appwrite (partie 4)


Cette série sur Appwrite, une plateforme BaaS, présente le flux d’authentification, le stockage de base de données et les fonctions sans serveur dans une application de facturation React. Dans la partie 4, nous approfondissons la CLI, les fonctions et le stockage d’Appwrite. Lorsqu’une facture est créée, mise à jour ou supprimée, son PDF fera de même dans le stockage Appwrite.

Bienvenue dans la dernière partie de la série « Premiers pas avec Appwrite ». Dans le partie précédente, nous avons implémenté un formulaire de facture et affiché des pages de facture avec des fonctionnalités qui permettent aux utilisateurs de créer, lire, mettre à jour et supprimer des factures. Dans cette partie, nous explorerons les fonctions et le stockage Appwrite. Concrètement, nous allons :

  • Plongez dans Appwrite CLI pour gérer les fonctions Appwrite
  • Créez un PDF avec les détails de la facture et stockez-le dans Appwrite Storage lorsqu’une facture est créée
  • Supprimez le fichier PDF actuel et créez-en un nouveau lorsqu’une facture est mise à jour
  • Supprimer le fichier PDF de la facture lorsque la facture est supprimée

Commençons par installer et configurer la CLI Appwrite, car nous l’utiliserons pour créer une fonction permettant de gérer les PDF des factures.

CLI d’écriture d’application

Il existe plusieurs façons de installer la CLI Appwritemais le plus simple consiste à exécuter la commande npm et à l’installer globalement.

$ npm install -g appwrite-cli

Une fois l’installation terminée, vérifiez-la en vérifiant la version de la CLI.

$ appwrite -v
```part
 
If the installation was successful, the next step is to log in.
 
```shell
$ appwrite login

Après vous être connecté, nous devons connecter notre projet.

$ appwrite init project

Choisissez l’option « Lier ce répertoire à un projet Appwrite existant » et sélectionnez le projet de facture.

Il vous sera demandé de saisir votre e-mail, votre mot de passe et le point de terminaison du serveur Appwrite. Si vous vous êtes inscrit en utilisant votre compte GitHub, vous devez visiter la page de votre compte sur le site Web Appwrite et suivre le processus de récupération de mot de passe pour configurer un nouveau mot de passe. Le point de terminaison du serveur Appwrite doit être https://cloud.appwrite.io/v1. Vous pouvez également trouver la valeur du point final dans les paramètres de votre projet.

C’est tout pour la configuration d’Appwrite CLI. Ensuite, créons une nouvelle fonction Appwrite.

Création d’une fonction Appwrite

Les fonctions Appwrite peuvent être créées de deux manières. La première consiste à le faire simplement via le tableau de bord des fonctions Appwrite. Nous pouvons connecter un référentiel Git Appwrite, puis sélectionner l’un des modèles de démarrage.

Créer une fonction

La deuxième façon consiste à utiliser la CLI Appwrite et à le faire à partir de la base de code d’un projet. Nous opterons pour cette dernière approche.

Exécutez la commande ci-dessous pour créer une nouvelle fonction Appwrite.

$ appwrite init function

Vous serez invité à répondre à quelques questions. Vous pouvez nommer la fonction onInvoiceChange. Vous n’êtes pas obligé de fournir une pièce d’identité car une pièce d’identité sera générée pour vous. En ce qui concerne le runtime, sélectionnez le dernier Node. La CLI aurait dû maintenant échafauder une nouvelle fonction à l’intérieur du functions annuaire.

Déplacez-vous dans le terminal vers le dossier de la fonction nouvellement créée en exécutant cd functions/onInvoiceChange commande. Pour créer un fichier PDF, nous utiliserons le pdf-lib bibliothèque, alors installons-la.

$ npm install pdf-lib

Ensuite, nous devons configurer le moment où nous voulons que la fonction s’exécute. Avec Appwrite, les fonctions peuvent être exécutées de plusieurs manières. Nous pouvons simplement faire une requête API ou utiliser le SDK ou le tableau de bord d’Appwrite pour exécuter une fonction. Appwrite prend également en charge fonctions programmées en utilisant des expressions cron. Enfin et surtout, les fonctions peuvent également être exécutées en réponse à événementstelles que les actions CRUD de collecte ou de document, les actions de compartiment de stockage ou les modifications d’équipe et d’utilisateur.

Dans ce scénario, nous souhaitons faire quelque chose lorsqu’une facture est créée, mise à jour ou supprimée. Par conséquent, nous allons configurer la fonction pour qu’elle soit exécutée via des événements.

Les paramètres d’une fonction peuvent être modifiés soit via le tableau de bord Appwrite, soit via le appwrite.json fichier qui a été créé automatiquement lorsque nous avons exécuté le appwrite init function commande. Nous allons configurer la fonction à l’aide du fichier JSON pour deux raisons. Tout d’abord, nous conservons toute la configuration dans le code afin qu’elle soit facilement accessible. Deuxièmement, chaque fois qu’une fonction est déployée via CLI, la configuration de appwrite.json Le fichier remplace celui saisi via les paramètres du tableau de bord.

Mettons à jour le appwrite.json déposer. Remplacez le vide "events": [] tableau avec l’extrait ci-dessous. Assurez-vous cependant de remplacer <INVOICE COLLECTION ID> avec l’identifiant de votre encaissement de factures.

"events": [
  "databases.*.collections.<INVOICE COLLECTION ID>.documents.*.create",
  "databases.*.collections.<INVOICE COLLECTION ID>.documents.*.update",
  "databases.*.collections.<INVOICE COLLECTION ID>.documents.*.delete"
],

Ci-dessous vous pouvez voir ce que appwrite.json le fichier devrait ressembler à :

appwrite.json

{
  "projectId": "<YOUR PROJECT ID>",
  "projectName": "<YOUR PROJECT NAME>",
  "functions": [
    {
      "$id": "654a6589a133aa745b76",
      "name": "onInvoiceChange",
      "runtime": "node-18.0",
      "execute": [],
      "events": [
        "databases.*.collections.<INVOICE COLLECTION ID>.documents.*.create",
        "databases.*.collections.<INVOICE COLLECTION ID>.documents.*.update",
        "databases.*.collections.<INVOICE COLLECTION ID>.documents.*.delete"
      ],
      "schedule": "",
      "timeout": 15,
      "enabled": true,
      "logging": true,
      "entrypoint": "src/main.js",
      "commands": "npm install",
      "ignore": ["node_modules", ".npm"],
      "path": "functions/onInvoiceChange"
    }
  ]
}

Voici la répartition de la structure des événements que nous avons définie :

service.serviceId.resource.resourceId.resource.resourceId.action

Et pour être plus précis, dans les événements que nous configurons, ils ont la structure suivante :

databases.databaseId.collections.collectionId.documents.documentId.action

Il existe de nombreux événements auxquels une fonction peut réagir. Vous pouvez trouver possible combinaisons d’événements ici. Appwrite fournit une interface utilisateur agréable pour ajouter des événements et je peux certainement vous recommander de l’essayer. Le GIF ci-dessous montre à quoi cela ressemble.

Événements de fonction du tableau de bord Appwrite

Toutefois, ne configurez aucun événement pour le onInvoiceChange fonctionner via le tableau de bord, car nous utilisons le appwrite.json fichier pour cela.

Ensuite, nous devons configurer les variables d’environnement que nous utiliserons dans le onInvoiceChange fonction. Nous devrons créer les éléments suivants :

  • APPWRITE_BUCKET_ID – L’ID du bucket de stockage.
  • APPWRITE_PROJECT_ID – L’ID du projet Appwrite.
  • APPWRITE_SERVER_ENDPOINT – Le point de terminaison du serveur Appwrite Cloud
  • APPWRITE_SERVER_API_KEY – Une clé API personnalisée qui accorde à la fonction l’accès pour effectuer des actions, telles que la création ou la suppression d’un fichier du stockage.

Avant de commencer à ajouter des variables d’environnement, ajoutons le APPWRITE_SERVER_API_KEY clé.

Parcourez les pages suivantes et cliquez sur le bouton « Créer une clé API » pour configurer une nouvelle clé API serveur que nous utiliserons dans le onInvoiceChange fonction.

Tableau de bord Appwrite -> Paramètres du projet -> Aperçu -> Afficher les clés API -> Créer une clé API

Vous devriez voir le formulaire « Créer une clé API », comme indiqué sur l’image ci-dessous.

Créer une clé API

Nous devons maintenant configurer les étendues auxquelles la clé API doit accorder l’accès.

Portées des clés API

Pour les besoins de ce didacticiel, vous pouvez simplement accorder toutes les étendues. Cependant, pour les applications réelles, il est conseillé de suivre le principe du privilège le plus bas. Fondamentalement, au lieu de donner accès à tout, accordez l’accès uniquement aux ressources nécessaires. Ainsi, suivant ce principe, le onInvoiceChange la fonction ne devrait avoir accès qu’au stockage files.read et files.write scopes, car il ne fait rien d’autre que télécharger et supprimer des fichiers du stockage.

Étendues de stockage

Ensuite, ajoutons les variables globales suivantes au projet. Dans les paramètres du projet, recherchez le Variables globales section et ajouter APPWRITE_BUCKET_ID, APPWRITE_PROJECT_ID, APPWRITE_SERVER_ENDPOINT et APPWRITE_SERVER_API_KEY.

Les deux premiers que vous pouvez trouver dans le .env fichier, que nous avons créé dans la première partie de cette série. Le APPWRITE_SERVER_ENDPOINT vous pouvez copier à partir du src/api/appwrite.api.js ou à partir des paramètres du projet. Le APPWRITE_SERVER_API_KEY nous avons créé il y a juste un instant. Votre section de variables globales devrait ressembler à l’image ci-dessous.

Variables globales du projet

C’est suffisant pour la partie configuration. Passons au fichier d’entrée du onInvoiceChange fonction et modifiez-le pour qu’il gère la création et la suppression de fichiers PDF.

Voici la fonction complète.

fonctions/onInvoiceChange/src/main.js


import { Client, Storage, InputFile, Permission, Role } from 'node-appwrite';
import { createInvoicePdf } from './helpers/createInvoicePdf.js';
import { Buffer } from 'node:buffer';
 
const APPWRITE_BUCKET_ID = process.env.APPWRITE_BUCKET_ID;
const APPWRITE_SERVER_API_KEY = process.env.APPWRITE_SERVER_API_KEY;
const APPWRITE_PROJECT_ID = process.env.APPWRITE_PROJECT_ID;
const APPWRITE_SERVER_ENDPOINT = process.env.APPWRITE_SERVER_ENDPOINT;
 
const onCreateInvoice = async ({ req, res, storage }) => {
  const { $id } = req.body;
  const { pdfBytes } = await createInvoicePdf(req.body);
  const buffer = Buffer.from(pdfBytes);
  const userId = req.headers['x-appwrite-user-id'];
  const fileId = `INVOICE_${$id}`;
  const filename = `${fileId}.pdf`;
  const ownerRole = Role.user(userId);
  await storage.createFile(
    APPWRITE_BUCKET_ID,
    fileId,
    InputFile.fromBuffer(buffer, filename),
    [
      Permission.read(ownerRole),
      Permission.update(ownerRole),
      Permission.delete(ownerRole),
    ]
  );
 
  return res.json({
    message: 'Invoice created',
  });
};
 
const onUpdateInvoice = async ({ log, req, res, storage }) => {
  const { $id } = req.body;
  const { pdfBytes } = await createInvoicePdf(req.body);
  const buffer = Buffer.from(pdfBytes);
  const fileId = `INVOICE_${$id}`;
  const filename = `${fileId}.pdf`;
  const userId = req.headers['x-appwrite-user-id'];
  const ownerRole = Role.user(userId);
 
  try {
    await storage.deleteFile(APPWRITE_BUCKET_ID, fileId);
  } catch (err) {
    log(err);
    log(`Could not delete invoice file with ID ${fileId} `);
  }
 
  await storage.createFile(
    APPWRITE_BUCKET_ID,
    fileId,
    InputFile.fromBuffer(buffer, filename),
    [
      Permission.read(ownerRole),
      Permission.update(ownerRole),
      Permission.delete(ownerRole),
    ]
  );
  return res.json({
    message: 'Invoice updated',
  });
};
 
const onDeleteInvoice = async ({ req, res, error, storage }) => {
  const { $id } = req.body;
  const fileId = `INVOICE_${$id}`;
 
  try {
    await storage.deleteFile(APPWRITE_BUCKET_ID, fileId);
  } catch (err) {
    error(error);
  }
  return res.json({
    message: 'Deleted',
  });
};
 
const eventHandlers = {
  create: onCreateInvoice,
  update: onUpdateInvoice,
  delete: onDeleteInvoice,
};
 
export default async ({ req, res, log, error }) => {
  if (req.method !== 'POST') {
    return res.send('Method not allowed', 403);
  }
 
  if (!req.body.invoiceId) {
    return res.send('Missing invoice ID', 403);
  }
 
  if (req.headers['x-appwrite-trigger'] !== 'event') {
    return res.send('Execution method not allowed.', 403);
  }
 
  const eventType = req.headers['x-appwrite-event'].split('.').at(-1);
 
  if (!Object.hasOwn(eventHandlers, eventType)) {
    return res.send('Event not supported', 403);
  }
 
  const handler = eventHandlers[eventType];
 
  const client = new Client()
    .setEndpoint(APPWRITE_SERVER_ENDPOINT)
    .setProject(APPWRITE_PROJECT_ID)
    .setKey(APPWRITE_SERVER_API_KEY);
  const storage = new Storage(client);
 
  return handler({ req, res, log, error, client, storage });
};

Il se passe pas mal de choses dans cette fonction, alors passons en revue étape par étape. Nous partirons du point d’entrée, qui est le gestionnaire de fonctions principal.

const eventHandlers = {
  create: onCreateInvoice,
  update: onUpdateInvoice,
  delete: onDeleteInvoice,
};
 
export default async ({ req, res, log, error }) => {
  if (req.method !== 'POST') {
    return res.send('Method not allowed', 403);
  }
 
  if (!req.body.invoiceId) {
    return res.send('Missing invoice ID', 403);
  }
 
  if (req.headers['x-appwrite-trigger'] !== 'event') {
    return res.send('Execution method not allowed.', 403);
  }
 
  const eventType = req.headers['x-appwrite-event'].split('.').at(-1);
 
  if (!Object.hasOwn(eventHandlers, eventType)) {
    return res.send('Event not supported', 403);
  }
 
  const handler = eventHandlers[eventType];
 
  const client = new Client()
    .setEndpoint(APPWRITE_SERVER_ENDPOINT)
    .setProject(APPWRITE_PROJECT_ID)
    .setKey(APPWRITE_SERVER_API_KEY);
  const storage = new Storage(client);
 
  return handler({ req, res, log, error, client, storage });
};

Tout d’abord, nous effectuons une validation et affirmons que :

  • La méthode de demande est POST.
  • Le invoiceId est présent dans la charge utile du corps.
  • Le x-appwrite-trigger l’en-tête est présent et est de type event. Après tout, nous souhaitons que cette fonction soit déclenchée uniquement en réponse à un événement.

Ensuite, nous extrayons le type d’événement du x-appwrite-event entête. Nous avons configuré trois événements pour cette fonction, le type d’événement doit donc être soit create, update ou delete.

Nous utilisons le type d’événement pour obtenir le bon gestionnaire à partir du eventHandlers objet. Lorsque nous obtenons le bon gestionnaire, nous initialisons le client Appwrite, une instance de stockage, et exécutons le gestionnaire.

Jetons maintenant un coup d’œil à chacun des gestionnaires, en commençant par onCreateInvoice.

const onCreateInvoice = async ({ req, res, storage }) => {
  const { $id } = req.body;
  const { pdfBytes } = await createInvoicePdf(req.body);
  const buffer = Buffer.from(pdfBytes);
  const userId = req.headers['x-appwrite-user-id'];
  const fileId = `INVOICE_${$id}`;
  const filename = `${fileId}.pdf`;
  const ownerRole = Role.user(userId);
  await storage.createFile(
    APPWRITE_BUCKET_ID,
    fileId,
    InputFile.fromBuffer(buffer, filename),
    [
      Permission.read(ownerRole),
      Permission.update(ownerRole),
      Permission.delete(ownerRole),
    ]
  );
 
  return res.json({
    message: 'Invoice created',
  });
};

Dans le onCreateInvoice gestionnaire, nous utilisons le createInvoicePdf assistant pour créer un PDF avec les détails de la facture. Nous allons créer cette assistante dans un instant.

Une fois la facture prête, nous utilisons le storage.createFile pour télécharger la facture générée dans le stockage. Notez comment nous transmettons les autorisations pour permettre à l’utilisateur de lire, mettre à jour et supprimer le fichier. Une fois la facture créée, nous envoyons une réponse positive.

Le onUpdateInvoice est très similaire à onCreateInvoice gestionnaire.

const onUpdateInvoice = async ({ log, req, res, storage }) => {
  const { $id } = req.body;
  const { pdfBytes } = await createInvoicePdf(req.body);
  const buffer = Buffer.from(pdfBytes);
  const fileId = `INVOICE_${$id}`;
  const filename = `${fileId}.pdf`;
  const userId = req.headers['x-appwrite-user-id'];
  const ownerRole = Role.user(userId);
 
  try {
    await storage.deleteFile(APPWRITE_BUCKET_ID, fileId);
  } catch (err) {
    log(err);
    log(`Could not delete invoice file with ID ${fileId} `);
  }
 
  await storage.createFile(
    APPWRITE_BUCKET_ID,
    fileId,
    InputFile.fromBuffer(buffer, filename),
    [
      Permission.read(ownerRole),
      Permission.update(ownerRole),
      Permission.delete(ownerRole),
    ]
  );
  return res.json({
    message: 'Invoice updated',
  });
};

La principale différence est qu’avant de créer un nouveau fichier, nous essayons d’abord de supprimer un fichier déjà existant à l’aide du storage.deleteFile méthode.

Finalement, le onDeleteInvoice supprime simplement le PDF de la facture.

const onDeleteInvoice = async ({ req, res, error, storage }) => {
  const { $id } = req.body;
  const fileId = `INVOICE_${$id}`;
 
  try {
    await storage.deleteFile(APPWRITE_BUCKET_ID, fileId);
  } catch (err) {
    error(error);
  }
  return res.json({
    message: 'Deleted',
  });
};

Ci-dessous vous pouvez voir le code du createInvoicePdf assistant. Il utilise pdf-lib pour créer le PDF de la facture.

fonctions/onInvoiceChange/src/helpers/createInvoicePdf.js

import { PDFDocument, PageSizes, StandardFonts } from 'pdf-lib';
 
const fontSize = {
  heading: 20,
  text: 14,
};
 
export const createInvoicePdf = async (invoiceData) => {
  const {
    invoiceId,
    date,
    dueDate,
    amount,
    description,
    senderName,
    senderAddress,
    senderPostcode,
    senderCity,
    senderCountry,
    senderPhone,
    senderEmail,
    clientName,
    accountName,
    clientAddress,
    clientPostcode,
    clientCity,
    clientCountry,
    clientEmail,
    clientPhone,
    accountIban,
    accountNumber,
    accountSortCode,
    accountAddress,
    accountPostCode,
    accountCity,
    accountCountry,
    paymentReceived,
    paymentDate,
  } = invoiceData;
 
  const document = await PDFDocument.create();
 
  const [width, height] = PageSizes.A4;
  const margin = 20;
  const primaryFont = await document.embedFont(StandardFonts.Helvetica);
  const primaryFontBold = await document.embedFont(StandardFonts.HelveticaBold);
  const page = document.addPage([width, height]); 
  page.drawText(`Invoice #${invoiceId}`, {
    x: margin,
    y: height - 50,
    size: fontSize.heading,
  });
 
  const dateText = new Date(date).toLocaleDateString();
  const dateTextWidth = primaryFont.widthOfTextAtSize(dateText, fontSize.text);
 
  page.drawText(dateText, {
    x: width - margin - dateTextWidth,
    y: height - 50,
    size: fontSize.text,
  });
 
  page.drawText('From:', {
    x: margin,
    y: height - 100,
    size: fontSize.text,
  });
 
  let senderDetailsOffset = 125;
  [
    senderName,
    senderAddress,
    senderPostcode,
    senderCity,
    senderCountry,
    senderPhone,
    senderEmail,
  ].forEach((text) => {
    if (text) {
      page.drawText(text, {
        x: margin,
        y: height - senderDetailsOffset,
        size: fontSize.text,
      });
      senderDetailsOffset += 20;
    }
  });
 
  page.drawText('To:', {
    x: width - margin - primaryFont.widthOfTextAtSize('To:', 14),
    y: height - 100,
    size: fontSize.text,
  });
 
  let clientDetailsOffset = 125;
  [
    clientName,
    clientAddress,
    clientPostcode,
    clientCity,
    clientCountry,
    clientPhone,
    clientEmail,
  ].forEach((text) => {
    if (text) {
      const textWidth = primaryFont.widthOfTextAtSize(text, fontSize.text);
      page.drawText(text, {
        x: width - margin - textWidth,
        y: height - clientDetailsOffset,
        size: fontSize.text,
      });
      clientDetailsOffset += 20;
    }
  });
 
  page.drawText('Invoice Details', {
    x: margin,
    y: height - 300,
    size: fontSize.text,
    font: primaryFontBold,
  });
 
  page.drawText(`${description}`, {
    x: margin,
    y: height - 330,
    size: fontSize.text,
  });
 
  const amountLabelText = 'Amount';
  const amountLabelTextWidth = primaryFont.widthOfTextAtSize(
    amountLabelText,
    fontSize.text
  );
 
  page.drawText('Amount', {
    x: width - margin - amountLabelTextWidth,
    y: height - 300,
    size: fontSize.text,
    font: primaryFontBold,
  });
 
  const amountText = amount;
  const amountTextWidth = primaryFont.widthOfTextAtSize(
    amountText,
    fontSize.text
  );
 
  page.drawText(amountText, {
    x: width - margin - amountTextWidth,
    y: height - 330,
    size: fontSize.text,
  });
 
  page.drawText('Method of Payment:', {
    x: margin,
    y: height - 380,
    size: fontSize.text,
    font: primaryFontBold,
  });
 
  page.drawText(`Name: ${accountName}`, {
    x: margin,
    y: height - 410,
    size: fontSize.text,
  });
 
  page.drawText(`Account Number: ${accountNumber}`, {
    x: margin,
    y: height - 430,
    size: fontSize.text,
  });
 
  page.drawText(`Sort Code: ${accountSortCode}`, {
    x: margin,
    y: height - 450,
    size: fontSize.text,
  });
 
  let offset = 0;
  if (accountIban) {
    page.drawText(`IBAN: ${accountIban}`, {
      x: margin,
      y: height - 470,
      size: fontSize.text,
    });
 
    offset += 20;
  }
 
  page.drawText('Address:', {
    x: margin,
    y: height - 470 - offset,
    size: fontSize.text,
  });
 
  page.drawText(accountAddress, {
    x: margin,
    y: height - 490 - offset,
    size: fontSize.text,
  });
 
  page.drawText(accountPostCode, {
    x: margin,
    y: height - 510 - offset,
    size: fontSize.text,
  });
  page.drawText(accountCity, {
    x: margin,
    y: height - 530 - offset,
    size: fontSize.text,
  });
  page.drawText(accountCountry, {
    x: margin,
    y: height - 550 - offset,
    size: fontSize.text,
  });
 
  page.drawText(
    `This invoice is due by ${new Date(dueDate).toLocaleDateString()}`,
    {
      x: margin,
      y: height - 600 - offset,
      size: fontSize.text,
    }
  );
  const thankYouText = 'Thank you for your business!';
  const thankYouTextWidth = primaryFont.widthOfTextAtSize(thankYouText, 12);
  page.drawText(thankYouText, {
    x: width / 2 - thankYouTextWidth / 2,
    y: height - 650 - offset,
    size: 12,
  });
 
  if (paymentReceived) {
    page.drawText(
      `Payment received on ${new Date(paymentDate).toLocaleDateString()}`,
      {
        x: margin,
        y: 20,
        size: 12,
      }
    );
  }
 
  const pdfBytes = await document.save();
  return {
    pdfBytes,
  };
};

Nous n’entrerons pas dans la question de savoir comment pdf-lib fonctionne, car cela sort du cadre de cette série, mais n’hésitez pas à vérifier son Documentation et apportez quelques modifications à la mise en page du PDF.

La dernière chose que nous devons faire est de déployer la fonction. Exécutez la commande suivante dans votre terminal à partir du répertoire racine.

$ appwrite deploy function

Lorsque vous y êtes invité, sélectionnez le onInvoiceChange fonction de déploiement.

Avant d’aller plus loin, je tiens à souligner ici que par rapport à Firebase, Appwrite n’offre pas de moyen officiel d’exécuter des fonctions localement. Firebase fournit le Suite d’émulateurs locaux, qui peut être utilisé pour exécuter les services de Firebase tels que Functions, Auth ou Firestore sur votre propre ordinateur. Malheureusement, une fonction Appwrite doit être déployée pour profiter des fonctionnalités d’Appwrite, telles que la planification ou les déclencheurs d’événements.

Fonctionnalité de téléchargement de facture

Ajoutons une nouvelle méthode appelée getInvoiceFileUrl au invoice.api.js fichier qui se chargera d’obtenir l’URL du fichier PDF de la facture.

src/api/invoice.api.js

import { ID, Permission, Role } from "appwrite";
import { databases, databaseId, storage } from "./appwrite.api";
 
const invoiceCollectionId = import.meta.env
  .VITE_APPWRITE_COLLECTION_ID_INVOICES;
 
export const listInvoices = () => {
  return databases.listDocuments(databaseId, invoiceCollectionId);
};
 
export const getInvoice = documentId => {
  return databases.getDocument(databaseId, invoiceCollectionId, documentId);
};
 
export const createInvoice = (userId, payload) => {
  const ownerRole = Role.user(userId);
  return databases.createDocument(
    databaseId,
    invoiceCollectionId,
    ID.unique(),
    payload,
    [
      Permission.read(ownerRole),
      Permission.update(ownerRole),
      Permission.delete(ownerRole),
    ]
  );
};
 
export const updateInvoice = (documentId, payload) => {
  return databases.updateDocument(
    databaseId,
    invoiceCollectionId,
    documentId,
    payload
  );
};
 
export const deleteInvoice = documentId => {
  return databases.deleteDocument(databaseId, invoiceCollectionId, documentId);
};
 
const bucketId = import.meta.env.VITE_APPWRITE_BUCKET_ID;
 
export const getInvoiceFileUrl = fileId => {
  return storage.getFileDownload(bucketId, fileId);
};

Ensuite, nous créerons un hook personnalisé pour gérer le téléchargement.

src/views/invoice/hooks/useDownloadInvoice.js

import { useState } from "react";
import { getInvoiceFileUrl } from "../../../api/invoice.api";
 
export const useDownloadInvoice = ({ invoiceId }) => {
  const [downloadInvoiceStatus, setDownloadInvoiceStatus] = useState("IDLE");
 
  const onDownloadInvoice = async () => {
    try {
      if (downloadInvoiceStatus === "PENDING") {
        return;
      }
      setDownloadInvoiceStatus("PENDING");
      const fileId = `INVOICE_${invoiceId}`;
      const result = await getInvoiceFileUrl(fileId);
      const anchor = document.createElement("a");
      anchor.href = result.href;
      anchor.download = `${fileId}.pdf`;
      anchor.click();
      anchor.remove();
      setDownloadInvoiceStatus("SUCCESS");
    } catch (error) {
      console.error(error);
      setDownloadInvoiceStatus("ERROR");
    }
  };
 
  return {
    downloadInvoiceStatus,
    setDownloadInvoiceStatus,
    onDownloadInvoice,
  };
};

Après avoir obtenu l’URL du fichier, nous créons un élément d’ancrage temporaire, lui attribuons l’URL et démarrons le téléchargement.

La dernière chose que nous devons faire est de mettre à jour le Invoice composant, car nous devons ajouter un bouton de téléchargement et avec le useDownloadInvoice crochet.

src/views/invoice/Invoice.jsx

import { Link, useParams } from "react-router-dom";
import BankDetails from "./components/BankDetails";
import ClientDetails from "./components/ClientDetails";
import InvoiceDetails from "./components/InvoiceDetails";
import SenderDetails from "./components/SenderDetails";
import { useDeleteInvoice } from "./hooks/useDeleteInvoice";
import { useDownloadInvoice } from "./hooks/useDownloadInvoice";
import { useFetchInvoice } from "./hooks/useFetchInvoice";
import { useInvoiceForm } from "./hooks/useInvoiceForm";
import { useSubmitInvoice } from "./hooks/useSubmitInvoice";
 
const config = {
  create: {
    submitButtonText: "Create",
  },
  update: {
    submitButtonText: "Update",
  },
};
 
const Invoice = () => {
  const params = useParams();
  const { isEditMode, form, setForm, onFormChange } = useInvoiceForm();
 
  const { fetchInvoiceStatus, initFetchInvoice } = useFetchInvoice({
    id: params.id,
    onSetInvoice: setForm,
  });
 
  const { submitInvoiceStatus, onSubmitInvoice } = useSubmitInvoice({
    form,
    isEditMode,
  });
 
  const { deleteInvoiceStatus, initDeletePrompt } = useDeleteInvoice({
    invoiceId: form.$id,
  });
 
  const { downloadInvoiceStatus, onDownloadInvoice } = useDownloadInvoice({
    invoiceId: form.$id,
  });
 
  const { submitButtonText } = isEditMode ? config.update : config.create;
 
  return (
    <div className="flex items-center justify-center w-full min-h-screen bg-gradient-to-r from-pink-300 via-purple-300 to-indigo-400">
      <div className="min-h-screen px-8 pb-16 bg-white md:w-3/4 md:ml-auto md:pr-0 md:pl-16 md:pb-24">
        <div className="flex items-center justify-between mr-8">
          <h1 className="my-8 text-2xl font-semibold text-indigo-900">
            Invoice
          </h1>
          <Link
            className="text-sm transition-all duration-150 text-indigo-900/50 hover:text-indigo-900"
            to="/"
          >
            Back To Invoices
          </Link>
        </div>
        {fetchInvoiceStatus === "PENDING" ? (
          <div>Fetching invoice data...</div>
        ) : null}
        {fetchInvoiceStatus === "ERROR" ? (
          <div>
            <button
              className="px-4 py-2 bg-indigo-600 rounded-md text-indigo-50"
              onClick={() => initFetchInvoice(params.id)}
            >
              Try Again
            </button>
          </div>
        ) : null}
        {fetchInvoiceStatus === "SUCCESS" ? (
          <form
            className="flex flex-col max-w-5xl gap-8"
            onSubmit={onSubmitInvoice}
          >
            <div className="flex flex-col gap-8 md:gap-12">
              <InvoiceDetails form={form} onFormChange={onFormChange} />
              <SenderDetails form={form} onFormChange={onFormChange} />
              <ClientDetails form={form} onFormChange={onFormChange} />
              <BankDetails form={form} onFormChange={onFormChange} />
            </div>
            <div className="flex justify-between">
              <button
                type="button"
                className="min-w-[6rem] px-4 py-3 mr-4 font-semibold text-indigo-800 transition-colors duration-150 bg-indigo-200/25 rounded-md hover:bg-rose-800 hover:text-rose-100"
                onClick={initDeletePrompt}
              >
                {deleteInvoiceStatus === "PENDING" ? "Deleting..." : "Delete"}
              </button>
              <div>
                {form.$id ? (
                  <button
                    type="button"
                    className="min-w-[6rem] px-4 py-3 mr-4 font-semibold text-indigo-900 transition-colors duration-150 bg-indigo-200/50 rounded-md hover:bg-indigo-800 hover:text-indigo-100"
                    onClick={onDownloadInvoice}
                  >
                    {downloadInvoiceStatus === "PENDING"
                      ? "Downloading..."
                      : "Download Invoice"}
                  </button>
                ) : null}
                <button
                  type="submit"
                  className="min-w-[6rem] px-4 py-3 mr-8 font-semibold text-indigo-100 transition-colors duration-150 bg-indigo-600 rounded-md hover:bg-indigo-800"
                >
                  {submitInvoiceStatus === "PENDING"
                    ? "Submitting..."
                    : submitButtonText}
                </button>
              </div>
            </div>
          </form>
        ) : null}
      </div>
    </div>
  );
};
 
export default Invoice;

Vous devriez maintenant pouvoir cliquer sur le bouton de téléchargement pour télécharger un fichier PDF de la facture.

Bouton Télécharger la facture

Conclusion

C’est tout pour cette série ! Cela a été tout un parcours, alors félicitations pour en être arrivé là. Au moment de la rédaction, Appwrite Cloud est en version bêta et n’offre pas autant de fonctionnalités que Firebase, mais cela peut être une bonne alternative si vous ne pouvez pas ou ne voulez pas utiliser Firebase.

Si vous êtes prêt à relever un défi, vous pouvez faire certaines choses pour améliorer cette application. Par exemple, vous pouvez essayer d’ajouter la fonctionnalité suivante :

  • Ajoutez une pagination à la liste des factures.
  • Ajoutez une validation au formulaire de facture.
  • Envoyez automatiquement un e-mail avec un PDF à l’adresse e-mail du client.
  • Modifier le createInvoicePdf assistant pour gérer les données manquantes.
  • Modifiez le formulaire de facture afin qu’au lieu d’avoir une seule entrée pour la description et le montant, cela permette à un utilisateur d’ajouter un certain nombre d’éléments distincts avec le montant et la devise.
  • Autoriser le téléchargement d’un logo qui serait affiché sur la facture.

J’espère que vous avez apprécié cette série. Vous pouvez trouver le code final de ce tutoriel dans ce dépôt GitHub. Bon codage !




Source link