Skip to content

Commit

Permalink
Merge branch 'develop' into feature/ui-rolefunctionpreview
Browse files Browse the repository at this point in the history
  • Loading branch information
Luphia authored Oct 9, 2024
2 parents 1948818 + 6ef1cbf commit a1f265a
Show file tree
Hide file tree
Showing 27 changed files with 818 additions and 84 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 1 addition & 4 deletions src/components/certificate/certificate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,7 +20,6 @@ interface CertificateProps {
const Certificate: React.FC<CertificateProps> = ({
data,
viewType,
activeTab,
activeSelection,
handleSelect,
isSelectedAll,
Expand All @@ -48,7 +46,6 @@ const Certificate: React.FC<CertificateProps> = ({
{viewType === VIEW_TYPES.GRID && (
<CertificateGrid
data={data.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage)}
activeTab={activeTab}
activeSelection={activeSelection}
handleSelect={handleSelect}
onDownload={onDownload}
Expand Down
31 changes: 12 additions & 19 deletions src/components/certificate/certificate_grid.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import React from 'react';
import { ICertificateUI } from '@/interfaces/certificate';
import CertificateThumbnail from '@/components/certificate/certificate_thumbnail';
import FloatingUploadPopup from '@/components/floating_upload_popup/floating_upload_popup';

interface CertificateGridProps {
data: ICertificateUI[]; // Info: (20240923 - tzuhan) 項目列表
activeSelection: boolean; // Info: (20240923 - tzuhan) 是否處於選擇狀態
activeTab: number;
handleSelect: (ids: number[], isSelected: boolean) => void;
onRemove: (id: number) => void;
onDownload: (id: number) => void;
Expand All @@ -16,29 +14,24 @@ interface CertificateGridProps {
const CertificateGrid: React.FC<CertificateGridProps> = ({
data,
activeSelection,
activeTab,
handleSelect,
onRemove,
onDownload,
onEdit,
}) => {
return (
<>
<div className="grid grid-cols-dynamic-fit place-items-center gap-4">
{data.map((certificate) => (
<CertificateThumbnail
data={certificate}
activeSelection={activeSelection}
handleSelect={handleSelect}
onRemove={onRemove}
onDownload={onDownload}
onEdit={onEdit}
/>
))}
</div>
{/* Info: (20240926- tzuhan) Floating Upload Popup */}
{activeTab === 0 && <FloatingUploadPopup />}
</>
<div className="grid grid-cols-dynamic-fit place-items-center gap-4">
{data.map((certificate) => (
<CertificateThumbnail
data={certificate}
activeSelection={activeSelection}
handleSelect={handleSelect}
onRemove={onRemove}
onDownload={onDownload}
onEdit={onEdit}
/>
))}
</div>
);
};

Expand Down
80 changes: 80 additions & 0 deletions src/components/certificate/certificate_qrcode_modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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<CertificateQRCodeModalProps> = ({
isOpen,
isOnTopOfModal = false,
token,
onClose,
}) => {
if (!isOpen) return null;
const { Canvas } = useQRCode();
const isDev = process.env.NODE_ENV === 'development';

return (
<div
className={`fixed inset-0 z-70 flex items-center justify-center ${isOnTopOfModal ? '' : 'bg-black/50'}`}
>
<div className="relative flex max-h-90vh flex-col rounded-sm bg-surface-neutral-surface-lv2 md:max-h-100vh">
{/* Info: (20240924 - tzuhan) 關閉按鈕 */}
<button
type="button"
className="absolute right-4 top-4 text-checkbox-text-primary"
onClick={onClose}
>
<RxCross1 size={32} />
</button>
{/* Info: (20240924 - tzuhan) 模態框標題 */}
<h2 className="flex flex-col items-center justify-center gap-2 border-b border-stroke-neutral-quaternary p-2 text-xl font-semibold text-card-text-title">
<div className="text-xl font-semibold">Url</div>
<div className="text-xs font-normal text-card-text-sub">for mobile upload</div>
</h2>
<div className="mx-20 my-10 flex flex-col items-center">
{/* Info: (20240924 - tzuhan) 發票縮略圖 */}
<Canvas
text={`${isDev ? 'http://192.168.2.29:3000' : DOMAIN}/${ISUNFA_ROUTE.UPLOAD}?token=${token}`}
options={{
errorCorrectionLevel: 'M',
margin: 3,
scale: 4,
width: 300,
color: {
dark: '#304872',
light: '#fff',
},
}}
/>
<a
className="mt-2 text-center text-xs text-card-text-sub"
href={`${isDev ? 'http://localhost:3000' : DOMAIN}/${ISUNFA_ROUTE.UPLOAD}?token=${token}`}
target="_blank"
rel="noreferrer"
>{`${isDev ? 'http://localhost:3000' : DOMAIN}/${ISUNFA_ROUTE.UPLOAD}?token=${token}`}</a>
</div>
<div className="flex justify-end gap-2 border-t border-stroke-neutral-quaternary px-4 py-3">
<Button
type="button"
variant="tertiaryOutlineGrey"
className="p-2 px-4"
onClick={onClose}
>
<div className="flex items-end gap-2">Close</div>
</Button>
</div>
</div>
</div>
);
};

export default CertificateQRCodeModal;
4 changes: 3 additions & 1 deletion src/components/filter_section/filter_section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@ const FilterSection: React.FC<FilterSectionProps> = ({

// Info: (20240919 - tzuhan) 每次狀態變更時,組合查詢條件並發送 API 請求
useEffect(() => {
fetchData();
if (typeof window !== 'undefined') {
fetchData();
}
}, [selectedType, selectedStatus, selectedDateRange, searchQuery, selectedSorting, sorting]);

return (
Expand Down
55 changes: 36 additions & 19 deletions src/components/floating_upload_popup/floating_upload_popup.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
import React, { useState } from 'react';
import UploadFileItem, { UploadFile } from '@/components/upload_certificate/upload_file_item';
import React, { useState, useMemo, useEffect } from 'react';
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<UploadFile[]>([
{ 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) 控制展開/收縮狀態
interface FloatingUploadPopupProps {
uploadingCertificates: ICertificateInfo[];
}

// Info: (20240919 - tzuhan) 計算總上傳進度和狀態
const totalFiles = files.length;
const completedFiles = files.filter((file) => file.progress === 100).length;
const isUploading = files.some(
(file) => file.progress > 0 && file.progress < 100 && file.status !== ProgressStatus.PAUSED
);
const FloatingUploadPopup: React.FC<FloatingUploadPopupProps> = ({ uploadingCertificates }) => {
const [files, setFiles] = useState<ICertificateInfo[]>(uploadingCertificates);
const [expanded, setExpanded] = useState(false);

// Info: (20241009 - tzuhan) Memoize calculated values to avoid redundant recalculations
const totalFiles = useMemo(() => uploadingCertificates.length, [files]);
const completedFiles = useMemo(
() => uploadingCertificates.filter((file) => file.status === ProgressStatus.SUCCESS).length,
[files]
);
const isUploading = useMemo(
() =>
files.some(
(file) => file.progress > 0 && file.progress < 100 && file.status !== ProgressStatus.PAUSED
),
[files]
);
// Info: (20240919 - tzuhan) 暫停或繼續上傳
const updateFileStatus = (prevFiles: UploadFile[], index: number) =>
const updateFileStatus = (prevFiles: ICertificateInfo[], index: number) =>
prevFiles.map((file, i) => {
return i === index
? {
Expand All @@ -42,13 +49,21 @@ const FloatingUploadPopup: React.FC = () => {
setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index));
};

return (
const displayed =
totalFiles > 0 ||
uploadingCertificates.filter((file) => file.status === ProgressStatus.IN_PROGRESS).length > 0;

useEffect(() => {
setFiles(uploadingCertificates);
}, [uploadingCertificates]);

const popUpBody = displayed ? (
<div className="dashboardCardShadow fixed bottom-4 right-4 w-480px overflow-hidden">
{/* Info: (20240919 - tzuhan) Header: 顯示標題與收縮/展開按鈕 */}
<div className="flex items-center justify-between p-4">
<div className="flex-auto flex-col items-center text-center">
<div className="flex items-center justify-center space-x-2 text-lg font-semibold">
<Image src="/elements/cloud_upload.svg" width={24} height={24} alt="clock" />
<Image src="/elements/cloud_upload.svg" width={24} height={24} alt="Upload icon" />
<div>Upload file</div>
</div>
{totalFiles > 0 && (
Expand Down Expand Up @@ -88,7 +103,9 @@ const FloatingUploadPopup: React.FC = () => {
</div>
)}
</div>
);
) : null;

return popUpBody;
};

export default FloatingUploadPopup;
10 changes: 7 additions & 3 deletions src/components/upload_area/upload_area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import Image from 'next/image';
interface UploadAreaProps {
isDisabled: boolean;
withScanner: boolean;
toggleQRCode?: () => void;
}

const UploadArea: React.FC<UploadAreaProps> = ({ isDisabled, withScanner }) => {
const UploadArea: React.FC<UploadAreaProps> = ({ isDisabled, withScanner, toggleQRCode }) => {
const { t } = useTranslation(['common', 'journal']);
const [isDragOver, setIsDragOver] = useState<boolean>(false);

Expand Down Expand Up @@ -81,7 +82,7 @@ const UploadArea: React.FC<UploadAreaProps> = ({ isDisabled, withScanner }) => {
/>
</button>

{withScanner && (
{withScanner && toggleQRCode && (
<>
<h3 className="px-4 text-xl font-bold text-text-neutral-tertiary">
{t('common:COMMON.OR')}
Expand Down Expand Up @@ -109,7 +110,10 @@ const UploadArea: React.FC<UploadAreaProps> = ({ isDisabled, withScanner }) => {
/>
<p className="font-semibold text-drag-n-drop-text-primary group-disabled:text-drag-n-drop-text-disable">
{t('journal:JOURNAL.USE_YOUR_PHONE_AS')}
<span className="cursor-pointer text-text-brand-primary-lv2 group-disabled:cursor-not-allowed group-disabled:text-drag-n-drop-text-disable">
<span
className="cursor-pointer text-text-brand-primary-lv2 group-disabled:cursor-not-allowed group-disabled:text-drag-n-drop-text-disable"
onClick={toggleQRCode}
>
{t('journal:JOURNAL.SCANNER')}
</span>
</p>
Expand Down
1 change: 1 addition & 0 deletions src/constants/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Info: (20240416 - Murky) type
export enum ProgressStatus {
SUCCESS = 'success',
FAILED = 'failed',
IN_PROGRESS = 'inProgress',
NOT_FOUND = 'notFound',
ALREADY_UPLOAD = 'alreadyUpload',
Expand Down
28 changes: 28 additions & 0 deletions src/constants/api_connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export enum APIName {
STATUS_INFO_GET = 'STATUS_INFO_GET',
ACCOUNT_LIST = 'ACCOUNT_LIST',
FILE_UPLOAD = 'FILE_UPLOAD',
PUBLIC_FILE_UPLOAD = 'PUBLIC_FILE_UPLOAD',
FILE_DELETE = 'FILE_DELETE',
FILE_GET = 'FILE_GET',
COMPANY_GET_BY_ID = 'COMPANY_GET_BY_ID',
Expand All @@ -102,6 +103,9 @@ export enum APIName {
PUBLIC_KEY_GET = 'PUBLIC_KEY_GET',
ZOD_EXAMPLE = 'ZOD_EXAMPLE', // Info: (20240909 - Murky) This is a Zod example, to demonstrate how to use Zod schema to validate data.
CERTIFICATE_LIST = 'CERTIFICATE_LIST',
PUSHER = 'PUSHER',
ENCRYPT = 'ENCRYPT',
DECRYPT = 'DECRYPT',
}

export enum APIPath {
Expand Down Expand Up @@ -162,6 +166,7 @@ export enum APIPath {
STATUS_INFO_GET = `${apiPrefix}/status_info`,
ACCOUNT_LIST = `${apiPrefix}/company/:companyId/account`,
FILE_UPLOAD = `${apiPrefix}/company/:companyId/file`,
PUBLIC_FILE_UPLOAD = `${apiPrefixV2}/upload`,
FILE_DELETE = `${apiPrefix}/company/:companyId/file/:fileId`,
FILE_GET = `${apiPrefix}/company/:companyId/file/:fileId`,
COMPANY_GET_BY_ID = `${apiPrefix}/company/:companyId`,
Expand All @@ -183,6 +188,9 @@ export enum APIPath {
PUBLIC_KEY_GET = `${apiPrefix}/company/:companyId/public_key`,
ZOD_EXAMPLE = `${apiPrefix}/company/zod`, // Info: (20240909 - Murky) This is a Zod example, to demonstrate how to use Zod schema to validate data.
CERTIFICATE_LIST = `${apiPrefix}/company/:companyId/certificate`,
PUSHER = `${apiPrefixV2}/pusher`,
ENCRYPT = `${apiPrefixV2}/encrypt`,
DECRYPT = `${apiPrefixV2}/decrypt`,
}
const createConfig = ({
name,
Expand Down Expand Up @@ -424,6 +432,11 @@ export const APIConfig: Record<IAPIName, IAPIConfig> = {
method: HttpMethod.POST,
path: APIPath.FILE_UPLOAD,
}),
[APIName.PUBLIC_FILE_UPLOAD]: createConfig({
name: APIName.PUBLIC_FILE_UPLOAD,
method: HttpMethod.POST,
path: APIPath.PUBLIC_FILE_UPLOAD,
}),
[APIName.FILE_DELETE]: createConfig({
name: APIName.FILE_DELETE,
method: HttpMethod.DELETE,
Expand Down Expand Up @@ -524,6 +537,21 @@ export const APIConfig: Record<IAPIName, IAPIConfig> = {
method: HttpMethod.GET,
path: APIPath.CERTIFICATE_LIST,
}),
[APIName.PUSHER]: createConfig({
name: APIName.PUSHER,
method: HttpMethod.POST,
path: APIPath.PUSHER,
}),
[APIName.ENCRYPT]: createConfig({
name: APIName.ENCRYPT,
method: HttpMethod.POST,
path: APIPath.ENCRYPT,
}),
[APIName.DECRYPT]: createConfig({
name: APIName.DECRYPT,
method: HttpMethod.POST,
path: APIPath.DECRYPT,
}),
[APIName.VOUCHER_GET_BY_ID_V2]: createConfig({
name: APIName.VOUCHER_GET_BY_ID_V2,
method: HttpMethod.GET,
Expand Down
Loading

0 comments on commit a1f265a

Please sign in to comment.