From 020ecff29c0ff305563450b3f73dfb8538052d50 Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Wed, 13 Dec 2023 23:29:14 +0000 Subject: [PATCH] Implemented file storage handling for Corporate Transfer and Comission Transfer --- src/components/PaymentAssetManager.tsx | 156 ++++++++++++++ src/firebase/index.ts | 2 + src/interfaces/paypal.ts | 2 + src/interfaces/storage.files.ts | 1 + src/pages/api/evaluate/interactiveSpeaking.ts | 6 +- src/pages/api/evaluate/speaking.ts | 6 +- .../api/payments/files/[type]/[paymentId].ts | 194 ++++++++++++++++++ src/pages/api/speaking.ts | 3 +- src/pages/api/users/update.ts | 3 +- src/pages/payment-record.tsx | 112 ++++++++++ 10 files changed, 473 insertions(+), 12 deletions(-) create mode 100644 src/components/PaymentAssetManager.tsx create mode 100644 src/interfaces/storage.files.ts create mode 100644 src/pages/api/payments/files/[type]/[paymentId].ts diff --git a/src/components/PaymentAssetManager.tsx b/src/components/PaymentAssetManager.tsx new file mode 100644 index 00000000..25c66727 --- /dev/null +++ b/src/components/PaymentAssetManager.tsx @@ -0,0 +1,156 @@ +import React, { ChangeEvent } from "react"; +import { BsUpload, BsDownload, BsTrash, BsFilePdf } from "react-icons/bs"; +import { FilesStorage } from "@/interfaces/storage.files"; +import axios from "axios"; + +interface Asset { + file: string | File; + complete: boolean; +} + +const PaymentAssetManager = (props: { + asset: string | undefined; + permissions: "read" | "write"; + type: FilesStorage; + paymentId: string; +}) => { + const { asset, permissions, type, paymentId } = props; + + const fileInputRef = React.useRef(null); + const fileInputReplaceRef = React.useRef(null); + + const [managingAsset, setManagingAsset] = React.useState({ + file: asset || "", + complete: asset ? true : false, + }); + + const { file, complete } = managingAsset; + + const deleteAsset = () => { + if (confirm("Are you sure you want to delete this document?")) { + axios + .delete(`/api/payments/files/${type}/${paymentId}`) + .then((response) => { + if (response.status === 200) { + console.log("File deleted successfully!"); + setManagingAsset({ + file: "", + complete: false, + }); + return; + } + + console.error("File deletion failed"); + }) + .catch((error) => { + console.error("Error occurred during file deletion:", error); + }); + } + }; + + const renderFileInput = ( + onChange: any, + ref: React.RefObject + ) => ( + + ); + + const handleFileChange = async (e: Event, method: "post" | "patch") => { + const newFile = (e.target as HTMLInputElement).files?.[0]; + if (newFile) { + setManagingAsset({ + file: newFile, + complete: false, + }); + + const formData = new FormData(); + formData.append("file", newFile); + + axios[method](`/api/payments/files/${type}/${paymentId}`, formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }) + .then((response) => { + if (response.status === 200) { + console.log("File uploaded successfully!"); + console.log("Uploaded File URL:", response.data.ref); + // Further actions upon successful upload + setManagingAsset({ + file: response.data.ref, + complete: true, + }); + return; + } + + console.error("File upload failed"); + }) + .catch((error) => { + console.error("Error occurred during file upload:", error); + }); + } + }; + + const downloadAsset = () => { + axios + .get(`/api/payments/files/${type}/${paymentId}`) + .then((response) => { + if (response.status === 200) { + console.log("Uploaded File URL:", response.data.url); + const link = document.createElement("a"); + link.download = response.data.filename; + link.href = response.data.url; + link.click(); + return; + } + + console.error("Failed to download file"); + }) + .catch((error) => { + console.error("Error occurred during file upload:", error); + }); + }; + + if (permissions === "read") { + if (file) return ; + return null; + } + + if (file) { + if (complete) { + return ( + <> + + fileInputReplaceRef.current?.click()} /> + + {renderFileInput( + (e: Event) => handleFileChange(e, "patch"), + fileInputReplaceRef + )} + {renderFileInput( + (e: Event) => handleFileChange(e, "post"), + fileInputRef + )} + + ); + } + + return ; + } + + return ( + <> + fileInputRef.current?.click()} /> + {renderFileInput((e: Event) => handleFileChange(e, "post"), fileInputRef)} + + ); +}; + +export default PaymentAssetManager; diff --git a/src/firebase/index.ts b/src/firebase/index.ts index 74c44245..3b29080b 100644 --- a/src/firebase/index.ts +++ b/src/firebase/index.ts @@ -1,5 +1,6 @@ import {initializeApp} from "firebase/app"; import * as admin from "firebase-admin/app"; +import { getStorage } from "firebase/storage"; const serviceAccount = require("@/constants/serviceAccountKey.json"); @@ -19,3 +20,4 @@ export const adminApp = admin.initializeApp( }, Math.random().toString(), ); +export const storage = getStorage(app); \ No newline at end of file diff --git a/src/interfaces/paypal.ts b/src/interfaces/paypal.ts index 4305b960..56bf2688 100644 --- a/src/interfaces/paypal.ts +++ b/src/interfaces/paypal.ts @@ -32,4 +32,6 @@ export interface Payment { value: number; isPaid: boolean; date: Date; + corporateTransfer?: string; + commissionTransfer?: string; } diff --git a/src/interfaces/storage.files.ts b/src/interfaces/storage.files.ts new file mode 100644 index 00000000..d43651d5 --- /dev/null +++ b/src/interfaces/storage.files.ts @@ -0,0 +1 @@ +export type FilesStorage = "commission" | "corporate"; diff --git a/src/pages/api/evaluate/interactiveSpeaking.ts b/src/pages/api/evaluate/interactiveSpeaking.ts index e1db5851..aa761ff1 100644 --- a/src/pages/api/evaluate/interactiveSpeaking.ts +++ b/src/pages/api/evaluate/interactiveSpeaking.ts @@ -4,9 +4,9 @@ import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; import axios from "axios"; import formidable from "formidable-serverless"; -import {getStorage, ref, uploadBytes} from "firebase/storage"; +import {ref, uploadBytes} from "firebase/storage"; import fs from "fs"; -import {app} from "@/firebase"; +import {storage} from "@/firebase"; export default withIronSessionApiRoute(handler, sessionOptions); @@ -16,8 +16,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return; } - const storage = getStorage(app); - const form = formidable({keepExtensions: true}); await form.parse(req, async (err: any, fields: any, files: any) => { if (err) console.log(err); diff --git a/src/pages/api/evaluate/speaking.ts b/src/pages/api/evaluate/speaking.ts index 0d632366..f7865836 100644 --- a/src/pages/api/evaluate/speaking.ts +++ b/src/pages/api/evaluate/speaking.ts @@ -4,9 +4,9 @@ import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; import axios from "axios"; import formidable from "formidable-serverless"; -import {getStorage, ref, uploadBytes} from "firebase/storage"; +import {ref, uploadBytes} from "firebase/storage"; import fs from "fs"; -import {app} from "@/firebase"; +import {storage} from "@/firebase"; export default withIronSessionApiRoute(handler, sessionOptions); @@ -16,8 +16,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return; } - const storage = getStorage(app); - const form = formidable({keepExtensions: true}); await form.parse(req, async (err: any, fields: any, files: any) => { if (err) console.log(err); diff --git a/src/pages/api/payments/files/[type]/[paymentId].ts b/src/pages/api/payments/files/[type]/[paymentId].ts new file mode 100644 index 00000000..88c4fa24 --- /dev/null +++ b/src/pages/api/payments/files/[type]/[paymentId].ts @@ -0,0 +1,194 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; +import { app, storage } from "@/firebase"; +import { + getFirestore, + getDoc, + doc, + updateDoc, + deleteField, +} from "firebase/firestore"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { FilesStorage } from "@/interfaces/storage.files"; + +import { Payment } from "@/interfaces/paypal"; +import fs from "fs"; +import { + ref, + uploadBytes, + deleteObject, + getDownloadURL, +} from "firebase/storage"; +import formidable from "formidable-serverless"; + +const db = getFirestore(app); + +const getPaymentField = (type: FilesStorage) => { + switch (type) { + case "commission": + return "commissionTransfer"; + case "corporate": + return "corporateTransfer"; + default: + return null; + } +}; + +const handleDelete = async ( + paymentId: string, + paymentField: "commissionTransfer" | "corporateTransfer" +) => { + const paymentRef = doc(db, "payments", paymentId); + const paymentDoc = await getDoc(paymentRef); + const { [paymentField]: paymentFieldPath } = paymentDoc.data() as Payment; + // Create a reference to the file to delete + const documentRef = ref(storage, paymentFieldPath); + await deleteObject(documentRef); + await updateDoc(paymentRef, { + [paymentField]: deleteField(), + }); +}; + +const handleUpload = async ( + req: NextApiRequest, + paymentId: string, + paymentField: "commissionTransfer" | "corporateTransfer" +) => + new Promise((resolve, reject) => { + const form = formidable({ keepExtensions: true }); + form.parse(req, async (err: any, fields: any, files: any) => { + if (err) { + reject(err); + return; + } + try { + const { file } = files; + const fileName = Date.now() + "-" + file.name; + const fileRef = ref(storage, fileName); + + const binary = fs.readFileSync(file.path).buffer; + const snapshot = await uploadBytes(fileRef, binary); + fs.rmSync(file.path); + + const paymentRef = doc(db, "payments", paymentId); + + await updateDoc(paymentRef, { + [paymentField]: snapshot.ref.fullPath, + }); + resolve(snapshot.ref.fullPath); + } catch (err) { + reject(err); + } + }); + }); + +export default withIronSessionApiRoute(handler, sessionOptions); +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ ok: false }); + return; + } + + if (req.method === "GET") return await get(req, res); + if (req.method === "POST") return await post(req, res); + if (req.method === "DELETE") return await del(req, res); + if (req.method === "PATCH") return await patch(req, res); + + res.status(404).json(undefined); +} + +async function get(req: NextApiRequest, res: NextApiResponse) { + const { type, paymentId } = req.query as { + type: FilesStorage; + paymentId: string; + }; + const paymentField = getPaymentField(type); + + if (paymentField === null) { + res.status(500).json({ error: "Failed to identify payment field" }); + return; + } + const paymentRef = doc(db, "payments", paymentId); + const { [paymentField]: paymentFieldPath } = ( + await getDoc(paymentRef) + ).data() as Payment; + + // Create a reference to the file to delete + const documentRef = ref(storage, paymentFieldPath); + const url = await getDownloadURL(documentRef); + res.status(200).json({ url, name: documentRef.name }); +} + +async function post(req: NextApiRequest, res: NextApiResponse) { + const { type, paymentId } = req.query as { + type: FilesStorage; + paymentId: string; + }; + const paymentField = getPaymentField(type); + + if (paymentField === null) { + res.status(500).json({ error: "Failed to identify payment field" }); + return; + } + + try { + const ref = await handleUpload(req, paymentId, paymentField); + res.status(200).json({ ref }); + } catch (error) { + res.status(500).json({ error }); + } +} + +async function del(req: NextApiRequest, res: NextApiResponse) { + const { type, paymentId } = req.query as { + type: FilesStorage; + paymentId: string; + }; + const paymentField = getPaymentField(type); + if (paymentField === null) { + res.status(500).json({ error: "Failed to identify payment field" }); + return; + } + + try { + await handleDelete(paymentId, paymentField); + res.status(200).json({ ok: true }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to delete file" }); + } +} + +async function patch(req: NextApiRequest, res: NextApiResponse) { + const { type, paymentId } = req.query as { + type: FilesStorage; + paymentId: string; + }; + const paymentField = getPaymentField(type); + if (paymentField === null) { + res.status(500).json({ error: "Failed to identify payment field" }); + return; + } + + try { + await handleDelete(paymentId, paymentField); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to delete file" }); + return; + } + + try { + const ref = await handleUpload(req, paymentId, paymentField); + res.status(200).json({ ref }); + } catch (err) { + res.status(500).json({ error: "Failed to upload file" }); + } +} + +export const config = { + api: { + bodyParser: false, + }, +}; diff --git a/src/pages/api/speaking.ts b/src/pages/api/speaking.ts index d33e927f..b1b1b8bd 100644 --- a/src/pages/api/speaking.ts +++ b/src/pages/api/speaking.ts @@ -3,7 +3,7 @@ import type {NextApiRequest, NextApiResponse} from "next"; import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; import {getDownloadURL, getStorage, ref} from "firebase/storage"; -import {app} from "@/firebase"; +import {app, storage} from "@/firebase"; import axios from "axios"; export default withIronSessionApiRoute(handler, sessionOptions); @@ -14,7 +14,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return; } - const storage = getStorage(app); const {path} = req.body as {path: string}; const pathReference = ref(storage, path); diff --git a/src/pages/api/users/update.ts b/src/pages/api/users/update.ts index 65bf3aa9..a4f80fc8 100644 --- a/src/pages/api/users/update.ts +++ b/src/pages/api/users/update.ts @@ -1,6 +1,6 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import type {NextApiRequest, NextApiResponse} from "next"; -import {app} from "@/firebase"; +import {app, storage} from "@/firebase"; import {getFirestore, collection, getDocs, getDoc, doc, setDoc} from "firebase/firestore"; import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; @@ -10,7 +10,6 @@ import {getAuth, signInWithEmailAndPassword, updateEmail, updatePassword} from " import {errorMessages} from "@/constants/errors"; const db = getFirestore(app); -const storage = getStorage(app); const auth = getAuth(app); export default withIronSessionApiRoute(handler, sessionOptions); diff --git a/src/pages/payment-record.tsx b/src/pages/payment-record.tsx index 70f094d7..cb622333 100644 --- a/src/pages/payment-record.tsx +++ b/src/pages/payment-record.tsx @@ -24,6 +24,7 @@ import Select from "react-select"; import Input from "@/components/Low/Input"; import ReactDatePicker from "react-datepicker"; import moment from "moment"; +import PaymentAssetManager from "@/components/PaymentAssetManager"; export const getServerSideProps = withIronSessionSsr(({req, res}) => { const user = req.session.user; @@ -305,6 +306,116 @@ export default function PaymentRecord() { .finally(reload); }; + const getFileAssetsColumns = () => { + if(user) { + const containerClassName = "flex gap-2 text-mti-purple-light hover:text-mti-purple-dark ease-in-out duration-300 cursor-pointer" + switch(user.type) { + case 'corporate': return [ + columnHelper.accessor("corporateTransfer", { + header: "Corporate transfer", + cell: (info) => ( +
+ +
+ ), + }), + ]; + case 'agent': return [ + columnHelper.accessor("commissionTransfer", { + header: "Commission transfer", + cell: (info) => ( +
+ +
+ ), + }), + ] + case 'admin': return [ + columnHelper.accessor("corporateTransfer", { + header: "Corporate transfer", + cell: (info) => ( +
+ +
+ ), + }), + columnHelper.accessor("commissionTransfer", { + header: "Commission transfer", + cell: (info) => ( +
+ +
+ ), + }), + ] + case 'developer': return [ + columnHelper.accessor("corporateTransfer", { + header: "Corporate transfer", + cell: (info) => ( +
+ +
+ ), + }), + columnHelper.accessor("commissionTransfer", { + header: "Commission transfer", + cell: (info) => ( +
+ +
+ ), + }), + ] + default: return []; + } + } + + return []; + } const defaultColumns = [ columnHelper.accessor("id", { header: "ID", @@ -365,6 +476,7 @@ export default function PaymentRecord() { ), }), + ...getFileAssetsColumns(), { header: "", id: "actions",