完善后台页面

This commit is contained in:
LittleBoy 2024-05-19 23:22:54 +08:00
parent 7d022bc4fa
commit 44095afbc7
17 changed files with 339 additions and 146 deletions

View File

@ -18,7 +18,6 @@
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@mui/system": "^5.15.15",
"@types/file-saver": "^2.0.7",
"ahooks": "^3.7.11",
"clsx": "^2.1.1",
"i18next": "^23.11.4",
@ -26,11 +25,9 @@
"jspdf-autotable": "^3.8.2",
"less": "^4.2.0",
"react": "^18.2.0",
"react-countup": "^6.5.3",
"react-dom": "^18.2.0",
"react-i18next": "^14.1.1",
"react-router-dom": "^6.23.1",
"zustand": "^4.5.2"
"react-router-dom": "^6.23.1"
},
"devDependencies": {
"@types/lodash": "^4.17.1",
@ -43,7 +40,6 @@
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"file-saver": "^2.0.5",
"typescript": "^5.2.2",
"vite": "^5.2.0"
}

View File

@ -17,27 +17,62 @@
outline: none;
font-family: -apple-system, "PingFang SC", 'Microsoft YaHei', sans-serif;
}
.cursor-pointer {
cursor: pointer;
}
.semi-layout{
.semi-layout {
min-height: 100vh;
}
.space-between{
.space-between {
justify-content: space-between;
}
.align-center{
.align-center {
display: flex;
align-items: center;
}
.page-content-container{
.page-content-container {
max-width: 90%;
width: 1400px;
margin: auto;
}
.auth-container{
.auth-container {
background: url(./images/auth/bg.png);
}
.semi-dropdown-item-active{
.semi-dropdown-item-active {
background-color: var(--semi-color-default-active);
}
.dashboard-menu-container {
padding: 15px;
.nav-item {
padding: 15px;
display: flex;
align-items: center;
text-decoration: none;
border-radius: 10px;
color: #999999;
.svg-icon{
font-size: 22px;
}
.menu-text{
margin-left: 10px;
font-size: 14px;
}
&.active {
background-color: #fff;
.svg-icon{
color: #FFC65F;
}
.menu-text{
color: #2D3748;
}
}
}
}

View File

@ -0,0 +1,108 @@
import React from "react";
export const IconQRCode = ({style}: { style?: React.CSSProperties }) => (
<svg className={'svg-icon'} style={style} xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1"
width="1em" height="1em" viewBox="0 0 15 15">
<defs>
<clipPath id="master_svg0_1_3654/1_0549">
<rect x="0" y="0" width="15" height="15" rx="6"/>
</clipPath>
</defs>
<g clip-path="url(#master_svg0_1_3654/1_0549)">
<g>
<path
d="M1.40625,1.875C1.40625,1.616118,1.616118,1.40625,1.875,1.40625L5.625,1.40625C5.88388,1.40625,6.09375,1.616118,6.09375,1.875L6.09375,5.625C6.09375,5.88388,5.88388,6.09375,5.625,6.09375L1.875,6.09375C1.616118,6.09375,1.40625,5.88388,1.40625,5.625L1.40625,1.875ZM2.8125,4.6875L4.6875,4.6875L4.6875,2.8125L2.8125,2.8125L2.8125,4.6875Z"
fill="currentColor"/>
</g>
<g>
<path
d="M1.40631103515625,9.37506103515625L1.40631103515625,13.125001035156249C1.40631103515625,13.38388103515625,1.61618103515625,13.593751035156249,1.87506203515625,13.593751035156249L5.62500103515625,13.593751035156249C5.88388103515625,13.593751035156249,6.09375103515625,13.38388103515625,6.09375103515625,13.125001035156249L6.09375103515625,9.37506103515625C6.09375103515625,9.11617903515625,5.88388103515625,8.90631103515625,5.62500103515625,8.90631103515625L1.87506203515625,8.90631103515625C1.61618103515625,8.90631103515625,1.40631103515625,9.11617903515625,1.40631103515625,9.37506103515625ZM2.81256103515625,12.18750103515625L2.81256103515625,10.31256103515625L4.6875010351562505,10.31256103515625L4.6875010351562505,12.18750103515625L2.81256103515625,12.18750103515625Z"
fill="currentColor"/>
</g>
<g>
<path
d="M9.375,1.40625C9.116119,1.40625,8.90625,1.616118,8.90625,1.875L8.90625,5.625C8.90625,5.88388,9.116119,6.09375,9.375,6.09375L13.125,6.09375C13.383880000000001,6.09375,13.59375,5.88388,13.59375,5.625L13.59375,1.875C13.59375,1.616118,13.383880000000001,1.40625,13.125,1.40625L9.375,1.40625ZM10.3125,2.8125L12.1875,2.8125L12.1875,4.6875L10.3125,4.6875L10.3125,2.8125Z"
fill="currentColor"/>
</g>
<g>
<path
d="M1.40625,7.03131103515625L13.59375,7.03131103515625L13.59375,7.96881103515625L1.40625,7.96881103515625L1.40625,7.03131103515625Z"
fill="currentColor"/>
</g>
<g>
<path d="M7.03125,1.40625L7.03125,6.09375L7.96875,6.09375L7.96875,1.40625L7.03125,1.40625Z"
fill="currentColor"/>
</g>
<g>
<path
d="M7.03125,13.593782109374999L7.03125,8.909912109375L7.96875,8.909912109375L7.96875,13.593782109374999L7.03125,13.593782109374999Z"
fill="currentColor"/>
</g>
<g>
<path
d="M8.902130126953125,11.718782109375L11.718760126953125,11.718782109375L11.718760126953125,10.312532109375L10.308390126953125,10.312532109375L10.308410126953126,8.909927308575L8.902158737153124,8.909912109375L8.902130126953125,11.718782109375Z"
fill="currentColor"/>
</g>
<g>
<path
d="M8.90625,12.65622L12.6537,12.65622L12.653690000000001,8.9062643051L13.591190000000001,8.90625L13.5912,12.937470000000001C13.5912,13.299900000000001,13.29738,13.593720000000001,12.93495,13.593720000000001L8.90625,13.593720000000001L8.90625,12.65622Z"
fill="currentColor"/>
</g>
</g>
</svg>
)
export const IconQuery = ({style}: { style?: React.CSSProperties }) => (
<svg className={'svg-icon'} style={style} xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1"
width="1em" height="1em" viewBox="0 0 15 15">
<defs>
<clipPath id="master_svg0_1_3654/1_0572">
<rect x="0" y="0" width="15" height="15" rx="0"/>
</clipPath>
</defs>
<g clip-path="url(#master_svg0_1_3654/1_0572)">
<g>
<path
d="M8.4375,8.20313Q8.4375,8.2607,8.43186,8.31799Q8.426210000000001,8.37528,8.41498,8.431750000000001Q8.40375,8.48821,8.387039999999999,8.5433Q8.37033,8.59839,8.3483,8.65158Q8.32626,8.70477,8.29913,8.75554Q8.27199,8.80632,8.24,8.85418Q8.20802,8.90205,8.1715,8.94655Q8.13497,8.991060000000001,8.09427,9.03177Q8.053560000000001,9.07247,8.00905,9.109Q7.96455,9.14552,7.9166799999999995,9.1775Q7.8688199999999995,9.20949,7.81804,9.23663Q7.76727,9.26376,7.71408,9.2858Q7.66089,9.30783,7.6058,9.324539999999999Q7.5507100000000005,9.34125,7.49425,9.35248Q7.43778,9.363710000000001,7.38049,9.36936Q7.3232,9.375,7.26563,9.375Q7.20805,9.375,7.15076,9.36936Q7.093468,9.363710000000001,7.037003,9.35248Q6.980539,9.34125,6.925448,9.324539999999999Q6.870356,9.30783,6.817168,9.2858Q6.76398,9.26376,6.713207,9.23663Q6.662434,9.20949,6.614566,9.1775Q6.566698,9.14552,6.522195,9.109Q6.477693,9.07247,6.436984,9.03177Q6.396276,8.991060000000001,6.3597529999999995,8.94655Q6.323231,8.90205,6.291247,8.85418Q6.259262,8.80632,6.232123,8.75554Q6.204985,8.70477,6.1829537,8.65158Q6.1609224,8.59839,6.1442105,8.5433Q6.1274987,8.48821,6.1162672,8.431750000000001Q6.1050358,8.37528,6.0993929,8.31799Q6.09375,8.2607,6.09375,8.20313Q6.09375,8.14555,6.0993929,8.08826Q6.1050358,8.030968,6.1162672,7.974503Q6.1274987,7.918039,6.1442105,7.862948Q6.1609224,7.807856,6.1829537,7.754668Q6.204985,7.70148,6.232123,7.650707Q6.259262,7.599934,6.291247,7.552066Q6.323231,7.504198,6.3597529999999995,7.459695Q6.396276,7.415193,6.436984,7.374484Q6.477693,7.333776,6.522195,7.2972529999999995Q6.566698,7.260731,6.614566,7.228747Q6.662434,7.196762,6.713207,7.169623Q6.76398,7.142485,6.817168,7.1204537Q6.870356,7.0984224,6.925448,7.0817105Q6.980539,7.0649987,7.037003,7.0537672Q7.093468,7.0425358,7.15076,7.0368929Q7.20805,7.03125,7.26563,7.03125Q7.3232,7.03125,7.38049,7.0368929Q7.43778,7.0425358,7.49425,7.0537672Q7.5507100000000005,7.0649987,7.6058,7.0817105Q7.66089,7.0984224,7.71408,7.1204537Q7.76727,7.142485,7.81804,7.169623Q7.8688199999999995,7.196762,7.9166799999999995,7.228747Q7.96455,7.260731,8.00905,7.2972529999999995Q8.053560000000001,7.333776,8.09427,7.374484Q8.13497,7.415193,8.1715,7.459695Q8.20802,7.504198,8.24,7.552066Q8.27199,7.599934,8.29913,7.650707Q8.32626,7.70148,8.3483,7.754668Q8.37033,7.807856,8.387039999999999,7.862948Q8.40375,7.918039,8.41498,7.974503Q8.426210000000001,8.030968,8.43186,8.08826Q8.4375,8.14555,8.4375,8.20313Z"
fill="currentColor" fill-opacity="1"/>
</g>
<g>
<path
d="M3.28125,0.9375C2.5046049999999997,0.9375,1.875,1.567105,1.875,2.34375L1.875,12.6563C1.875,13.4329,2.5046049999999997,14.0625,3.28125,14.0625L11.71875,14.0625C12.4954,14.0625,13.125,13.4329,13.125,12.6563L13.125,5.15622L10.3125,5.15622C9.53585,5.15622,8.90625,4.526619999999999,8.90625,3.74997L8.90625,0.9375L3.28125,0.9375ZM9.375,8.20312C9.375,8.61589,9.25644,9.00097,9.05152,9.32616L10.32722,10.60187L9.66431,11.2648L8.38859,9.98906C8.06342,10.19396,7.67836,10.3125,7.26562,10.3125C6.10065,10.3125,5.15625,9.3681,5.15625,8.20312C5.15625,7.03815,6.10065,6.09375,7.26562,6.09375C8.4306,6.09375,9.375,7.03815,9.375,8.20312Z"
fill="currentColor" fill-opacity="1"/>
</g>
<g>
<path
d="M12.84958,3.55950103515625C13.02483,3.73431103515625,13.1238,3.97131103515625,13.12499,4.21872103515625L10.3125,4.21872103515625C10.053619,4.21872103515625,9.84375,4.008851035156249,9.84375,3.74997103515625L9.84375,0.93756103515625C10.088203,0.94027833515625,10.322109,1.03840803515625,10.495386,1.21125403515625L12.84958,3.55950103515625Z"
fill="currentColor" fill-opacity="1"/>
</g>
</g>
</svg>
)
export const IconReconciliation = ({style}: { style?: React.CSSProperties }) => (
<svg className={'svg-icon'} style={style} xmlns="http://www.w3.org/2000/svg" fill="none" version="1.1"
width="1em" height="1em" viewBox="0 0 15 15">
<defs>
<clipPath id="master_svg0_1_3654/1_0566">
<rect x="0" y="0" width="15" height="15" rx="6"/>
</clipPath>
</defs>
<g clip-path="url(#master_svg0_1_3654/1_0566)">
<g>
<path
d="M2.81335,5.56129C2.8471,4.29633,3.88314,3.28125,5.15625,3.28125C5.34382,3.28125,5.52626,3.30328,5.70108,3.34491C6.28865,2.459048,7.29486,1.875,8.4375,1.875C10.04117,1.875,11.3761,3.0254399999999997,11.6621,4.54604C13.0745,5.15864,14.0625,6.5655,14.0625,8.20312C14.0625,9.294170000000001,13.624,10.28278,12.9136,11.00232C13.1438,10.2376,12.5883,9.3752,11.6976,9.3752L11.247,9.3752L11.247,7.03119C11.247,6.51343,10.82728,6.09369,10.30951,6.09369L9.37201,6.09369C9.13329,6.09369,8.91541,6.18292,8.74989,6.32984C8.19456,5.96552,7.39427,5.99146,6.88692,6.60201L5.15099,8.69101C4.46064,9.52176,5.05144,10.78088,6.1316,10.78087L6.56222,10.78087L6.56222,12.1875L4.45312,12.1875C2.5115,12.1875,0.9375,10.6135,0.9375,8.67188C0.9375,7.32245,1.697769,6.1506,2.81335,5.56129Z"
fill="currentColor" fill-opacity="1"/>
</g>
<g>
<path
d="M10.20127127734375,12.95489896484375C9.92087127734375,13.29233896484375,9.37200927734375,13.09405896484375,9.37200927734375,12.65530896484375L9.37200927734375,7.03118896484375L10.30950927734375,7.03118896484375L10.30950927734375,10.31269896484375L11.69760927734375,10.31269896484375C11.975589277343751,10.31269896484375,12.12763927734375,10.63672896484375,11.949969277343751,10.85053896484375L10.20127127734375,12.95489896484375Z"
fill="currentColor" fill-opacity="1"/>
</g>
<g>
<path
d="M8.43601703125,7.46545189453125C8.43681703125,7.47703889453125,8.43721703125,7.48880089453125,8.43721703125,7.50075389453125L8.43721703125,13.124936894531249L7.49971703125,13.124936894531249L7.49971703125,9.84335689453125L6.13159703125,9.84335689453125C5.84567423125,9.84335689453125,5.68928803125,9.51007689453125,5.87202693125,9.29016689453125L7.60795703125,7.20116489453125C7.87685703125,6.87756589453125,8.39263703125,7.04663799453125,8.43450703125,7.44799089453125C8.43510703125,7.45376189453125,8.43560703125,7.45957789453125,8.43601703125,7.46545189453125Z"
fill="currentColor" fill-opacity="1"/>
</g>
</g>
</svg>
)

View File

@ -47,7 +47,12 @@ export const AuthProvider = ({children}: { children: React.ReactNode }) => {
dispatch({
payload: {
isInitialized: true,
isLoggedIn: true
isLoggedIn: true,
user: {
id: 1,
nickname: 'test-user',
department: 'root',
}
}
})
}, 300)
@ -62,7 +67,7 @@ export const AuthProvider = ({children}: { children: React.ReactNode }) => {
user: {
id: 1,
nickname: 'test-user',
department: 'ro',
department: 'root',
}
}
})

View File

@ -30,6 +30,7 @@ export const I18nSwitcher = () => {
<Dropdown.Item
active={locale == it.key} key={it.key}
onClick={() => changeLocale(it.key)}
icon={<span>{it.key.split('-')[1].toUpperCase()}</span>}
>{it.text}</Dropdown.Item>
))}
</Dropdown.Menu>
@ -37,7 +38,8 @@ export const I18nSwitcher = () => {
>
<Button theme="borderless">
<Space align={'center'} spacing={2} style={{marginTop:2}}>
<IconLanguage/> {LocaleList.find(s => s.key == locale)?.text}
<IconLanguage style={{ color: '#666' }} />
{/* {LocaleList.find(s => s.key == locale)?.text}*/}
</Space>
</Button>
</Dropdown>

View File

@ -4,7 +4,12 @@
"go_home": "Go Home"
},
"layout": {
"logout": "Logout"
"logout": "Logout",
"menu": {
"bill": "Bill Query",
"check": "Reconciliation",
"manual": "Manual Pay"
}
},
"login": {
"submit": "LOGIN WITH SSO",

View File

@ -4,7 +4,12 @@
"go_home": "回到首页"
},
"layout": {
"logout": "注销登录"
"logout": "注销登录",
"menu": {
"bill": "账单查询",
"check": "对账",
"manual": "现场支付"
}
},
"login": {
"submit": "使用SSO登录",

View File

@ -4,7 +4,12 @@
"go_home": "回到首页"
},
"layout": {
"logout": "注销登录"
"logout": "注销登录",
"menu": {
"bill": "账单查询",
"check": "对账",
"manual": "现场支付"
}
},
"login": {
"submit": "使用SSO登入",

View File

@ -4,6 +4,7 @@ import useConfig from "@/hooks/useConfig.ts";
import styled from "@emotion/styled";
import AppLogo from "@/assets/AppLogo";
import {useNavigate} from "react-router-dom";
import useAuth from "@/hooks/useAuth.ts";
const LoginContainer = styled.div({
@ -22,12 +23,15 @@ const SubmitContainer = styled.div({
margin: '70px auto 20px'
})
const AuthLogin = () => {
const { login } = useAuth();
const navigate = useNavigate();
const {t} = useTranslation();
const {appName} = useConfig()
const showDashboard = ()=> {
navigate('/dashboard')
login().then(()=>{
navigate('/dashboard')
})
}
return (<LoginContainer>

7
src/pages/bill/query.tsx Normal file
View File

@ -0,0 +1,7 @@
const BillQuery = () => {
return (<div>
<h1>BillQuery</h1>
</div>)
}
export default BillQuery

View File

@ -0,0 +1,7 @@
const BillReconciliation = () => {
return (<div>
<h1>BillQuery</h1>
</div>)
}
export default BillReconciliation

View File

@ -1,99 +1,10 @@
import {Button} from "@douyinfe/semi-ui";
import JsPDF from "jspdf";
import autoTable from 'jspdf-autotable'
import {useEffect, useState} from "react";
import {saveAs} from "file-saver";
import {GeneratePdf} from "@/service/generate-pdf.ts";
function drawItem(doc: JsPDF, item: {
title: string;
content?: string
}, y: number, align: 'left' | 'right' = 'left', fontSize: number = 14) {
doc.setFontSize(fontSize);
// const width = doc.internal.pageSize.getWidth();
doc.text(item.title, align == 'left' ? 20 : 180, y);
if(item.content && item.content.length > 0) doc.text(item.content, align == 'left' ? 65 : 230, y);
}
export default function Index() {
// doc.save('a4.pdf');
const [viewUrl, setViewUrl] = useState('');
useEffect(() => {
const doc = new JsPDF({
orientation: 'landscape',
format:'a4'
});
console.log(doc.getFontList())
// const width = doc.internal.pageSize.getWidth();
// const height = doc.internal.pageSize.getHeight();
doc.setFont('Helvetica','normal','bold');
doc.setFontSize(20);
doc.text('ACKNOWLEDGEMENT RECEIPT', 100, 20, {});
doc.setFont('Helvetica','normal','normal');
drawItem(doc, {title: 'Student Name:', content: 'BAI ZHONGHONG'}, 40)
drawItem(doc, {title: 'Reference Number:', content: '202402655'}, 40, "right")
drawItem(doc, {title: 'Student Number:', content: '236171275'}, 48)
drawItem(doc, {title: 'Print Date:', content: '2024-05-19'}, 48, "right")
drawItem(doc, {
title: 'Programme:',
content: 'MASTER OF SOCIAL SCIENCES IN INTERNATIONAL RELATIONS FOR \nBELT AND ROAD COUNTRIES'
}, 56)
drawItem(doc, {title: 'Mode of Study:', content: 'FULL-TIME'}, 70)
// draw table
autoTable(doc, {
startY: 80,
theme: 'grid',
margin: { top: 37,left:20,right:20 },
styles: {
fontSize: 13,
fillColor: [255, 255, 255],
textColor: [0, 0, 0],
lineColor: [0, 0, 0],
lineWidth: 0.2,
minCellHeight:10,
valign: 'middle'
},
headStyles: {
fontSize: 15,
},
head: [['No.', 'Transaction Date', 'Payment Method', 'HK$']],
body: [
['#87254', '2024-05-13', 'DOCUMENT FEE', 'AsiaPay(Wechat Pay)', '100.00'],
['#87255', '2024-05-13', 'VISA FEE', 'AsiaPay(Wechat Pay)', '50.00'],
[{
colSpan: 3,
content: 'TOTAL:',
styles: {valign: 'middle', halign: 'right'},
}, '150.00'],
],
})
// draw foot
drawItem(doc, {title: 'Remarks:', content: ''}, 155)
drawItem(doc, {title: 'Cashier:', content: ''}, 155,"right")
doc.setFont('Helvetica','italic');
drawItem(doc, {title: 'Please retain this acknowledgement receipt for your record.'}, 185)
const url = doc.output('bloburl')
setViewUrl(url.toString())
}, [])
function downloadPDF() {
saveAs(viewUrl, 'receipt-87254.pdf');
}
return (<div>
<h1>Index</h1>
<Button onClick={downloadPDF}>PDF</Button>
<div>
<iframe src={viewUrl} style={{
border: 'none',
width: '100%',
height: 800
}}></iframe>
</div>
<Button onClick={()=>GeneratePdf('test-11.pdf')}>PDF</Button>
</div>)
}

View File

@ -6,6 +6,8 @@ import AuthLogin from "@/pages/auth/login.tsx";
import AuthLayout from "@/routes/layout/auth-layout.tsx";
const ManualIndex = lazy(() => import("@/pages/manual/index.tsx"));
const BillQuery = lazy(() => import("@/pages/bill/query.tsx"));
const BillReconciliation = lazy(() => import("@/pages/bill/reconciliation.tsx"));
const routes: RouteObject[] = [
@ -37,6 +39,14 @@ const routes: RouteObject[] = [
path: 'manual',
element: <ManualIndex/>
},
{
path: 'bill',
element: <BillQuery/>
},
{
path: 'reconciliation',
element: <BillReconciliation/>
},
]
},
]

View File

@ -7,6 +7,8 @@ import AuthGuard from "@/routes/layout/auth-guard.tsx";
import AppLogo from "@/assets/AppLogo.tsx";
import useAuth from "@/hooks/useAuth.ts";
import {I18nSwitcher} from "@/i18n";
import {DashboardNavigation} from "@/routes/layout/dashboard-navigation.tsx";
import { IconExit } from "@douyinfe/semi-icons";
const {Header, Content, Sider} = Layout;
@ -22,7 +24,7 @@ export const HeaderUserAvatar = () => {
position={'bottomRight'}
render={
<Dropdown.Menu>
<Dropdown.Item onClick={logout}>{t('layout.logout')}</Dropdown.Item>
<Dropdown.Item icon={<IconExit />} onClick={logout}>{t('layout.logout')}</Dropdown.Item>
</Dropdown.Menu>
}
>
@ -64,15 +66,19 @@ export const CommonHeader: React.FC<CommonHeaderProps> = ({children, title, righ
type LayoutProps = {
children: React.ReactNode
}
export const BaseLayout: React.FC<LayoutProps> = ({children}) => {
return (<Layout>
<CommonHeader/>
<Layout style={{
backgroundColor: '#f8f9fa'
}}>
<Sider style={{width: '250px'}}>Sider</Sider>
<Sider style={{width: '250px'}}>
<DashboardNavigation />
</Sider>
<Content style={{padding: '15px'}}>
<div className="content-container" style={{backgroundColor: '#fff', minHeight: 300, borderRadius: 10}}>
<div className="content-container" style={{backgroundColor: '#fff', minHeight: 300, borderRadius: 10,padding:15}}>
{children}
</div>
</Content>

View File

@ -0,0 +1,48 @@
import {NavLink} from "react-router-dom";
import {useMemo} from "react";
import {useTranslation} from "react-i18next";
import useAuth from "@/hooks/useAuth.ts";
import {IconQRCode, IconQuery, IconReconciliation} from "@/components/logo";
const AllDashboardMenu = [
{
key: 'manual',
icon: <IconQRCode/>,
path: '/dashboard/manual',
},
{
key: 'bill',
icon: <IconQuery/>,
path: '/dashboard/bill',
role: ['root', 'ro', 'fo']
},
{
key: 'check',
icon: <IconReconciliation/>,
path: '/dashboard/reconciliation',
role: ['root', 'ro', 'fo']
}
]
export function DashboardNavigation() {
const {t} = useTranslation()
const {user} = useAuth();
const navItems = useMemo(() => {
if (!user) return [];
return AllDashboardMenu.filter(it => {
return !it.role || it.role.includes(user.department)
});
}, [user])
return (<div className={'dashboard-menu-container'}>
{navItems.map((it) => (
<NavLink to={it.path} key={it.key} className={'nav-item'}>
{it.icon}
<span className="menu-text">{t(`layout.menu.${it.key}`)}</span>
</NavLink>
))}
</div>
);
}

View File

@ -0,0 +1,73 @@
import JsPDF from "jspdf";
import autoTable from "jspdf-autotable";
function drawItem(doc: JsPDF, item: {
title: string;
content?: string
}, y: number, align: 'left' | 'right' = 'left', fontSize: number = 13) {
doc.setFontSize(fontSize);
// const width = doc.internal.pageSize.getWidth();
doc.text(item.title, align == 'left' ? 20 : 180, y);
if (item.content && item.content.length > 0) doc.text(item.content, align == 'left' ? 65 : 230, y, {maxWidth: 150});
}
export function GeneratePdf(filename: string) {
const doc = new JsPDF({
orientation: 'landscape',
format: 'a4'
});
// const width = doc.internal.pageSize.getWidth();
// const height = doc.internal.pageSize.getHeight();
doc.setFont('Helvetica', 'normal', 'bold');
doc.setFontSize(20);
doc.text('ACKNOWLEDGEMENT RECEIPT', 100, 20, {});
doc.setFont('Helvetica', 'normal', 'normal');
drawItem(doc, {title: 'Student Name:', content: 'BAI ZHONGHONG'}, 40)
drawItem(doc, {title: 'Reference Number:', content: '202402655'}, 40, "right")
drawItem(doc, {title: 'Student Number:', content: '236171275'}, 48)
drawItem(doc, {title: 'Print Date:', content: '2024-05-19'}, 48, "right")
drawItem(doc, {
title: 'Programme:',
content: 'MASTER OF SOCIAL SCIENCES IN INTERNATIONAL RELATIONS FOR BELT AND ROAD COUNTRIES'
}, 56)
drawItem(doc, {title: 'Mode of Study:', content: 'FULL-TIME'}, 70)
// draw table
autoTable(doc, {
startY: 80,
theme: 'grid',
margin: {top: 37, left: 20, right: 20},
styles: {
fontSize: 13,
fillColor: [255, 255, 255],
textColor: [0, 0, 0],
lineColor: [0, 0, 0],
lineWidth: 0.2,
minCellHeight: 10,
valign: 'middle'
},
headStyles: {
fontSize: 15,
},
head: [['No.', 'Transaction Date', 'Payment Type', 'Payment Method', 'HK$']],
body: [
['#87254', '2024-05-13', 'DOCUMENT FEE ', 'AsiaPay(Wechat Pay)', '100.00'],
['#87255', '2024-05-13', 'VISA FEE', 'AsiaPay(Wechat Pay)', '50.00'],
[{
colSpan: 3,
content: 'TOTAL:',
styles: {valign: 'middle', halign: 'right'},
}, '150.00'],
],
})
// draw foot
drawItem(doc, {title: 'Remarks:', content: ''}, 155)
drawItem(doc, {title: 'Cashier:', content: ''}, 155, "right")
doc.setFont('Helvetica', 'italic');
drawItem(doc, {title: 'Please retain this acknowledgement receipt for your record.'}, 185)
doc.save(filename);
}

View File

@ -826,11 +826,6 @@
resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
"@types/file-saver@^2.0.7":
version "2.0.7"
resolved "https://registry.npmmirror.com/@types/file-saver/-/file-saver-2.0.7.tgz#8dbb2f24bdc7486c54aa854eb414940bbd056f7d"
integrity sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==
"@types/lodash@^4.17.1":
version "4.17.4"
resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.4.tgz#0303b64958ee070059e3a7184048a55159fe20b7"
@ -1233,11 +1228,6 @@ cosmiconfig@^7.0.0:
path-type "^4.0.0"
yaml "^1.10.0"
countup.js@^2.8.0:
version "2.8.0"
resolved "https://registry.npmmirror.com/countup.js/-/countup.js-2.8.0.tgz#64951f2df3ede28839413d654d8fef28251c32a8"
integrity sha512-f7xEhX0awl4NOElHulrl4XRfKoNH3rB+qfNSZZyjSZhaAoUk6elvhH+MNxMmlmuUJ2/QNTWPSA7U4mNtIAKljQ==
cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@ -1520,11 +1510,6 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"
file-saver@^2.0.5:
version "2.0.5"
resolved "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@ -2135,13 +2120,6 @@ raf@^3.4.1:
dependencies:
performance-now "^2.1.0"
react-countup@^6.5.3:
version "6.5.3"
resolved "https://registry.npmmirror.com/react-countup/-/react-countup-6.5.3.tgz#e892aa3eab2d6ba9c3cdba30bf4ed6764826d848"
integrity sha512-udnqVQitxC7QWADSPDOxVWULkLvKUWrDapn5i53HE4DPRVgs+Y5rr4bo25qEl8jSh+0l2cToJgGMx+clxPM3+w==
dependencies:
countup.js "^2.8.0"
react-dom@^18.2.0:
version "18.3.1"
resolved "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
@ -2495,11 +2473,6 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
use-sync-external-store@1.2.0:
version "1.2.0"
resolved "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
utility-types@^3.10.0:
version "3.11.0"
resolved "https://registry.npmmirror.com/utility-types/-/utility-types-3.11.0.tgz#607c40edb4f258915e901ea7995607fdf319424c"
@ -2559,10 +2532,3 @@ yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zustand@^4.5.2:
version "4.5.2"
resolved "https://registry.npmmirror.com/zustand/-/zustand-4.5.2.tgz#fddbe7cac1e71d45413b3682cdb47b48034c3848"
integrity sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==
dependencies:
use-sync-external-store "1.2.0"