From f8541c6fac947cc4879015dea25d4f723760f01f Mon Sep 17 00:00:00 2001 From: TzuHanLiang Date: Mon, 7 Oct 2024 13:24:41 +0800 Subject: [PATCH 1/7] feat: add basic pusher outline --- package.json | 5 +- src/components/upload_area/upload_area.tsx | 8 +- src/constants/url.ts | 1 + src/pages/api/v2/pusher.ts | 30 +++++++ src/pages/mobile_upload.tsx | 85 +++++++++++++++++++ .../users/accounting/certificate_list.tsx | 61 ++++++++++++- 6 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 src/pages/api/v2/pusher.ts create mode 100644 src/pages/mobile_upload.tsx diff --git a/package.json b/package.json index a76055831..dab30b0c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iSunFA", - "version": "0.8.2+31", + "version": "0.8.2+33", "private": false, "scripts": { "dev": "next dev", @@ -41,11 +41,14 @@ "next-auth": "^4.24.7", "next-i18next": "^15.2.0", "next-logger": "^5.0.0", + "next-qrcode": "^2.5.1", "next-session": "^4.0.5", "nodemailer": "^6.9.8", "pino": "^9.3.2", "pino-multi-stream": "^6.0.0", "pino-pretty": "^11.2.2", + "pusher": "^5.2.0", + "pusher-js": "^8.4.0-rc2", "react": "^18.3.1", "react-apexcharts": "^1.4.1", "react-chartjs-2": "^5.2.0", diff --git a/src/components/upload_area/upload_area.tsx b/src/components/upload_area/upload_area.tsx index 9abbfe41d..0b599cccf 100644 --- a/src/components/upload_area/upload_area.tsx +++ b/src/components/upload_area/upload_area.tsx @@ -5,9 +5,10 @@ import Image from 'next/image'; interface UploadAreaProps { isDisabled: boolean; withScanner: boolean; + toggleQRCode: () => void; } -const UploadArea: React.FC = ({ isDisabled, withScanner }) => { +const UploadArea: React.FC = ({ isDisabled, withScanner, toggleQRCode }) => { const { t } = useTranslation(['common', 'journal']); const [isDragOver, setIsDragOver] = useState(false); @@ -109,7 +110,10 @@ const UploadArea: React.FC = ({ isDisabled, withScanner }) => { />

{t('journal:JOURNAL.USE_YOUR_PHONE_AS')} - + {t('journal:JOURNAL.SCANNER')}

diff --git a/src/constants/url.ts b/src/constants/url.ts index 725481597..b2cebf144 100644 --- a/src/constants/url.ts +++ b/src/constants/url.ts @@ -36,6 +36,7 @@ export const ISUNFA_ROUTE = { USERS_FINANCIAL_REPORTS_BALANCE_SHEET: `/users/reports/financials?report_type=${FinancialReportTypesKey.balance_sheet}`, USERS_FINANCIAL_REPORTS_INCOME_STATEMENT: `/users/reports/financials?report_type=${FinancialReportTypesKey.comprehensive_income_statement}`, USERS_FINANCIAL_REPORTS_CASH_FLOW: `/users/reports/financials?report_type=${FinancialReportTypesKey.cash_flow_statement}`, + UPLOAD: `mobile_upload`, }; export const EXTERNAL_API = { diff --git a/src/pages/api/v2/pusher.ts b/src/pages/api/v2/pusher.ts new file mode 100644 index 000000000..fa854dc5f --- /dev/null +++ b/src/pages/api/v2/pusher.ts @@ -0,0 +1,30 @@ +// /pages/api/pusher.ts +import type { NextApiRequest, NextApiResponse } from 'next'; +import Pusher from 'pusher'; + +// Info: (20241004-tzuhan) 初始化 Pusher +const pusher = new Pusher({ + appId: process.env.PUSHER_APP_ID!, + key: process.env.PUSHER_KEY!, + secret: process.env.PUSHER_SECRET!, + cluster: process.env.PUSHER_CLUSTER!, + useTLS: true, +}); + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === 'POST') { + const { images } = req.body; + + // Info: (20241004-tzuhan) 發送圖片 URL 給其他訂閱者 + try { + await pusher.trigger('certificate-channel', 'certificate-event', { + images, + }); + res.status(200).json({ status: 'Certificate sent successfully' }); + } catch (error) { + res.status(500).json({ status: 'Failed to send certificates', error }); + } + } else { + res.status(405).send('Method not allowed'); + } +} diff --git a/src/pages/mobile_upload.tsx b/src/pages/mobile_upload.tsx new file mode 100644 index 000000000..1b8830ac0 --- /dev/null +++ b/src/pages/mobile_upload.tsx @@ -0,0 +1,85 @@ +// /pages/page2.tsx +import { Button } from '@/components/button/button'; +import Image from 'next/image'; +import { useState } from 'react'; + +interface ImageData { + url: string; + status: string; +} + +const MobileUpload = () => { + const [selectedImages, setSelectedImages] = useState([]); + const [uploadedImages, setUploadedImages] = useState([]); + const [isUploading, setIsUploading] = useState(false); + + const handleImageChange = (e: React.ChangeEvent) => { + if (e.target.files) { + setSelectedImages(Array.from(e.target.files)); + } + }; + + const uploadImages = async () => { + setIsUploading(true); + + const imageUploads = await Promise.all( + selectedImages.map(async (image) => { + try { + // 模擬上傳圖片並獲得 URL + const url = URL.createObjectURL(image); + return { url, status: 'success' }; + } catch (error) { + return { url: '', status: 'failed' }; + } + }) + ); + + setUploadedImages(imageUploads); + + // 發送圖片和對應的狀態到伺服器 + try { + const response = await fetch('/api/v2/pusher', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ images: imageUploads }), + }); + + if (!response.ok) { + throw new Error('Failed to send images'); + } + } catch (error) { + // Deprecated: (20241004 - tzuhan) Debugging purpose only + // eslint-disable-next-line no-console + console.log('Error sending images:', error); + } finally { + setIsUploading(false); + } + }; + + return ( +
+

Upload Images

+ + +
+

Uploaded Images

+ {uploadedImages.map((image, index) => ( +
+ {image.status === 'success' ? ( + {`Uploaded + ) : ( +

Image {index + 1} upload failed

+ )} +

Status: {image.status}

+
+ ))} +
+
+ ); +}; + +export default MobileUpload; diff --git a/src/pages/users/accounting/certificate_list.tsx b/src/pages/users/accounting/certificate_list.tsx index 86c37234b..81abdcc39 100644 --- a/src/pages/users/accounting/certificate_list.tsx +++ b/src/pages/users/accounting/certificate_list.tsx @@ -1,4 +1,5 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useState, useEffect } from 'react'; +import { useQRCode } from 'next-qrcode'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { ILocale } from '@/interfaces/locale'; import Head from 'next/head'; @@ -12,8 +13,17 @@ import SelectionToolbar from '@/components/selection_tool_bar/selection_tool_bar import { ICertificate, ICertificateUI, OPERATIONS, VIEW_TYPES } from '@/interfaces/certificate'; import Certificate from '@/components/certificate/certificate'; import CertificateEditModal from '@/components/certificate/certificate_edit_modal'; +import Pusher, { Channel } from 'pusher-js'; +import Image from 'next/image'; +import { ISUNFA_ROUTE } from '@/constants/url'; +interface ImageData { + url: string; + status: string; +} const UploadCertificatePage: React.FC = () => { + const { Canvas } = useQRCode(); + const [showQRCode, setShowQRCode] = useState(false); const [activeTab, setActiveTab] = useState(0); const [data, setData] = useState<{ [tab: number]: { [id: number]: ICertificateUI } }>({ 0: {}, @@ -30,6 +40,7 @@ const UploadCertificatePage: React.FC = () => { 0: false, 1: false, }); + const [receivedImages, setReceivedImages] = useState([]); const handleApiResponse = useCallback((resData: ICertificate[]) => { const sumInvoiceTotalPrice = { @@ -174,6 +185,25 @@ const UploadCertificatePage: React.FC = () => { console.log('Save selected id:', certificate); }, []); + useEffect(() => { + const pusher = new Pusher(process.env.NEXT_PUBLIC_PUSHER_APP_KEY!, { + cluster: process.env.NEXT_PUBLIC_PUSHER_APP_CLUSTER!, + }); + + const channel: Channel = pusher.subscribe('certificate-channel'); + + const imageHandler = (imageData: { images: ImageData[] }) => { + setReceivedImages((prev) => [...prev, ...imageData.images]); + }; + + channel.bind('certificate-event', imageHandler); + + return () => { + channel.unbind('certificate-event', imageHandler); + pusher.unsubscribe('certificate-channel'); + }; + }, []); + return ( <> @@ -200,10 +230,37 @@ const UploadCertificatePage: React.FC = () => { {/* Info: (20240919 - tzuhan) Header */}
+ {showQRCode && ( + + )} {/* Info: (20240919 - tzuhan) Main Content */}
{/* Info: (20240919 - tzuhan) Upload Area */} - + setShowQRCode((prev) => !prev)} + /> +
+ {receivedImages.map((image, index) => ( +
+ {`Received +

Status: {image.status}

+
+ ))} +
{/* Info: (20240919 - tzuhan) Tabs */} Date: Mon, 7 Oct 2024 17:36:28 +0800 Subject: [PATCH 2/7] feat: add API to test [unfinish] --- package.json | 2 +- src/components/certificate/certificate.tsx | 5 +- .../certificate/certificate_grid.tsx | 31 ++- .../certificate/certificate_qrcode_modal.tsx | 74 +++++++ .../floating_upload_popup.tsx | 22 ++- src/components/upload_area/upload_area.tsx | 4 +- src/interfaces/certificate.ts | 17 +- src/pages/api/v2/pusher.ts | 23 ++- src/pages/api/v2/upload.ts | 67 +++++++ .../accounting => }/certificate_list.tsx | 108 ++++++---- src/pages/mobile_upload.tsx | 187 +++++++++++++----- 11 files changed, 409 insertions(+), 131 deletions(-) create mode 100644 src/components/certificate/certificate_qrcode_modal.tsx create mode 100644 src/pages/api/v2/upload.ts rename src/pages/{users/accounting => }/certificate_list.tsx (77%) diff --git a/package.json b/package.json index dab30b0c6..c637fe216 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iSunFA", - "version": "0.8.2+33", + "version": "0.8.2+34", "private": false, "scripts": { "dev": "next dev", diff --git a/src/components/certificate/certificate.tsx b/src/components/certificate/certificate.tsx index 350ffe1d2..d77902a73 100644 --- a/src/components/certificate/certificate.tsx +++ b/src/components/certificate/certificate.tsx @@ -2,12 +2,11 @@ import React, { useState } from 'react'; import Pagination from '@/components/pagination/pagination'; import { ICertificateUI, VIEW_TYPES } from '@/interfaces/certificate'; import CertificateTable from '@/components/certificate/certificate_table'; -import CertificateGrid from './certificate_grid'; +import CertificateGrid from '@/components/certificate/certificate_grid'; interface CertificateProps { data: ICertificateUI[]; // Info: (20240923 - tzuhan) 項目列表 viewType: VIEW_TYPES; // Info: (20240923 - tzuhan) 顯示模式 - activeTab: number; // Info: (20240926 - tzuhan) 活躍的 Tab activeSelection: boolean; // Info: (20240923 - tzuhan) 是否處於選擇狀態 handleSelect: (ids: number[], isSelected: boolean) => void; isSelectedAll: boolean; @@ -21,7 +20,6 @@ interface CertificateProps { const Certificate: React.FC = ({ data, viewType, - activeTab, activeSelection, handleSelect, isSelectedAll, @@ -48,7 +46,6 @@ const Certificate: React.FC = ({ {viewType === VIEW_TYPES.GRID && ( void; onRemove: (id: number) => void; onDownload: (id: number) => void; @@ -16,29 +14,24 @@ interface CertificateGridProps { const CertificateGrid: React.FC = ({ data, activeSelection, - activeTab, handleSelect, onRemove, onDownload, onEdit, }) => { return ( - <> -
- {data.map((certificate) => ( - - ))} -
- {/* Info: (20240926- tzuhan) Floating Upload Popup */} - {activeTab === 0 && } - +
+ {data.map((certificate) => ( + + ))} +
); }; diff --git a/src/components/certificate/certificate_qrcode_modal.tsx b/src/components/certificate/certificate_qrcode_modal.tsx new file mode 100644 index 000000000..cb163802b --- /dev/null +++ b/src/components/certificate/certificate_qrcode_modal.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { ISUNFA_ROUTE } from '@/constants/url'; +import { DOMAIN } from '@/constants/config'; +import { RxCross1 } from 'react-icons/rx'; +import { Button } from '@/components/button/button'; +import { useQRCode } from 'next-qrcode'; + +interface CertificateQRCodeModalProps { + isOpen: boolean; + isOnTopOfModal: boolean; + token: string; + onClose: () => void; // Info: (20240924 - tzuhan) 關閉模態框的回調函數 +} + +const CertificateQRCodeModal: React.FC = ({ + isOpen, + isOnTopOfModal = false, + token, + onClose, +}) => { + if (!isOpen) return null; + const { Canvas } = useQRCode(); + const isDev = process.env.NODE_ENV === 'development'; + + return ( +
+
+ {/* Info: (20240924 - tzuhan) 關閉按鈕 */} + + {/* Info: (20240924 - tzuhan) 模態框標題 */} +

+
Url
+
for mobile upload
+

+
+ {/* Info: (20240924 - tzuhan) 發票縮略圖 */} + +
+
+ +
+
+
+ ); +}; + +export default CertificateQRCodeModal; diff --git a/src/components/floating_upload_popup/floating_upload_popup.tsx b/src/components/floating_upload_popup/floating_upload_popup.tsx index 7a772852b..c3959ecdc 100644 --- a/src/components/floating_upload_popup/floating_upload_popup.tsx +++ b/src/components/floating_upload_popup/floating_upload_popup.tsx @@ -1,14 +1,20 @@ import React, { useState } from 'react'; -import UploadFileItem, { UploadFile } from '@/components/upload_certificate/upload_file_item'; +import UploadFileItem from '@/components/upload_certificate/upload_file_item'; import { ProgressStatus } from '@/constants/account'; import Image from 'next/image'; +import { ICertificateInfo } from '@/interfaces/certificate'; -const FloatingUploadPopup: React.FC = () => { - const [files, setFiles] = useState([ - { name: 'preline-ui.xls', size: 7, progress: 20, status: ProgressStatus.IN_PROGRESS }, - { name: 'preline-ui.xls', size: 7, progress: 50, status: ProgressStatus.IN_PROGRESS }, - { name: 'preline-ui.xls', size: 7, progress: 80, status: ProgressStatus.IN_PROGRESS }, - ]); +interface FloatingUploadPopupProps { + uploadingCertificates: ICertificateInfo[]; +} + +const FloatingUploadPopup: React.FC = ({ uploadingCertificates }) => { + const [files, setFiles] = useState(uploadingCertificates); + // const [files, setFiles] = useState([ + // { name: 'preline-ui.xls', size: 7, progress: 20, status: ProgressStatus.IN_PROGRESS }, + // { name: 'preline-ui.xls', size: 7, progress: 50, status: ProgressStatus.IN_PROGRESS }, + // { name: 'preline-ui.xls', size: 7, progress: 80, status: ProgressStatus.IN_PROGRESS }, + // ]); const [expanded, setExpanded] = useState(false); // Info: (20240919 - tzuhan) 控制展開/收縮狀態 // Info: (20240919 - tzuhan) 計算總上傳進度和狀態 @@ -19,7 +25,7 @@ const FloatingUploadPopup: React.FC = () => { ); // Info: (20240919 - tzuhan) 暫停或繼續上傳 - const updateFileStatus = (prevFiles: UploadFile[], index: number) => + const updateFileStatus = (prevFiles: ICertificateInfo[], index: number) => prevFiles.map((file, i) => { return i === index ? { diff --git a/src/components/upload_area/upload_area.tsx b/src/components/upload_area/upload_area.tsx index 0b599cccf..ef47fdbca 100644 --- a/src/components/upload_area/upload_area.tsx +++ b/src/components/upload_area/upload_area.tsx @@ -5,7 +5,7 @@ import Image from 'next/image'; interface UploadAreaProps { isDisabled: boolean; withScanner: boolean; - toggleQRCode: () => void; + toggleQRCode?: () => void; } const UploadArea: React.FC = ({ isDisabled, withScanner, toggleQRCode }) => { @@ -82,7 +82,7 @@ const UploadArea: React.FC = ({ isDisabled, withScanner, toggle /> - {withScanner && ( + {withScanner && toggleQRCode && ( <>

{t('common:COMMON.OR')} diff --git a/src/interfaces/certificate.ts b/src/interfaces/certificate.ts index 050dc835f..42220e0c9 100644 --- a/src/interfaces/certificate.ts +++ b/src/interfaces/certificate.ts @@ -1,5 +1,6 @@ -// Info: (20240920 - tzuhan) 定義 ICertificate 接口 +import { ProgressStatus } from '@/constants/account'; +// Info: (20240920 - tzuhan) 定義 ICertificate 接口 export enum CERTIFICATE_TYPES { INPUT = 'Input', OUTPUT = 'Output', @@ -30,6 +31,20 @@ export interface ICertificate { uploader: string; } +export interface ICertificateData { + token: string; + url: string; + status: ProgressStatus; +} + +export interface ICertificateInfo { + url: string; + name: string; + size: number; + status: ProgressStatus; + progress: number; +} + export enum VIEW_TYPES { GRID = 'grid', LIST = 'list', diff --git a/src/pages/api/v2/pusher.ts b/src/pages/api/v2/pusher.ts index fa854dc5f..b6ca42850 100644 --- a/src/pages/api/v2/pusher.ts +++ b/src/pages/api/v2/pusher.ts @@ -1,25 +1,32 @@ -// /pages/api/pusher.ts import type { NextApiRequest, NextApiResponse } from 'next'; import Pusher from 'pusher'; // Info: (20241004-tzuhan) 初始化 Pusher const pusher = new Pusher({ appId: process.env.PUSHER_APP_ID!, - key: process.env.PUSHER_KEY!, + key: process.env.NEXT_PUBLIC_PUSHER_KEY!, secret: process.env.PUSHER_SECRET!, - cluster: process.env.PUSHER_CLUSTER!, - useTLS: true, + host: process.env.NEXT_PUBLIC_PUSHER_HOST!, + useTLS: process.env.PUSHER_USE_TLS === 'true', }); export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method === 'POST') { - const { images } = req.body; + const { certificates } = req.body; + + // eslint-disable-next-line no-console + console.log('certificates:', certificates); // Info: (20241004-tzuhan) 發送圖片 URL 給其他訂閱者 try { - await pusher.trigger('certificate-channel', 'certificate-event', { - images, - }); + await Promise.all( + certificates.map(async (certificate: { url: string; token: string }) => { + await pusher.trigger('certificate-channel', 'certificate-event', { + url: certificate.url, + token: certificate.token, + }); + }) + ); res.status(200).json({ status: 'Certificate sent successfully' }); } catch (error) { res.status(500).json({ status: 'Failed to send certificates', error }); diff --git a/src/pages/api/v2/upload.ts b/src/pages/api/v2/upload.ts new file mode 100644 index 000000000..0bef24f54 --- /dev/null +++ b/src/pages/api/v2/upload.ts @@ -0,0 +1,67 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { IncomingForm, File as FormidableFile } from 'formidable'; +import { promises as fs } from 'fs'; + +import path from 'path'; + +// Info: (20241007 - tzuhan) 禁用內建的 body 解析 +export const config = { + api: { + bodyParser: false, + }, +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method === 'POST') { + const form = new IncomingForm({ + uploadDir: path.join(process.cwd(), '/tmp'), + keepExtensions: true, // Info: (20241007 - tzuhan) 保留文件擴展名 + }); + + // Deprecated: (20241011 - tzuhan) Debugging purpose + // eslint-disable-next-line no-console + console.log('form:', form); + + form.parse(req, async (err, fields, files) => { + if (err) { + // Deprecated: (20241011 - tzuhan) Debugging purpose + // eslint-disable-next-line no-console + console.error('Error parsing the files:', err); + return res.status(500).json({ message: 'File upload error' }); + } + + // Deprecated: (20241011 - tzuhan) Debugging purpose + // eslint-disable-next-line no-console + console.log('files:', files); + + // Info: (20241007 - tzuhan) 處理多個文件 + const uploadedFiles = Array.isArray(files.files) ? files.files : [files.files]; // 確保它是數組 + + const uploadDir = path.join(process.cwd(), '/public/uploads'); + await fs.mkdir(uploadDir, { recursive: true }); // Info: (20241007 - tzuhan) 確保上傳目錄存在 + + // Info: (20241007 - tzuhan) 生成每個文件的 URL 和 token + const certificates = await Promise.all( + uploadedFiles + .filter((file: FormidableFile | undefined) => !!file) + .map(async (file: FormidableFile) => { + const tempFilePath = file.filepath; + const fileName = `${Date.now()}_${file.originalFilename}`; + const finalFilePath = path.join(uploadDir, fileName); + + await fs.rename(tempFilePath, finalFilePath); // Info: (20241007 - tzuhan) 將文件移動到最終目錄 + + const fileUrl = `/uploads/${fileName}`; // Info: (20241007 - tzuhan) 文件的相對 URL + + return { + fileUrl, + }; + }) + ); + + return res.status(200).json({ certificates }); + }); + } else { + res.status(405).json({ message: 'Method not allowed' }); + } +} diff --git a/src/pages/users/accounting/certificate_list.tsx b/src/pages/certificate_list.tsx similarity index 77% rename from src/pages/users/accounting/certificate_list.tsx rename to src/pages/certificate_list.tsx index 81abdcc39..7191ff249 100644 --- a/src/pages/users/accounting/certificate_list.tsx +++ b/src/pages/certificate_list.tsx @@ -1,5 +1,4 @@ import React, { useCallback, useState, useEffect } from 'react'; -import { useQRCode } from 'next-qrcode'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { ILocale } from '@/interfaces/locale'; import Head from 'next/head'; @@ -10,19 +9,23 @@ import Tabs from '@/components/tabs/tabs'; import FilterSection from '@/components/filter_section/filter_section'; import { APIName } from '@/constants/api_connection'; import SelectionToolbar from '@/components/selection_tool_bar/selection_tool_bar'; -import { ICertificate, ICertificateUI, OPERATIONS, VIEW_TYPES } from '@/interfaces/certificate'; +import { + ICertificate, + ICertificateInfo, + ICertificateUI, + OPERATIONS, + VIEW_TYPES, +} from '@/interfaces/certificate'; import Certificate from '@/components/certificate/certificate'; import CertificateEditModal from '@/components/certificate/certificate_edit_modal'; import Pusher, { Channel } from 'pusher-js'; +import FloatingUploadPopup from '@/components/floating_upload_popup/floating_upload_popup'; +import CertificateQRCodeModal from '@/components/certificate/certificate_qrcode_modal'; import Image from 'next/image'; -import { ISUNFA_ROUTE } from '@/constants/url'; +import { v4 as uuidv4 } from 'uuid'; +import { ProgressStatus } from '@/constants/account'; -interface ImageData { - url: string; - status: string; -} const UploadCertificatePage: React.FC = () => { - const { Canvas } = useQRCode(); const [showQRCode, setShowQRCode] = useState(false); const [activeTab, setActiveTab] = useState(0); const [data, setData] = useState<{ [tab: number]: { [id: number]: ICertificateUI } }>({ @@ -40,7 +43,8 @@ const UploadCertificatePage: React.FC = () => { 0: false, 1: false, }); - const [receivedImages, setReceivedImages] = useState([]); + const token = uuidv4(); // Info: (20241007 - tzuhan) 生成唯一 token + const [uploadingCertificates, setUploadingCertificates] = useState([]); const handleApiResponse = useCallback((resData: ICertificate[]) => { const sumInvoiceTotalPrice = { @@ -185,21 +189,58 @@ const UploadCertificatePage: React.FC = () => { console.log('Save selected id:', certificate); }, []); + const certificateHandler = async (certificateData: { url: string; token: string }) => { + // TODO: (20241007 - tzuhan) post certificate data to server and get uploading certificate list back and update certificate list when uploaded + // TODO: (20241007 - tzuhan) get Token from server + if (certificateData.token === token) { + // Info: (20241007 - tzuhan) 使用 fetch 下載圖片文件 + const response = await fetch(certificateData.url); + const blob = await response.blob(); + + // Info: (20241007 - tzuhan) 獲取文件名,從 response headers 提取 + const contentDisposition = response.headers.get('Content-Disposition'); + let fileName = 'unknown'; + if (contentDisposition) { + const match = contentDisposition.match(/filename="(.+)"/); + if (match && match[1]) { + // Info: (20241007 - tzuhan) 使用陣列解構提取文件名 + [, fileName] = match; + } + } else { + // Info: (20241007 - tzuhan) 如果沒有提供 header,可以從 url 推斷出文件名 + fileName = certificateData.url.split('/').pop() || 'unknown'; + } + + // Info: (20241007 - tzuhan) 獲取文件大小 + const fileSize = blob.size; + + const imageObjectUrl = URL.createObjectURL(blob); + setUploadingCertificates((prev) => [ + ...prev, + { + url: imageObjectUrl, + status: ProgressStatus.IN_PROGRESS, + name: fileName, + size: fileSize, + progress: 80, + }, + ]); + } + }; + useEffect(() => { - const pusher = new Pusher(process.env.NEXT_PUBLIC_PUSHER_APP_KEY!, { - cluster: process.env.NEXT_PUBLIC_PUSHER_APP_CLUSTER!, + const pusher = new Pusher(process.env.NEXT_PUBLIC_PUSHER_KEY!, { + cluster: '', + wsHost: process.env.NEXT_PUBLIC_PUSHER_HOST!, + wsPort: parseFloat(process.env.NEXT_PUBLIC_PUSHER_PORT!), }); const channel: Channel = pusher.subscribe('certificate-channel'); - const imageHandler = (imageData: { images: ImageData[] }) => { - setReceivedImages((prev) => [...prev, ...imageData.images]); - }; - - channel.bind('certificate-event', imageHandler); + channel.bind('certificate-event', certificateHandler); return () => { - channel.unbind('certificate-event', imageHandler); + channel.unbind('certificate-event', certificateHandler); pusher.unsubscribe('certificate-channel'); }; }, []); @@ -221,6 +262,14 @@ const UploadCertificatePage: React.FC = () => { onSave={handleSave} /> )} + {showQRCode && ( + setShowQRCode((prev) => !prev)} + isOnTopOfModal={false} + token={token} + /> + )} {/* Info: (20240919 - tzuhan) Side Menu */} @@ -229,22 +278,6 @@ const UploadCertificatePage: React.FC = () => {
{/* Info: (20240919 - tzuhan) Header */}
- - {showQRCode && ( - - )} {/* Info: (20240919 - tzuhan) Main Content */}
{/* Info: (20240919 - tzuhan) Upload Area */} @@ -254,7 +287,7 @@ const UploadCertificatePage: React.FC = () => { toggleQRCode={() => setShowQRCode((prev) => !prev)} />
- {receivedImages.map((image, index) => ( + {uploadingCertificates.map((image, index) => (
{`Received

Status: {image.status}

@@ -292,7 +325,7 @@ const UploadCertificatePage: React.FC = () => { onActiveChange={setActiveSelection} items={Object.values(data[activeTab])} itemType="Certificates" - subtitle={`Invoice Total Price: ${sumPrice} TWD`} + subtitle={`Invoice Total Price: ${sumPrice[activeTab]} TWD`} selectedCount={filterSelectedIds().length} totalCount={Object.values(data[activeTab]).length || 0} handleSelect={handleSelect} @@ -307,7 +340,6 @@ const UploadCertificatePage: React.FC = () => { { onEdit={onEdit} />
+ {/* Info: (20240926- tzuhan) Floating Upload Popup */} + {uploadingCertificates.length > 0 && ( + + )}
diff --git a/src/pages/mobile_upload.tsx b/src/pages/mobile_upload.tsx index 1b8830ac0..6b879b85a 100644 --- a/src/pages/mobile_upload.tsx +++ b/src/pages/mobile_upload.tsx @@ -1,85 +1,168 @@ -// /pages/page2.tsx +import { useRouter } from 'next/router'; import { Button } from '@/components/button/button'; +import Head from 'next/head'; import Image from 'next/image'; import { useState } from 'react'; +import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; +import { ILocale } from '@/interfaces/locale'; +import { ICertificateData } from '@/interfaces/certificate'; +import { ProgressStatus } from '@/constants/account'; -interface ImageData { +interface FileWithUrl extends File { url: string; - status: string; } -const MobileUpload = () => { - const [selectedImages, setSelectedImages] = useState([]); - const [uploadedImages, setUploadedImages] = useState([]); +const MobileUploadPage: React.FC = () => { + const router = useRouter(); + const { query } = router; + const { token } = query; + const [selectedCertificates, setSelectedCertificates] = useState([]); + const [uploadedCertificates, setUploadedCertificates] = useState([]); const [isUploading, setIsUploading] = useState(false); - const handleImageChange = (e: React.ChangeEvent) => { + const handleCertificateChange = (e: React.ChangeEvent) => { if (e.target.files) { - setSelectedImages(Array.from(e.target.files)); + setSelectedCertificates( + Array.from(e.target.files).map((file) => { + return { + ...file, + url: URL.createObjectURL(file), + }; + }) + ); } }; - const uploadImages = async () => { + const uploadCertificates = async () => { setIsUploading(true); - const imageUploads = await Promise.all( - selectedImages.map(async (image) => { - try { - // 模擬上傳圖片並獲得 URL - const url = URL.createObjectURL(image); - return { url, status: 'success' }; - } catch (error) { - return { url: '', status: 'failed' }; - } - }) - ); - - setUploadedImages(imageUploads); - - // 發送圖片和對應的狀態到伺服器 + // Info: (20241007 - tzuhan) 使用 FormData 上傳文件 + const formData = new FormData(); + selectedCertificates.forEach((certificate) => { + formData.append('file', certificate); + }); + try { - const response = await fetch('/api/v2/pusher', { + const response = await fetch('/api/v2/upload', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ images: imageUploads }), + body: formData, }); + const data = await response.json(); if (!response.ok) { - throw new Error('Failed to send images'); + throw new Error('Failed to upload certificates'); } + + // Deprecated: (20241011 - tzuhan) Debugging purpose + // eslint-disable-next-line no-console + console.log('data:', data); + + const { certificates } = data; + certificates.forEach(async (certificate: { fileUrl: string }) => { + const certificateUpload = { + url: certificate.fileUrl, + token: token as string, + status: ProgressStatus.IN_PROGRESS, + }; + + setUploadedCertificates([...uploadedCertificates, certificateUpload]); + + // Deprecated: (20241011 - tzuhan) Debugging purpose + // eslint-disable-next-line no-console + console.log('certificates:', certificateUpload); + + // Info: (20241007 - tzuhan) 通過 Pusher 傳送圖片 URL 和 token + await fetch('/api/v2/pusher', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ certificate: certificateUpload }), + }); + }); + + setSelectedCertificates([]); } catch (error) { - // Deprecated: (20241004 - tzuhan) Debugging purpose only + // Deprecated: (20241011 - tzuhan) Debugging purpose // eslint-disable-next-line no-console - console.log('Error sending images:', error); + console.error('Error uploading certificates:', error); } finally { setIsUploading(false); } }; return ( -
-

Upload Images

- - -
-

Uploaded Images

- {uploadedImages.map((image, index) => ( -
- {image.status === 'success' ? ( - {`Uploaded - ) : ( -

Image {index + 1} upload failed

- )} -

Status: {image.status}

+ <> + + + + + Upload Certificate - iSunFA + +
+

Upload Certificates

+ + +
+

Selected Certificates

+
+ {selectedCertificates.map((certificate, index) => ( +
+ {`Uploaded +
+ ))}
- ))} -
-
+
+
+

Uploaded Certificates

+
+ {uploadedCertificates.map((certificate, index) => ( +
+ {certificate.status !== ProgressStatus.SYSTEM_ERROR ? ( + {`Uploaded + ) : ( +

Certificate {index + 1} upload failed

+ )} +

Status: {certificate.status}

+
+ ))} +
+
+ + ); }; -export default MobileUpload; +const getStaticPropsFunction = async ({ locale }: ILocale) => ({ + props: { + ...(await serverSideTranslations(locale, [ + 'common', + 'journal', + 'kyc', + 'project', + 'report_401', + 'salary', + 'setting', + 'terms', + ])), + }, +}); + +export const getStaticProps = getStaticPropsFunction; + +export default MobileUploadPage; From 88fda1cbde4abada611ec6a37dfa123ecdbfee67 Mon Sep 17 00:00:00 2001 From: Tiny_Murky Date: Wed, 9 Oct 2024 11:58:15 +0800 Subject: [PATCH 3/7] Cash flow begin fix --- package.json | 2 +- src/interfaces/voucher.ts | 15 +++++++++++++ src/lib/utils/repo/voucher.beta.repo.ts | 6 +++++- src/lib/utils/repo/voucher.repo.ts | 6 +++++- .../report/cash_flow_statement_generator.ts | 21 +++++++++++++------ 5 files changed, 41 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 47ed8048f..038ad00ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iSunFA", - "version": "0.8.2+37", + "version": "0.8.2+38", "private": false, "scripts": { "dev": "next dev", diff --git a/src/interfaces/voucher.ts b/src/interfaces/voucher.ts index cc619f839..96a05facf 100644 --- a/src/interfaces/voucher.ts +++ b/src/interfaces/voucher.ts @@ -58,6 +58,21 @@ export type IVoucherFromPrismaIncludeJournalLineItems = Prisma.VoucherGetPayload }; }>; +export type IVoucherForCashFlow = Prisma.VoucherGetPayload<{ + include: { + journal: { + include: { + invoice: true; + }; + }; + lineItems: { + include: { + account: true; + }; + }; + }; +}>; + export type IVoucherFromPrismaIncludeLineItems = Prisma.VoucherGetPayload<{ include: { lineItems: { diff --git a/src/lib/utils/repo/voucher.beta.repo.ts b/src/lib/utils/repo/voucher.beta.repo.ts index 0cdf7fe44..dffe6e065 100644 --- a/src/lib/utils/repo/voucher.beta.repo.ts +++ b/src/lib/utils/repo/voucher.beta.repo.ts @@ -191,7 +191,11 @@ export async function findManyVoucherWithCashInPrisma( }; const include = { - journal: true, + journal: { + include: { + invoice: true, + }, + }, lineItems: { include: { account: true, diff --git a/src/lib/utils/repo/voucher.repo.ts b/src/lib/utils/repo/voucher.repo.ts index a41f47259..2e291938c 100644 --- a/src/lib/utils/repo/voucher.repo.ts +++ b/src/lib/utils/repo/voucher.repo.ts @@ -303,7 +303,11 @@ export async function findManyVoucherWithCashInPrisma( }, }, include: { - journal: true, + journal: { + include: { + invoice: true, + }, + }, lineItems: { include: { account: true, diff --git a/src/lib/utils/report/cash_flow_statement_generator.ts b/src/lib/utils/report/cash_flow_statement_generator.ts index 60b2dfe64..afec4c42b 100644 --- a/src/lib/utils/report/cash_flow_statement_generator.ts +++ b/src/lib/utils/report/cash_flow_statement_generator.ts @@ -9,7 +9,10 @@ import { } from '@/interfaces/accounting_account'; import { IDirectCashFlowMapping, IOperatingCashFlowMapping } from '@/interfaces/cash_flow'; import { OPERATING_CASH_FLOW_INDIRECT_MAPPING } from '@/constants/cash_flow/operating_cash_flow'; -import { IVoucherFromPrismaIncludeJournalLineItems } from '@/interfaces/voucher'; +import { + IVoucherForCashFlow, + IVoucherFromPrismaIncludeJournalLineItems, +} from '@/interfaces/voucher'; import { findManyVoucherWithCashInPrisma } from '@/lib/utils/repo/voucher.repo'; import { INVESTING_CASH_FLOW_DIRECT_MAPPING } from '@/constants/cash_flow/investing_cash_flow'; import { FINANCING_CASH_FLOW_DIRECT_MAPPING } from '@/constants/cash_flow/financing_cash_flow'; @@ -28,7 +31,7 @@ export default class CashFlowStatementGenerator extends FinancialReportGenerator private voucherRelatedToCash: IVoucherFromPrismaIncludeJournalLineItems[]; - private voucherLastPeriod: IVoucherFromPrismaIncludeJournalLineItems[]; + private voucherLastPeriod: IVoucherForCashFlow[]; private YEAR_RANGE = 5; @@ -38,7 +41,7 @@ export default class CashFlowStatementGenerator extends FinancialReportGenerator companyId: number, startDateInSecond: number, endDateInSecond: number, - voucherRelatedToCash: IVoucherFromPrismaIncludeJournalLineItems[] + voucherRelatedToCash: IVoucherForCashFlow[] ) { const reportSheetType = ReportSheetType.CASH_FLOW_STATEMENT; super(companyId, startDateInSecond, endDateInSecond, reportSheetType); @@ -54,13 +57,19 @@ export default class CashFlowStatementGenerator extends FinancialReportGenerator endDateInSecond ); this.voucherRelatedToCash = voucherRelatedToCash.filter((voucher) => { - const laterThanStartDate = voucher.journal.createdAt >= startDateInSecond; - const earlierThanEndDate = voucher.journal.createdAt <= endDateInSecond; + const laterThanStartDate = voucher.journal?.invoice?.date + ? voucher.journal.invoice.date >= startDateInSecond + : false; + const earlierThanEndDate = voucher.journal?.invoice?.date + ? voucher.journal?.invoice?.date <= endDateInSecond + : false; return laterThanStartDate && earlierThanEndDate; }); this.voucherLastPeriod = voucherRelatedToCash.filter((voucher) => { - const earlierThanStartDate = voucher.journal.createdAt < startDateInSecond; + const earlierThanStartDate = voucher.journal?.invoice?.date + ? voucher.journal?.invoice?.date < startDateInSecond + : false; return earlierThanStartDate; }); } From 25a707f008b1820e3a754f3d530f92c5663e711f Mon Sep 17 00:00:00 2001 From: TzuHanLiang Date: Wed, 9 Oct 2024 15:52:29 +0800 Subject: [PATCH 4/7] feat: pusher implement --- package.json | 2 +- .../certificate/certificate_qrcode_modal.tsx | 8 +- .../floating_upload_popup.tsx | 43 +++-- src/constants/account.ts | 1 + src/constants/api_connection.ts | 28 ++++ src/constants/file.ts | 3 + src/constants/pusher.ts | 13 ++ src/interfaces/api_connection.ts | 9 +- src/interfaces/certificate.ts | 10 +- src/lib/pusher.ts | 19 +++ src/lib/pusherClient.ts | 26 +++ src/lib/utils/parse_image_form.ts | 40 ++++- src/lib/utils/pusher_token.ts | 32 ++++ src/pages/api/v2/encrypt.ts | 24 +++ src/pages/api/v2/pusher.ts | 59 +++---- src/pages/api/v2/upload.ts | 110 ++++++------ src/pages/mobile_upload.tsx | 157 ++++++++++++------ .../accounting}/certificate_list.tsx | 137 ++++++++------- 18 files changed, 491 insertions(+), 230 deletions(-) create mode 100644 src/constants/pusher.ts create mode 100644 src/lib/pusher.ts create mode 100644 src/lib/pusherClient.ts create mode 100644 src/lib/utils/pusher_token.ts create mode 100644 src/pages/api/v2/encrypt.ts rename src/pages/{ => users/accounting}/certificate_list.tsx (76%) diff --git a/package.json b/package.json index c637fe216..45349ca1b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iSunFA", - "version": "0.8.2+34", + "version": "0.8.2+35", "private": false, "scripts": { "dev": "next dev", diff --git a/src/components/certificate/certificate_qrcode_modal.tsx b/src/components/certificate/certificate_qrcode_modal.tsx index cb163802b..49fffb986 100644 --- a/src/components/certificate/certificate_qrcode_modal.tsx +++ b/src/components/certificate/certificate_qrcode_modal.tsx @@ -40,7 +40,7 @@ const CertificateQRCodeModal: React.FC = ({
Url
for mobile upload

-
+
{/* Info: (20240924 - tzuhan) 發票縮略圖 */} = ({ }, }} /> + {`${isDev ? 'http://localhost:3000' : DOMAIN}/${ISUNFA_ROUTE.UPLOAD}?token=${token}`}
@@ -127,8 +180,8 @@ const MobileUploadPage: React.FC = () => {

Uploaded Certificates

{uploadedCertificates.map((certificate, index) => ( -
- {certificate.status !== ProgressStatus.SYSTEM_ERROR ? ( +
+ {certificate.status !== ProgressStatus.FAILED ? ( {`Uploaded { ); }; -const getStaticPropsFunction = async ({ locale }: ILocale) => ({ +export const getStaticProps = async ({ locale }: ILocale) => ({ props: { ...(await serverSideTranslations(locale, [ 'common', @@ -163,6 +216,4 @@ const getStaticPropsFunction = async ({ locale }: ILocale) => ({ }, }); -export const getStaticProps = getStaticPropsFunction; - export default MobileUploadPage; diff --git a/src/pages/certificate_list.tsx b/src/pages/users/accounting/certificate_list.tsx similarity index 76% rename from src/pages/certificate_list.tsx rename to src/pages/users/accounting/certificate_list.tsx index 7191ff249..a9604fdc6 100644 --- a/src/pages/certificate_list.tsx +++ b/src/pages/users/accounting/certificate_list.tsx @@ -18,14 +18,19 @@ import { } from '@/interfaces/certificate'; import Certificate from '@/components/certificate/certificate'; import CertificateEditModal from '@/components/certificate/certificate_edit_modal'; -import Pusher, { Channel } from 'pusher-js'; +import { getPusherInstance } from '@/lib/pusherClient'; import FloatingUploadPopup from '@/components/floating_upload_popup/floating_upload_popup'; import CertificateQRCodeModal from '@/components/certificate/certificate_qrcode_modal'; -import Image from 'next/image'; -import { v4 as uuidv4 } from 'uuid'; -import { ProgressStatus } from '@/constants/account'; +import APIHandler from '@/lib/utils/api_handler'; +import { CERTIFICATE_EVENT, PRIVATE_CHANNEL } from '@/constants/pusher'; +import useStateRef from 'react-usestateref'; +import { useUserCtx } from '@/contexts/user_context'; -const UploadCertificatePage: React.FC = () => { +const CertificateListPage: React.FC = () => { + const { selectedCompany } = useUserCtx(); + const { id: companyId } = selectedCompany!; + const { trigger: encryptAPI } = APIHandler(APIName.ENCRYPT); + const [token, setToken, tokenRef] = useStateRef(undefined); const [showQRCode, setShowQRCode] = useState(false); const [activeTab, setActiveTab] = useState(0); const [data, setData] = useState<{ [tab: number]: { [id: number]: ICertificateUI } }>({ @@ -43,8 +48,10 @@ const UploadCertificatePage: React.FC = () => { 0: false, 1: false, }); - const token = uuidv4(); // Info: (20241007 - tzuhan) 生成唯一 token - const [uploadingCertificates, setUploadingCertificates] = useState([]); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [uploadingCertificates, setUploadingCertificates, uploadingCertificatesRef] = useStateRef<{ + [id: number]: ICertificateInfo; + }>({}); const handleApiResponse = useCallback((resData: ICertificate[]) => { const sumInvoiceTotalPrice = { @@ -189,61 +196,60 @@ const UploadCertificatePage: React.FC = () => { console.log('Save selected id:', certificate); }, []); - const certificateHandler = async (certificateData: { url: string; token: string }) => { - // TODO: (20241007 - tzuhan) post certificate data to server and get uploading certificate list back and update certificate list when uploaded - // TODO: (20241007 - tzuhan) get Token from server - if (certificateData.token === token) { - // Info: (20241007 - tzuhan) 使用 fetch 下載圖片文件 - const response = await fetch(certificateData.url); - const blob = await response.blob(); + const certificateHandler = useCallback( + async (message: { token: string; certificate: ICertificateInfo }) => { + const { token: receivedToken, certificate: certificateData } = message; + // Deprecated: (20241011 - tzuhan) Debugging purpose + // eslint-disable-next-line no-console + console.log( + `pusher got message, (token(${tokenRef.current})===_token${receivedToken})?${tokenRef.current === receivedToken} message:`, + message + ); - // Info: (20241007 - tzuhan) 獲取文件名,從 response headers 提取 - const contentDisposition = response.headers.get('Content-Disposition'); - let fileName = 'unknown'; - if (contentDisposition) { - const match = contentDisposition.match(/filename="(.+)"/); - if (match && match[1]) { - // Info: (20241007 - tzuhan) 使用陣列解構提取文件名 - [, fileName] = match; - } + if (receivedToken === tokenRef.current) { + const updatedCertificates = { + ...uploadingCertificatesRef.current, + }; + updatedCertificates[certificateData.id] = certificateData; + setUploadingCertificates(updatedCertificates); + // Deprecated: (20241011 - tzuhan) Debugging purpose + // eslint-disable-next-line no-console + console.log(`uploadingCertificatesRef.current:`, uploadingCertificatesRef.current); + } + }, + [tokenRef, setUploadingCertificates] + ); + const getToken = useCallback(async () => { + if (!tokenRef.current) { + const res = await encryptAPI({ body: { companyId } }); + if (res.success && res.data) { + setToken(res.data); } else { - // Info: (20241007 - tzuhan) 如果沒有提供 header,可以從 url 推斷出文件名 - fileName = certificateData.url.split('/').pop() || 'unknown'; + setToken(''); } - - // Info: (20241007 - tzuhan) 獲取文件大小 - const fileSize = blob.size; - - const imageObjectUrl = URL.createObjectURL(blob); - setUploadingCertificates((prev) => [ - ...prev, - { - url: imageObjectUrl, - status: ProgressStatus.IN_PROGRESS, - name: fileName, - size: fileSize, - progress: 80, - }, - ]); } - }; + }, [tokenRef, companyId, setToken]); + + const toggleQRCode = useCallback(() => { + getToken(); + setShowQRCode((prev) => !prev); + }, []); useEffect(() => { - const pusher = new Pusher(process.env.NEXT_PUBLIC_PUSHER_KEY!, { - cluster: '', - wsHost: process.env.NEXT_PUBLIC_PUSHER_HOST!, - wsPort: parseFloat(process.env.NEXT_PUBLIC_PUSHER_PORT!), - }); + getToken(); + }, [getToken]); - const channel: Channel = pusher.subscribe('certificate-channel'); + useEffect(() => { + const pusher = getPusherInstance(); + const channel = pusher.subscribe(PRIVATE_CHANNEL.CERTIFICATE); - channel.bind('certificate-event', certificateHandler); + channel.bind(CERTIFICATE_EVENT.UPLOAD, certificateHandler); return () => { - channel.unbind('certificate-event', certificateHandler); - pusher.unsubscribe('certificate-channel'); + channel.unbind(CERTIFICATE_EVENT.UPLOAD, certificateHandler); + pusher.unsubscribe(PRIVATE_CHANNEL.CERTIFICATE); }; - }, []); + }, [certificateHandler]); return ( <> @@ -251,7 +257,7 @@ const UploadCertificatePage: React.FC = () => { - Upload Certificate - iSunFA + Certificate List - iSunFA
{editingId && ( @@ -262,12 +268,12 @@ const UploadCertificatePage: React.FC = () => { onSave={handleSave} /> )} - {showQRCode && ( + {showQRCode && !!token && ( setShowQRCode((prev) => !prev)} isOnTopOfModal={false} - token={token} + token={tokenRef.current!} /> )} @@ -281,20 +287,7 @@ const UploadCertificatePage: React.FC = () => { {/* Info: (20240919 - tzuhan) Main Content */}
{/* Info: (20240919 - tzuhan) Upload Area */} - setShowQRCode((prev) => !prev)} - /> -
- {uploadingCertificates.map((image, index) => ( -
- {`Received -

Status: {image.status}

-
- ))} -
- + {/* Info: (20240919 - tzuhan) Tabs */} { />
{/* Info: (20240926- tzuhan) Floating Upload Popup */} - {uploadingCertificates.length > 0 && ( - - )} +
@@ -375,4 +368,4 @@ const getStaticPropsFunction = async ({ locale }: ILocale) => ({ export const getStaticProps = getStaticPropsFunction; -export default UploadCertificatePage; +export default CertificateListPage; From f05f5b7971044b88f8431d6d252a41d8b07de4b7 Mon Sep 17 00:00:00 2001 From: TzuHanLiang Date: Wed, 9 Oct 2024 17:43:06 +0800 Subject: [PATCH 5/7] feat: remove debug code --- package.json | 2 +- src/lib/utils/parse_image_form.ts | 28 +--------------------------- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 346bc828a..99d0a4032 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iSunFA", - "version": "0.8.2+38", + "version": "0.8.2+39", "private": false, "scripts": { "dev": "next dev", diff --git a/src/lib/utils/parse_image_form.ts b/src/lib/utils/parse_image_form.ts index 71af979e0..76bee46a0 100644 --- a/src/lib/utils/parse_image_form.ts +++ b/src/lib/utils/parse_image_form.ts @@ -1,4 +1,4 @@ -import formidable, { IncomingForm, Fields, Files } from 'formidable'; +import { IncomingForm, Fields, Files } from 'formidable'; import { promises as fs } from 'fs'; import path from 'path'; import { NextApiRequest } from 'next'; @@ -6,32 +6,12 @@ import { FORMIDABLE_OPTIONS } from '@/constants/config'; import { FileFolder, getFileFolder } from '@/constants/file'; import loggerBack from '@/lib/utils/logger_back'; -export const extractKeyAndIvFromFields = (fields: formidable.Fields) => { - const { encryptedSymmetricKey, iv } = fields; - const keyStr = - encryptedSymmetricKey && encryptedSymmetricKey.length ? encryptedSymmetricKey[0] : ''; - const ivStr = iv ? iv[0] : ''; - - const ivUnit8: Uint8Array = new Uint8Array(ivStr.split(',').map((num) => parseInt(num, 10))); - - const isEncrypted = !!(keyStr && ivUnit8.length > 0); - - return { - isEncrypted, - encryptedSymmetricKey: keyStr, - iv: ivUnit8, - }; -}; - export const parseForm = async ( req: NextApiRequest, subDir: FileFolder = FileFolder.TMP, // Info: (20240726 - Jacky) 預設子資料夾名稱為tmp subSubDir?: string // Info: (202410008 - Tzuhan) 如果有傳入subSubDir,則使用subSubDir ) => { let uploadDir = getFileFolder(subDir); - // Deprecated: (20241011-tzuhan) Debugging purpose - // eslint-disable-next-line no-console - console.log(`parseForm (subDir: ${subDir}, subSubDir: ${subSubDir}), req`, req); // Info: (202410008 - Tzuhan) 如果有傳入subSubDir,更新 uploadDir if (subSubDir) { @@ -48,14 +28,8 @@ export const parseForm = async ( const parsePromise = new Promise<{ fields: Fields; files: Files }>((resolve, reject) => { form.parse(req, (err, fields, files) => { if (err) { - // Deprecated: (20241011-tzuhan) Debugging purpose - // eslint-disable-next-line no-console - console.error(`form.parse err:`, err); reject(err); } else { - // Deprecated: (20241011-tzuhan) Debugging purpose - // eslint-disable-next-line no-console - console.log(`form.parse fields, files:`, fields, files); resolve({ fields, files }); } }); From 03d3380b1f6be40fc5f8e8ed5682db18647dd9b6 Mon Sep 17 00:00:00 2001 From: Luphia Chang Date: Wed, 9 Oct 2024 19:09:47 +0800 Subject: [PATCH 6/7] Update upload.ts --- src/pages/api/v2/upload.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/api/v2/upload.ts b/src/pages/api/v2/upload.ts index 81ea99294..33948e00d 100644 --- a/src/pages/api/v2/upload.ts +++ b/src/pages/api/v2/upload.ts @@ -34,6 +34,7 @@ async function handlePostRequest(req: NextApiRequest) { const parsedForm = await parseForm(req, UPLOAD_TYPE_TO_FOLDER_MAP[type], token as string); // TODO: (20241011 - tzuhan) Handle file upload logic here, save to DB + // Deprecated: (20241011-tzuhan) Debugging purpose // eslint-disable-next-line no-console console.log(`API POST companyId(${companyId}) parsedForm: `, parsedForm); @@ -69,7 +70,7 @@ export default async function handler( ({ statusMessage, payload } = await handleRequest(req, res)); } else { statusMessage = STATUS_MESSAGE.METHOD_NOT_ALLOWED; - + // Deprecated: (20241011-tzuhan) Debugging purpose // eslint-disable-next-line no-console console.error('Failed to send certificates update via Pusher', `METHOD_NOT_ALLOWED`); } From e4ebee371e789a812050c8c236a26f98dd1bd2dc Mon Sep 17 00:00:00 2001 From: Luphia Chang Date: Wed, 9 Oct 2024 19:11:12 +0800 Subject: [PATCH 7/7] Update certificate_list.tsx --- src/pages/users/accounting/certificate_list.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/users/accounting/certificate_list.tsx b/src/pages/users/accounting/certificate_list.tsx index 74593713a..1e211fb41 100644 --- a/src/pages/users/accounting/certificate_list.tsx +++ b/src/pages/users/accounting/certificate_list.tsx @@ -48,6 +48,7 @@ const CertificateListPage: React.FC = () => { 0: false, 1: false, }); + // Deprecated: (20241011-tzuhan) Debugging purpose // eslint-disable-next-line @typescript-eslint/no-unused-vars const [uploadingCertificates, setUploadingCertificates, uploadingCertificatesRef] = useStateRef<{ [id: number]: ICertificateInfo;