Compare commits

...

17 Commits

Author SHA1 Message Date
0910808853 add apply_button permission for batch sync log 2024-09-11 17:32:06 +08:00
d656720a79 update 2024-09-10 12:00:22 +08:00
c67bcc5d07 fixed: update user role change 2024-09-10 11:43:25 +08:00
6eeed9b451 update modal error message 2024-09-05 15:47:49 +08:00
6dbfbddeb6 update paid time to utc time 2024-09-03 17:05:15 +08:00
730d1b4fb6 update permission 2024-08-31 21:58:53 +08:00
f163722e11 add confirm modal when student not exists 2024-08-31 21:40:54 +08:00
ae1d587b91 update for test 2024-08-29 22:16:49 +08:00
cd839e923f update for test 2024-08-29 14:25:42 +08:00
e18fa9f15f update 2024-08-28 22:53:54 +08:00
1e0c687a9b update 2024-08-28 22:53:02 +08:00
2939228cfa 权限管理 2024-08-28 17:25:11 +08:00
7c84803b36 update pdf params 2024-08-27 22:55:51 +08:00
8590d577bd feat: add user permission manage 2024-08-27 22:25:14 +08:00
1411360614 feat: add import billing records 2024-08-27 16:38:19 +08:00
960f08d92e feat: Complete add billing records 2024-08-27 15:45:02 +08:00
e778553600 feat: new permission ui update 2024-08-18 23:41:18 +08:00
36 changed files with 1902 additions and 531 deletions

View File

@ -14,5 +14,9 @@ module.exports = {
'warn', 'warn',
{ allowConstantExport: true }, { allowConstantExport: true },
], ],
'@typescript-eslint/no-explicit-any':[
'off'
],
'no-mixed-spaces-and-tabs':'off'
}, },
} }

44
Dockerfile-test Normal file
View File

@ -0,0 +1,44 @@
# Dockerfile for a React app build and run
FROM node:18.19.1-alpine AS builder
MAINTAINER yaclty2@gmail.com
WORKDIR /app
# envs 配置
# 应用部署后的URL
ENV APP_SITE_URL ""
# 应用接口前缀
ENV APP_API_URL ""
# Copy source code to the builder
COPY package.json yarn.lock* ./
COPY public ./public
COPY src ./src
COPY index.html ./
COPY *.ts .
COPY *.json .
# Install dependencies and build the app
RUN yarn install
RUN yarn build-test
# Step 2. Production image, copy all the files and run nginx
FROM nginx:1.26-alpine3.19 AS runner
WORKDIR /app
# envs 配置
ENV APP_API_URL localhost:50000
# nginx配置文件
#COPY nginx.conf /etc/nginx/conf.d/default.conf.template
# 编译文件
COPY --from=builder /app/dist ./
# RUN /bin/sh envsubst /etc/nginx/templates/*.template /etc/nginx/conf.d
#WORKDIR /etc/nginx/conf.d/
#ENTRYPOINT sed -i "s~<!--app_url-->~<script>const APP_SITE_URL='${APP_SITE_URL}';</script>~" /app/index.html && envsubst '$APP_API_URL' < default.conf.template > default.conf && cat default.conf && nginx -g 'daemon off;'
# 暴露80端口
EXPOSE 80
# 启动Nginx服务
CMD ["nginx", "-g", "daemon off;"]

View File

@ -27,7 +27,7 @@
<div class="ldio-yzaezf3dcmj"><div></div></div> <div class="ldio-yzaezf3dcmj"><div></div></div>
</div> </div>
</div> </div>
<div>Payment System resources loading ...</div> <div>Payment System loading ...</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -22,8 +22,8 @@ server {
} }
location ^~/api { location ^~/api {
proxy_pass http://localhost:30000; proxy_pass https://103.124.155.66;
proxy_set_header Host $host; proxy_set_header Host test-payment-be.hkchc.team;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header REMOTE-HOST $remote_addr;

View File

@ -34,7 +34,8 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^14.1.1", "react-i18next": "^14.1.1",
"react-router-dom": "^6.23.1" "react-router-dom": "^6.23.1",
"read-excel-file": "^5.8.5"
}, },
"devDependencies": { "devDependencies": {
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",

View File

@ -201,9 +201,98 @@ body #root{
} }
} }
} }
.semi-popconfirm{
.semi-popconfirm-inner{
padding: 15px 15px;
min-width: 260px;
}
.semi-popconfirm-header{
//align-items: center;
margin-bottom: 5px;
}
.semi-popconfirm-header-icon{
width: 18px;
}
.semi-popconfirm-header-title{
line-height: 16px;
}
.semi-icon-extra-large{
font-size: 20px;
}
.semi-popconfirm-body-withIcon{
margin-left: 30px;
}
.semi-popconfirm-footer{
margin-top: 15px;
.semi-button{
//padding: 2px 10px;
//line-height: 16px;
height: 28px;
}
}
}
.upload-wrapper{
position: relative;
.semi-upload-file-list-title{
display: none;
}
.semi-upload-file-list{
//flex-basis: auto;
margin: 0;
left:170px;
top:0px;
//top:40px;
position: absolute;
}
.semi-upload-file-card{
height: auto;
border-radius: var(--semi-border-radius-small);
--semi-color-fill-0: rgba(0,0,0,0.05)
}
.semi-upload-file-card-preview-placeholder{
width: 24px;
height: 22px;
margin: 5px;
}
.semi-icon-file{
font-size: 16px;
}
}
/************ end overrides ****************/ /************ end overrides ****************/
.bill-pdf-previewer{
border-radius: 5px;
overflow: hidden;
.bill-pdf-container{
height: 550px;
overflow: hidden;
width: 100%;
border-radius: 5px;
border:none;
display: block;
}
}
.import-record-wrapper{
margin: 15px 0;
.table-list{
max-width: 100%;
overflow: auto;
max-height: 350px;
}
table{
width: 100%;
border-collapse: collapse;
tr{
&:first-child td{
white-space: nowrap;
}
}
td{
padding: 6px 10px;
border:solid 1px #eee;
}
}
}
.dashboard-menu-container { .dashboard-menu-container {
padding: var(--dashboard-layout-padding, 15px); padding: var(--dashboard-layout-padding, 15px);
position: sticky; position: sticky;

View File

@ -0,0 +1,18 @@
import {IconAlertCircle} from "@douyinfe/semi-icons";
import React from "react";
import './style.less'
type AlertProps = {
message: React.ReactNode;
}
function Alert(props: AlertProps) {
if(!props.message) return null;
return <div className={'alert-container'}>
<IconAlertCircle/>
<span className={'alert-text'}>{props.message}</span>
</div>
}
export default Alert

View File

@ -0,0 +1,8 @@
.alert-container{
display: flex;
align-items: center;
color:red;
.alert-text{
margin-left: 5px;
}
}

View File

@ -5,6 +5,7 @@ import {useTranslation} from "react-i18next";
import {Card} from "@/components/card"; import {Card} from "@/components/card";
import {BillQueryParams} from "@/service/api/bill.ts"; import {BillQueryParams} from "@/service/api/bill.ts";
import {useBillTypes} from "@/hooks/useBillTypes.ts"; import {useBillTypes} from "@/hooks/useBillTypes.ts";
import {usePaymentChannels} from "@/hooks/usePaymentChannels.ts";
type SearchFormProps = { type SearchFormProps = {
onSearch?: (params: BillQueryParams) => void; onSearch?: (params: BillQueryParams) => void;
@ -29,6 +30,7 @@ type SearchFormFields = {
} }
const SearchForm: React.FC<SearchFormProps> = (props) => { const SearchForm: React.FC<SearchFormProps> = (props) => {
const BillTypes = useBillTypes() const BillTypes = useBillTypes()
const { paymentChannelList } = usePaymentChannels()
const formSubmit = (value: SearchFormFields) => { const formSubmit = (value: SearchFormFields) => {
const params: BillQueryParams = {} const params: BillQueryParams = {}
@ -146,12 +148,11 @@ const SearchForm: React.FC<SearchFormProps> = (props) => {
</Form.Select> </Form.Select>
</Col> </Col>
<Col xxl={4} xl={6} md={8}> <Col xxl={4} xl={6} md={8}>
<Form.Select showClear field="payment_channel" label={t('bill.title_pay_channel')} <Form.Select
placeholder={t('base.please_select')} style={{width: '100%'}}> showClear field="payment_channel" label={t('bill.title_pay_channel')}
<Form.Select.Option value="FLYWIRE">FLYWIRE</Form.Select.Option> placeholder={t('base.please_select')} style={{width: '100%'}}
<Form.Select.Option value="CBP">CBP</Form.Select.Option> optionList={paymentChannelList}
<Form.Select.Option value="PPS">PPS</Form.Select.Option> />
</Form.Select>
</Col> </Col>
<Col xxl={4} xl={6} md={8}> <Col xxl={4} xl={6} md={8}>
<Form.Select showClear field="is_delivered" label={t('bill.delivered_status')} <Form.Select showClear field="is_delivered" label={t('bill.delivered_status')}

View File

@ -0,0 +1,22 @@
import React from "react";
import useAuth from "@/hooks/useAuth.ts";
type PermissionKeys =
'apply_button'
| 'apply_page'
| 'bill_page'
| 'complete_button'
| 'confirm_button'
| 'manual_payment'
| 'permission_edit';
type PermissionCheckProps = {
permission: PermissionKeys ;
children: React.ReactNode;
}
export const PermissionCheck: React.FC<PermissionCheckProps> = ({permission, children}) => {
const {user} = useAuth()
return <>
{user && user.permissions && user.permissions[permission] ? children : null}
</>
}

View File

@ -52,6 +52,16 @@ export const AuthProvider = ({children}: { children: React.ReactNode }) => {
const [state, dispatch] = useReducer(authReducer, initialState); const [state, dispatch] = useReducer(authReducer, initialState);
// MOCK INIT DATA // MOCK INIT DATA
const refreshUserInfo = async ()=>{
const user = await getUserInfo();
dispatch({
action: 'refresh',
payload: {
isLoggedIn: !!user,
user: getInitUserData(user)
}
})
}
const init = async () => { const init = async () => {
const token = getAuthToken(); const token = getAuthToken();
if (!token) { if (!token) {
@ -141,7 +151,8 @@ export const AuthProvider = ({children}: { children: React.ReactNode }) => {
return (<AuthContext.Provider value={{ return (<AuthContext.Provider value={{
...state, ...state,
login, logout, login, logout,
mockLogin, updateUser mockLogin, updateUser,
refreshUserInfo
}}>{children}</AuthContext.Provider>) }}>{children}</AuthContext.Provider>)
} }
export default AuthContext export default AuthContext

View File

@ -36,6 +36,7 @@ export const ConfigProvider = ({children}: { children: React.ReactNode }) => {
setConfig({...config, fontFamily}) setConfig({...config, fontFamily})
} }
useEffect(() => { useEffect(() => {
console.log(`APP-BUILD-AT:${buildVersion}`)
// init localization use LocalStorage // init localization use LocalStorage
if (config.i18n) { if (config.i18n) {
i18n.changeLanguage(config.i18n).then(() => console.log('init localization use', config.i18n)) i18n.changeLanguage(config.i18n).then(() => console.log('init localization use', config.i18n))

View File

@ -0,0 +1,42 @@
import {useState} from "react";
export function usePaymentChannels(){
//_setPaymentChannelList
const [paymentChannelList] = useState<OptionValue[]>([
{value: 'FLYWIRE', label: 'FlyWire'},
{value: 'PPS', label: 'PPS'},
{value: 'CBP', label: 'CBP'},
// {value:'CASH', label:'Cash'},
// {value: 'ASIAPAY', label: 'AsiaPay'},
// {value: 'OTHER', label: 'Other'},
])
//_setPaymentChannelList
const [paymentMethodList] = useState<OptionValue[]>([
{value: 'card', label: 'Card(VISA,MasterCard,UnionPay,JCB...)'},
{value: 'wechat', label: 'Wechat'},
{value: 'alipay', label: 'Alipay'},
{value:'cash', label:'Cash'},
{value: 'other', label: 'Other'},
])
// useEffect(()=>{
// if(BillTypes.length == 0){
// selectBillTypeList().then(ret => {
// const types = ret.filter(it=>!it.description.toUpperCase().startsWith('ADJUSTMENT'))
// .map(it=>({value: it.type, label: it.description}))
// setBillTypes(types)
// // 避免出现多次
// BillTypesCache.length = 0;
// BillTypesCache.push(...types)
// })
// }
// },[])
return {
paymentChannelList,
paymentMethodList
}
}

View File

@ -1,4 +1,4 @@
import {useEffect, useState} from "react"; import {useRequest} from "ahooks";
function getRemoteUserNameList() { function getRemoteUserNameList() {
return new Promise<string[][]>((resolve, reject) => { return new Promise<string[][]>((resolve, reject) => {
@ -23,13 +23,21 @@ function getRemoteUserNameList() {
} }
export function useRemoteUserList() { export function useRemoteUserList() {
const loadUserList = async () => {
const [usernameList, setUserList] = useState<string[]>([]) const list = await getRemoteUserNameList();
return list.flat();
useEffect(()=>{ }
getRemoteUserNameList().then(data=>{ const {data:usernameList} = useRequest(loadUserList,{
setUserList(data.flat()) cacheKey:'remote-username-list',
staleTime: 300000
}) })
},[]) // const [userList, setUserList] = useState<string[]>([])
//const [usernameList, setUserList] = useState<string[]>([])
// useEffect(()=>{
// getRemoteUserNameList().then(data=>{
// setUserList(data.flat())
// })
// },[])
return usernameList return usernameList
} }

View File

@ -1,16 +1,22 @@
{ {
"base": { "base": {
"add": "Add", "add": "Add",
"add_success": "Add success",
"bill_number": "Bill Number", "bill_number": "Bill Number",
"btn_search_submit": "Search", "btn_search_submit": "Search",
"cancel": "Cancel", "cancel": "Cancel",
"close": "Close", "close": "Close",
"confirm": "Confirm", "confirm": "Confirm",
"confirm_next_operation": "Please confirm this operation", "confirm_and_add": "Confirm and add",
"confirm_delete": "Please confirm delete record",
"confirm_import": "Confirm Import",
"confirm_information": "Confirmation Information",
"confirm_next_operation": "Are you sure to process this action",
"confirm_paid": "Confirm paid", "confirm_paid": "Confirm paid",
"copy-pay-url": "Copy payment link", "copy-pay-url": "Copy payment link",
"delete": "Delete",
"operate_fail": "Operation failed", "operate_fail": "Operation failed",
"operate_success": "Operation success", "operate_success": "Process success",
"please_enter": "Please Enter", "please_enter": "Please Enter",
"please_select": "Please Select", "please_select": "Please Select",
"please_select_bill_type": "Please Select Bill Type", "please_select_bill_type": "Please Select Bill Type",
@ -18,11 +24,23 @@
"query_bill": "Failed to query bill:", "query_bill": "Failed to query bill:",
"remove": "Remove", "remove": "Remove",
"save": "Save", "save": "Save",
"student_number": "Student Number" "save_failed": "Save failed",
"save_success": "Saved successfully",
"select_excel_file": "Select File",
"select_upload_file": "Select File",
"student_number": "Student Number",
"title_error_tip": "Error message",
"validate": {
"email": "Email format is incorrect",
"error_details_message": "Bill detail not correct"
},
"warning": "Warning"
}, },
"bill": { "bill": {
"add_bill_record": "Add Transaction Record",
"bill_date": "In", "bill_date": "In",
"bill_number": "Bill Number", "bill_number": "Bill Number",
"btn_cancel_confirm": "Cancel Confirm",
"cancel": "Cancel", "cancel": "Cancel",
"cancel_confirm": "Please make sure to cancel the bill", "cancel_confirm": "Please make sure to cancel the bill",
"cancel_confirm_bills": "Confirm the check check bill?", "cancel_confirm_bills": "Confirm the check check bill?",
@ -41,13 +59,23 @@
"confirm_student_number": "Confirm Student Number", "confirm_student_number": "Confirm Student Number",
"confirm_success": "Confirm success!", "confirm_success": "Confirm success!",
"confirmed": "Confirmed", "confirmed": "Confirmed",
"create": {
"add_confirm": "The data is abnormal. Please confirm whether to continue",
"confirm": "Confirm Add",
"pay_area": "Payment Area"
},
"delivered_status": "Delivered Status", "delivered_status": "Delivered Status",
"delivered_status_no": "Undivided", "delivered_status_no": "Undivided",
"delivered_status_yes": "Delivered", "delivered_status_yes": "Delivered",
"download-qr-code": "Download QR Code", "download-qr-code": "Download QR Code",
"download_receipt": "Download receipt", "download_receipt": "Download receipt",
"export_excel": "Export Transaction Excel", "export_excel": "Export Transaction Excel",
"import_bill": "Add Transaction Record", "import": {
"error_require_file": "Please select the transaction record file to import",
"error_require_payment_channel": "Please select a payment channel",
"error_require_records": "File data incorrect"
},
"import_bill": "Import Transaction Record",
"import_excel": "Import Transaction Excel", "import_excel": "Import Transaction Excel",
"paid": "Paid", "paid": "Paid",
"paid_confirm": "Please confirm the order status is set to paid", "paid_confirm": "Please confirm the order status is set to paid",
@ -138,5 +166,24 @@
"text_failed": "Payment Failure", "text_failed": "Payment Failure",
"text_success": "Payment Success", "text_success": "Payment Success",
"total_amount": "Total Amount HKD" "total_amount": "Total Amount HKD"
},
"permission": {
"message": {
"empty_tips": "No data, please create new",
"error_require": "Please set your username or role",
"username_exist": "Username already exists"
},
"title": {
"add": "New",
"bill.btn.check": "Reconciliation button",
"bill.btn.confirm": "Confirm bill button",
"bill.check": "Reconciliation page",
"bill.pay": "Manually set bill completion button",
"bill.query": "Bill query page",
"manual": "Manual payment page",
"permission": "Permission editing",
"role": "Role",
"username": "Username"
}
} }
} }

View File

@ -1,14 +1,20 @@
{ {
"base": { "base": {
"add": "增加", "add": "增加",
"add_success": "添加成功",
"bill_number": "账单编号", "bill_number": "账单编号",
"btn_search_submit": "搜索", "btn_search_submit": "搜索",
"cancel": "取消", "cancel": "取消",
"close": "关闭", "close": "关闭",
"confirm": "确定", "confirm": "确定",
"confirm_and_add": "确认并添加",
"confirm_delete": "请确认是否删除此数据",
"confirm_import": "确认导入",
"confirm_information": "确认信息",
"confirm_next_operation": "请确认是否进行此操作", "confirm_next_operation": "请确认是否进行此操作",
"confirm_paid": "确认已支付", "confirm_paid": "确认已支付",
"copy-pay-url": "复制支付链接", "copy-pay-url": "复制支付链接",
"delete": "删除",
"operate_fail": "操作失败", "operate_fail": "操作失败",
"operate_success": "操作成功", "operate_success": "操作成功",
"please_enter": "请输入", "please_enter": "请输入",
@ -18,11 +24,23 @@
"query_bill": "查询账单失败:", "query_bill": "查询账单失败:",
"remove": "删除", "remove": "删除",
"save": "保存", "save": "保存",
"student_number": "学号" "save_failed": "保存失败",
"save_success": "保存成功",
"select_excel_file": "选择文件",
"select_upload_file": "选择文件",
"student_number": "学号",
"title_error_tip": "错误提示",
"validate": {
"email": "Email格式不正确",
"error_details_message": "账单详情设置不正确"
},
"warning": "警告"
}, },
"bill": { "bill": {
"add_bill_record": "添加交易记录",
"bill_date": "开始支付时间", "bill_date": "开始支付时间",
"bill_number": "账单编号", "bill_number": "账单编号",
"btn_cancel_confirm": "取消确认",
"cancel": "作废", "cancel": "作废",
"cancel_confirm": "确定作废此账单", "cancel_confirm": "确定作废此账单",
"cancel_confirm_bills": "确认对账选中账单?", "cancel_confirm_bills": "确认对账选中账单?",
@ -41,13 +59,23 @@
"confirm_student_number": "确认学号", "confirm_student_number": "确认学号",
"confirm_success": "对账成功!", "confirm_success": "对账成功!",
"confirmed": "已对账", "confirmed": "已对账",
"create": {
"add_confirm": "数据异常请确认是否继续添加",
"confirm": "确认添加",
"pay_area": "支付区域"
},
"delivered_status": "分账状态", "delivered_status": "分账状态",
"delivered_status_no": "未分账", "delivered_status_no": "未分账",
"delivered_status_yes": "已分账", "delivered_status_yes": "已分账",
"download-qr-code": "下载二维码", "download-qr-code": "下载二维码",
"download_receipt": "下载收据", "download_receipt": "下载收据",
"export_excel": "导出交易记录", "export_excel": "导出交易记录",
"import_bill": "添加交易记录", "import": {
"error_require_file": "请选择要导入的交易记录文件",
"error_require_payment_channel": "请选择支付渠道",
"error_require_records": "文件数据不正确"
},
"import_bill": "导入交易记录",
"import_excel": "导入交易记录", "import_excel": "导入交易记录",
"paid": "已支付", "paid": "已支付",
"paid_confirm": "是否将此订单状态设为已支付", "paid_confirm": "是否将此订单状态设为已支付",
@ -138,5 +166,24 @@
"text_failed": "支付失败", "text_failed": "支付失败",
"text_success": "支付成功", "text_success": "支付成功",
"total_amount": "应付总金额" "total_amount": "应付总金额"
},
"permission": {
"message": {
"empty_tips": "暂无数据,请新增",
"error_require": "请设置用户名或角色",
"username_exist": "用户名已经存在了"
},
"title": {
"add": "新增",
"bill.btn.check": "对账按钮",
"bill.btn.confirm": "确认账单按钮",
"bill.check": "对账页面",
"bill.pay": "手动设置账单完成按钮",
"bill.query": "账单查询页面",
"manual": "现场支付页面",
"permission": "权限编辑",
"role": "角色",
"username": "用户名"
}
} }
} }

View File

@ -1,14 +1,20 @@
{ {
"base": { "base": {
"add": "增加", "add": "增加",
"add_success": "添加成功",
"bill_number": "帳單編號", "bill_number": "帳單編號",
"btn_search_submit": "搜尋", "btn_search_submit": "搜尋",
"cancel": "取消", "cancel": "取消",
"close": "關閉", "close": "關閉",
"confirm": "確定", "confirm": "確定",
"confirm_and_add": "確認並添加",
"confirm_delete": "請確認是否刪除此數據",
"confirm_import": "確認導入",
"confirm_information": "確認訊息",
"confirm_next_operation": "請確認是否進行此操作", "confirm_next_operation": "請確認是否進行此操作",
"confirm_paid": "確認已支付", "confirm_paid": "確認已支付",
"copy-pay-url": "複製付款連結", "copy-pay-url": "複製付款連結",
"delete": "刪除",
"operate_fail": "操作失敗", "operate_fail": "操作失敗",
"operate_success": "操作成功", "operate_success": "操作成功",
"please_enter": "請輸入", "please_enter": "請輸入",
@ -18,11 +24,23 @@
"query_bill": "查詢帳單失敗:", "query_bill": "查詢帳單失敗:",
"remove": "刪除", "remove": "刪除",
"save": "儲存", "save": "儲存",
"student_number": "學號" "save_failed": "保存失敗",
"save_success": "保存成功",
"select_excel_file": "選擇文件",
"select_upload_file": "選擇文件",
"student_number": "學號",
"title_error_tip": "錯誤提示",
"validate": {
"email": "Email格式不正確",
"error_details_message": "帳單詳情設定不正確"
},
"warning": "警告"
}, },
"bill": { "bill": {
"add_bill_record": "新增交易記錄",
"bill_date": "開始支付時間", "bill_date": "開始支付時間",
"bill_number": "帳單編號", "bill_number": "帳單編號",
"btn_cancel_confirm": "取消確認",
"cancel": "作廢", "cancel": "作廢",
"cancel_confirm": "確定作廢此帳單", "cancel_confirm": "確定作廢此帳單",
"cancel_confirm_bills": "確認對帳選取帳單?", "cancel_confirm_bills": "確認對帳選取帳單?",
@ -41,13 +59,23 @@
"confirm_student_number": "確認學號", "confirm_student_number": "確認學號",
"confirm_success": "對帳成功!", "confirm_success": "對帳成功!",
"confirmed": "已對帳", "confirmed": "已對帳",
"create": {
"add_confirm": "數據異常請確認是否繼續添加",
"confirm": "確認新增",
"pay_area": "支付區域"
},
"delivered_status": "分帳狀態", "delivered_status": "分帳狀態",
"delivered_status_no": "未分帳", "delivered_status_no": "未分帳",
"delivered_status_yes": "已分賬", "delivered_status_yes": "已分賬",
"download-qr-code": "下載二維碼", "download-qr-code": "下載二維碼",
"download_receipt": "下載收據", "download_receipt": "下載收據",
"export_excel": "導出交易记录", "export_excel": "導出交易记录",
"import_bill": "新增交易记录", "import": {
"error_require_file": "請選擇要匯入的交易記錄文件",
"error_require_payment_channel": "請選擇支付管道",
"error_require_records": "文件資料不正確"
},
"import_bill": "導入交易記錄",
"import_excel": "導入交易记录", "import_excel": "導入交易记录",
"paid": "已支付", "paid": "已支付",
"paid_confirm": "是否將此訂單狀態設為已支付", "paid_confirm": "是否將此訂單狀態設為已支付",
@ -138,5 +166,24 @@
"text_failed": "付款失敗", "text_failed": "付款失敗",
"text_success": "付款成功", "text_success": "付款成功",
"total_amount": "應付總金額" "total_amount": "應付總金額"
},
"permission": {
"message": {
"empty_tips": "暫無數據,請新增",
"error_require": "請設定使用者名稱或角色",
"username_exist": "使用者名稱已經存在了"
},
"title": {
"add": "新增",
"bill.btn.check": "對帳按鈕",
"bill.btn.confirm": "確認帳單按鈕",
"bill.check": "對帳頁面",
"bill.pay": "手動設定帳單完成按鈕",
"bill.query": "帳單查詢頁面",
"manual": "現場付款頁面",
"permission": "權限編輯",
"role": "角色",
"username": "使用者名稱"
}
} }
} }

View File

@ -0,0 +1,61 @@
.table-wrapper{
font-size: 14px;
.form-table {
border: 1px var(--semi-color-fill-2) solid;
}
.item {
border-left: solid 1px transparent;
&:first-child{
border-left: none;
margin-left: 0px;
}
}
.table-row {
display: flex;
//align-items: flex-start;
.item {
border-left-color: var(--semi-color-fill-2);
border-bottom: 1px var(--semi-color-fill-2) solid;
padding: 10px 20px;
flex:1;
display: flex;
align-items: center;
}
&:last-child{
.item {
border-bottom: none;
}
}
}
.header {
font-weight: bold;
border-bottom: 1px var(--semi-color-fill-2) solid !important;
.item-username,.item-role{
display: flex !important;
}
}
.item-type{
min-width: 80px;
}
.item-username{
min-width: 170px;
display: block !important;
}
.item-role{
min-width: 130px;
width: 130px;
display: block !important;
}
.item-type-bill-pay{
min-width: 160px;
}
.item-operation {
min-width: 140px;
width: 140px;
}
}

View File

@ -1,98 +1,311 @@
import {Card} from "@/components/card";
import {useSetState} from "ahooks"; import {useSetState} from "ahooks";
import {Button, Select, Space, Toast} from "@douyinfe/semi-ui"; import {Button, Checkbox, Empty, Popconfirm, Select, Space, Spin, Toast} from "@douyinfe/semi-ui";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import {useEffect, useMemo} from "react"; import {useEffect, useMemo} from "react";
import {getPermissionList, savePermissionList} from "@/service/api/user.ts";
import {useRemoteUserList} from "@/hooks/useRemoteUserList.ts";
const DEFAULT_ROLES = [ import {Card} from "@/components/card";
'root', 'ro', 'fo' import './permission.less';
] import {useRemoteUserList} from "@/hooks/useRemoteUserList.ts";
import {
createUserPermission,
getUserPermissionList,
removeUserPermission,
updateUserPermission
} from "@/service/api/user.ts";
import useAuth from "@/hooks/useAuth.ts";
// const PermissionList = [
// 'manual',
// 'bill.query',
// 'bill.check',
// 'bill.pay',
// 'bill.btn.confirm',
// 'bill.btn.check',
// 'permission'
// ]
const RoleOptionList = [
{label: 'ROOT', value: 'root'},
{label: 'RO', value: 'ro'},
{label: 'FO', value: 'fo'},
];
type UserPermissionItemProps = {
it: UserPermission;
onChange: (action: 'remove' | 'saved' | 'modify', value: UserPermission) => Promise<void>;
usernameOptionList: OptionValue[]
roleOptionList: OptionValue[]
}
const UserPermissionItem = ({it, onChange, usernameOptionList,roleOptionList}: UserPermissionItemProps) => {
const {t} = useTranslation()
const {user,refreshUserInfo} = useAuth()
const [state,setState] = useSetState({
loading: false,
currentRole: it.role
})
const onValueChange = (value: {
[key: string]: string | boolean
}) => {
onChange('modify', {
...it,
...value
}).then();
}
const onRemove = () => {
if(it.id > 0){
setState({loading: true})
onChange('remove', it).finally(()=>{
setState({loading: false})
})
return;
}
onChange('remove', it).then();
}
const onSave = () => {
console.log(it)
if (!it.role || !it.username) {
Toast.warning(t('permission.message.error_require'))
return;
}
setState({loading: true})
onChange('saved', it).then(()=>{
if(self) refreshUserInfo().then(()=>console.log('self refresh'));
}).finally(()=>{
setState({loading: false})
})
}
// const [values,setValues] = use
return (<div className="table-row">
<div className="item item-username item-type">
<div className="form-item">
{it.id > 0 ? <span>{it.username}</span> : <Select
style={{width: '100%'}}
filter
optionList={usernameOptionList}
placeholder={t('base.please_select')}
value={it.username}
onChange={(value) => onValueChange({username: String(value)})}
/>}
</div>
</div>
<div className="item item-role item-type">
<div className="form-item">
<Select
style={{width: '100%'}}
optionList={roleOptionList}
value={it.role}
onChange={(value) => onValueChange({role: String(value)})}
placeholder={t('base.please_select')}
/>
</div>
</div>
<div className={`item item-type item-type-manual`}>
<Checkbox
checked={it.manual_payment}
onChange={e => onValueChange({manual_payment: !!e.target.checked})}/>
</div>
<div className={`item item-type item-type-bill-query`}>
<Checkbox
checked={it.bill_page}
onChange={e => onValueChange({bill_page: !!e.target.checked})}/>
</div>
<div className={`item item-type item-type-bill-check`}>
<Checkbox
checked={it.apply_page}
onChange={e => onValueChange({apply_page: !!e.target.checked})}/>
</div>
<div className={`item item-type item-type-bill-pay`}>
<Checkbox
checked={it.complete_button}
onChange={e => onValueChange({complete_button: !!e.target.checked})}/>
</div>
<div className={`item item-type item-type-bill-btn-confirm`}>
<Checkbox
checked={it.confirm_button}
onChange={e => onValueChange({confirm_button: !!e.target.checked})}/>
</div>
<div className={`item item-type item-type-bill-btn-check`}>
<Checkbox
checked={it.apply_button}
onChange={e => onValueChange({apply_button: !!e.target.checked})}/>
</div>
{user?.permissions?.role == 'root' && <div className={`item item-type item-type-bill-permission`}>
<Checkbox
checked={it.permission_edit}
disabled={user?.username == it.username}
onChange={e => onValueChange({permission_edit: !!e.target.checked})}/>
</div>}
<div className="item item-operation text-center">
<Space>
{user?.username != it.username && <Popconfirm
disabled={state.loading}
title={t('base.warning')} onConfirm={onRemove}
position={'topRight'}
content={`${t('base.confirm_delete')}?`}
>
<Button theme={'solid'} type="danger" size={'small'}>{t('base.delete')}</Button>
</Popconfirm>}
<Button loading={state.loading} size={'small'} theme={'solid'} onClick={onSave}>{t('base.save')}</Button>
</Space>
</div>
</div>)
}
const Permission = () => { const Permission = () => {
const {t} = useTranslation() const {t} = useTranslation()
const usernameList = useRemoteUserList(); const usernameList = useRemoteUserList();
const {user} = useAuth();
const [state, setState] = useSetState<{ const [state, setState] = useSetState<{
list: PermissionUserList[]; list: UserPermission[];
allList: UserPermission[];
loading?: boolean; loading?: boolean;
}>({ }>({
list: [] list: [],
allList:[]
}) })
const onUsernameChange = (role_name: string, username_list: string[]) => {
const index = state.list.findIndex(it => it.role_name == role_name) const usernameOptionList = useMemo(() => (usernameList?usernameList.map(name => ({label: name, value: name})):[]), [usernameList])
if (index != -1) { const roleOptionList = useMemo(() => {
state.list[index] = { const userRole = user?.permissions?.role ?? 'staff';
role_name, username_list if(userRole == 'root') return RoleOptionList;
}; return RoleOptionList.filter(it => (it.value == userRole));
}, [user,state.allList])
const buildPermission = (it: UserPermission)=>{
it.permission_edit = !!it.permission_edit
it.bill_page = !!it.bill_page
it.apply_page = !!it.apply_page
it.complete_button = !!it.complete_button
it.confirm_button = !!it.confirm_button
it.apply_button = !!it.apply_button
it.manual_payment = !!it.manual_payment
return it;
}
// load user permission list
const loadUserPermissionList = () => {
setState({loading: true})
getUserPermissionList().then(list => {
setState({allList:[...list]})
const userRole = (user?.permissions?.role ?? 'staff').toLowerCase();
list.forEach(it=>buildPermission(it))
if(userRole != 'root') {
list = list.filter(it => (it.role.toLowerCase() == userRole));
}
setState({loading: false, list})
})
}
useEffect(loadUserPermissionList, [user])
// remove a user permission
const removeItem = (index: number, id: number) => {
const removeFromList = () => {
const newList = [...state.list];
newList.splice(index, 1)
setState({list: newList})
}
if (id > 0) { // 数据库数据 需要调用接口
removeUserPermission(id).then(removeFromList)
return;
}
removeFromList();
}
const addNewRecord = () => {
setState({ setState({
list: state.list list: [...state.list, {
id: 0,
username: '',
role: '',
manual_payment: false,
bill_page: false,
apply_page: false,
complete_button: false,
confirm_button: false,
apply_button: false,
permission_edit: false
}]
}) })
} }
const handleChange = async (action: 'remove' | 'saved' | 'modify', value: UserPermission, index: number) => {
if (action == 'remove') removeItem(index, value.id)
else if (action == 'modify') {
const newList = [...state.list];
newList[index] = {
...value
} }
const saveRoles = () => { setState({list: newList})
setState({ } else if (action == 'saved') {
loading: true // 判断是否存在同名用户
}) const exist = state.list.filter(it=>it.id > 0).find(it => it.username == value.username && it.id != value.id)
savePermissionList(state.list).then(() => { if (exist) {
Toast.success({content: `Save Success`, duration: 3,}) Toast.error(t('permission.message.username_exist'))
}).catch(e => { return;
Toast.error({
content: `Save Error:${e.message}`,
duration: 3,
})
}).finally(() => {
setState({loading: false})
})
} }
const loadAllPermission = () => { const process = value.id > 0 ? updateUserPermission : createUserPermission;
setState({ try {
loading: true await process(value).then(() => {
}) Toast.success(t('base.save_success'))
getPermissionList().then(list => { //loadUserPermissionList();
const roles: { // const newList = [...state.list];
[key: string]: string[] // buildPermission(newValue)
} = {} // newList[index] = {
// array to object // ...newValue
list.forEach(it => { // }
roles[it.role_name] = it.username_list // setState({list: newList})
})
const permissionList: PermissionUserList[] = [];
DEFAULT_ROLES.forEach(role_name => {
permissionList.push({
role_name, username_list: roles[role_name]
})
})
setState({list: permissionList})
}).finally(() => {
setState({loading: false})
}) })
} catch (e) {
Toast.error(t('base.save_failed') + `(${(e as Error).message})`)
throw e;
}
}
} }
useEffect(loadAllPermission, [])
const optionList = useMemo(() => (usernameList.map(name => ({label: name, value: name}))), [usernameList])
return (<Card style={{marginBottom: 20}}> return (<Card style={{marginBottom: 20}}>
{state.list.map(it => (<div key={it.role_name} style={{marginBottom: 20}}> <div className="username-list table-wrapper">
<div className="permission-title" style={{marginBottom: 5}}>{it.role_name.toUpperCase()}</div> <div className="form-table">
<Select <div className="header table-row">
style={{width: '100%', backgroundColor: 'var(--semi-color-fill-0)'}} <div className="item item-username item-type">{t('permission.title.username')}</div>
filter <div className="item item-role item-type">{t('permission.title.role')}</div>
multiple <div className={`item item-type item-type-manual`}>{t(`permission.title.manual`)}</div>
size={'large'} <div className={`item item-type item-type-bill-query`}>{t(`permission.title.bill.query`)}</div>
defaultValue={it.username_list} <div className={`item item-type item-type-bill-check`}>{t(`permission.title.bill.check`)}</div>
optionList={optionList} <div className={`item item-type item-type-bill-pay`}>{t(`permission.title.bill.pay`)}</div>
defaultActiveFirstOption <div
allowCreate={true} className={`item item-type item-type-bill-btn-confirm`}>{t(`permission.title.bill.btn.confirm`)}</div>
placeholder={t('base.please_select')} <div
onChange={(users) => onUsernameChange(it.role_name, users as string[])} className={`item item-type item-type-bill-btn-check`}>{t(`permission.title.bill.btn.check`)}</div>
{user?.permissions?.role == 'root' && <div className={`item item-type item-type-bill-permission`}>{t(`permission.title.permission`)}</div>}
<div className="item item-operation">{t('bill.title_operate')}</div>
</div>
<Spin spinning={state.loading}>
{state.list.map((it, index) => (<UserPermissionItem
key={index} it={it}
onChange={async (action, value) => {
await handleChange(action, value, index)
}}
usernameOptionList={usernameOptionList}
roleOptionList={roleOptionList}
/>))}
{state.list.length == 0 && <div style={{backgroundColor: '#fafafa'}}>
<Empty
description={t('permission.message.empty_tips')}
style={{paddingBottom: 20}}
/> />
</div>))} </div>}
<Space> </Spin>
<Button </div>
loading={state.loading} onClick={saveRoles} </div>
theme={'solid'}>{state.loading ? 'Loading' : t('base.save')}</Button> {!state.loading && <Space style={{marginTop: 20}}>
{/*<div>{state.message||''}</div>*/} <Button style={{width: 100}} onClick={addNewRecord} theme={'solid'}>{t('permission.title.add')}</Button>
</Space> </Space>}
</Card>) </Card>)
} }
export default Permission export default Permission

View File

@ -1,60 +1,250 @@
import {Button, Col, Form, Modal, Row, Select, Space} from "@douyinfe/semi-ui"; import {
import React from "react"; Button,
Col,
Descriptions,
Divider,
Form,
InputNumber,
Modal,
Row,
Select,
Space,
Toast
} from "@douyinfe/semi-ui";
import {IconAlertCircle} from "@douyinfe/semi-icons";
import {Data} from "@douyinfe/semi-ui/lib/es/descriptions";
import React, {useMemo} from "react";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import {useSetState} from "ahooks"; import {useSetState} from "ahooks";
import {useBillTypes} from "@/hooks/useBillTypes.ts"; import {useBillTypes} from "@/hooks/useBillTypes.ts";
import {usePaymentChannels} from "@/hooks/usePaymentChannels.ts";
import {addBillRecord} from "@/service/api/bill.ts"
import dayjs from "dayjs";
import MoneyFormat from "@/components/money-format.tsx";
import {BizError} from "@/service/types.ts";
type BillPaidModalProps = { type BillPaidModalProps = {
onConfirm: () => void onConfirm: () => void
onCancel?: () => void onCancel?: () => void
} }
export const AddBillModal: React.FC<BillPaidModalProps> = (props) => { type BillTypeListProps = {
onClose?: (refresh?: boolean) => void;
onChange?: (confirms: ConfirmedBillDetail[]) => void;
}
export const BillTypeList: React.FC<BillTypeListProps> = (props) => {
const {t} = useTranslation() const {t} = useTranslation()
const [state, setState] = useSetState<{
confirmed: ConfirmedBillDetail[]
}>({
confirmed: [{bill_type: '', amount: 0}]
})
const BillTypes = useBillTypes() const BillTypes = useBillTypes()
const [state, setState] = useSetState<{ const onChange = (value: string, index: number, type: 'type' | 'amount') => {
loading?: boolean; if (state.confirmed.length <= index || !value) return;
open?:boolean const confirmed = [...state.confirmed]
}>({}) if (type == 'type') {
confirmed[index].bill_type = value
} else {
const onSubmit = (values: BillUpdateParams) => { confirmed[index].amount = Number(value)
setState({
loading: true
})
console.log(values)
} }
return (<>
<Button onClick={()=>setState({open:true})} theme={'solid'}>{t('bill.import_bill')}</Button>
<Modal
title={t('bill.import_bill')}
visible={state.open}
closeOnEsc={true}
onCancel={()=>setState({open:false})}
footer={null}
width={600}
okText={t('base.confirm')}
maskClosable={false}
>
<Form<BillUpdateParams> onSubmit={onSubmit} initValues={{ setState({confirmed})
payment_channel: 'FLYWIRE', props.onChange?.(confirmed)
payment_method: '', }
merchant_ref: '',
payment_amount: '', const addOrRemove = (index: number) => {
actual_payment_amount: '', // 不允许删除最后一个
}}> if (index > -1 && state.confirmed.length <= 1) return;
<Row gutter={20}> const confirmed = [...state.confirmed, ...(index == -1 ? [{bill_type: '', amount: 0}] : [])]
<Col span={12}> if (index > -1) confirmed.splice(index, 1)
<Form.Select setState({confirmed})
field={t('manual.bill_type')} props.onChange?.(confirmed)
style={{width: '100%'}} }
placeholder={t('base.please_select')}>
return (<div className={'bill-type-list'} style={{marginTop: 10}}>
<Divider>{t('bill.title_bill_detail')}</Divider>
{
state.confirmed.map((item, index) => (
<div key={index} className="confirm-item-btn align-center space-between" style={{marginTop: 20}}>
<Select
value={item.bill_type}
style={{width: 240}}
onChange={v => onChange(String(v), index, 'type')}
placeholder={t('base.please_select_bill_type')}>
{ {
BillTypes.map((it, idx) => ( BillTypes.map((it, idx) => (
<Select.Option key={idx} value={it.label}>{it.label}</Select.Option>)) <Select.Option key={idx} value={it.label}>{it.label}</Select.Option>))
} }
</Form.Select> </Select>
<Space spacing={10}>
<InputNumber
hideButtons precision={2} value={item.amount} type={'number'}
onChange={v => onChange(String(v), index, 'amount')} style={{width: 140}}/>
<Button
disabled={state.confirmed.length <= 1} onClick={() => addOrRemove(index)}
theme={'solid'} type={'secondary'}>{t('base.remove')}</Button>
</Space>
</div>
))
}
<div style={{marginTop: 10, marginBottom: 20}}>
<Button onClick={() => addOrRemove(-1)}>{t('base.add')}</Button>
</div>
</div>)
}
export const AddBillModal: React.FC<BillPaidModalProps> = (props) => {
const {t, i18n} = useTranslation()
const {paymentChannelList, paymentMethodList} = usePaymentChannels();
const [state, setState] = useSetState<{
loading?: boolean;
confirmLoading?: boolean;
open?: boolean;
details: ConfirmedBillDetail[];
errorMessage?: string;
errorConfirmMessage?: string;
errorConfirmOk?: boolean;
values?: CreateBillRecordModel;
}>({
details: []
})
const onSubmit = (values: CreateBillRecordModel) => {
if (state.details.length == 0) {
setState({errorMessage: t('base.validate.error_details_message')})
return;
}
for (const it of state.details) {
if (!it.bill_type || it.amount <= 0) {
setState({errorMessage: t('base.validate.error_details_message')})
return;
}
}
values.details = state.details
values.check_student = true;
values.initiated_paid_date = dayjs(values.initiated_paid_date).format("YYYY-MM-DD")
values.paid_date = dayjs(values.paid_date).format("YYYY-MM-DD")
values.delivered_date = dayjs(values.delivered_date).format("YYYY-MM-DD")
setState({
loading: true, errorMessage: undefined,
values,errorConfirmOk:false
})
addBillRecord(values).then(() => {
setState({open: false})
Toast.success(t('base.add_success'))
props.onConfirm()
}).catch((e: BizError) => {
if (e.code == -50415) { // STUDENT_INFO_NOT_FOUND
// duplicate
setState({errorConfirmMessage: e.message,errorConfirmOk:true})
} else {
setState({errorConfirmMessage: e.message})
}
}).finally(() => {
setState({
loading: false
})
})
//
}
const handleClose = () => {
setState({
loading: false,
open: false,
errorMessage: undefined
})
}
// process confirm
const handleCloseConfirm = () => {
setState({errorConfirmMessage: undefined})
}
const handleConfirmAdd = () => {
if (!state.values) return;
setState({confirmLoading: true})
state.values.check_student = false;
addBillRecord(state.values).then(() => {
setState({
loading: false,
open: false,
errorMessage: undefined,
errorConfirmMessage: undefined,
})
Toast.success(t('base.add_success'))
props.onConfirm()
}).catch(e => {
setState({errorConfirmMessage: e.message})
}).finally(() => {
setState({
confirmLoading: false,
})
})
}
const details = useMemo(() => {
if (!state.values) return;
const {
application_number, payment_channel, payment_method,
student_email, merchant_ref, paid_area,
initiated_paid_date, paid_date, delivered_date
} = state.values
//, span: 2
const _data: Data[] = [
{key: 'Merchant Ref', value: merchant_ref},
{key: t('bill.bill_number'), value: application_number},
{key: 'Email', value: student_email},
{key: t('bill.title_pay_channel'), value: payment_channel},
{key: t('bill.title_pay_method'), value: payment_method},
{key: t('bill.create.pay_area'), value: paid_area},
{key: t('bill.title_initiated_paid_at'), value: initiated_paid_date},
{key: t('bill.title_paid_at'), value: paid_date},
{key: t('bill.title_delivered_at'), value: delivered_date, span: 2},
]
state.details.forEach(it => {
_data.push({key: t('bill.title_bill_type'), value: it.bill_type})
_data.push({key: t('bill.title_amount'), value: <MoneyFormat money={it.amount}/>})
});
return _data
}, [i18n.language, state.values]);
return (<>
<Button onClick={() => setState({open: true})} theme={'solid'}>{t('bill.add_bill_record')}</Button>
<Modal
title={t('bill.add_bill_record')}
visible={state.open}
closeOnEsc={true}
onCancel={handleClose}
footer={null}
width={600}
maskClosable={false}
>
<Form<CreateBillRecordModel> onSubmit={onSubmit} initValues={{
merchant_ref: '',
application_number: '',
student_email: '',
paid_area: '',
initiated_paid_date: '',
paid_date: '',
delivered_date: '',
payment_method: '',
payment_channel: '',
check_student: true,
details: []
}}>
<Row gutter={20}>
<Col span={12}>
<Form.Input
rules={[
{required: true, message: 'required error'},
]}
showClear field="merchant_ref" label="Merchant Ref"
placeholder={t('base.please_enter')} style={{width: '100%'}}/>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Form.Input <Form.Input
@ -68,70 +258,128 @@ export const AddBillModal: React.FC<BillPaidModalProps> = (props) => {
<Row gutter={20}> <Row gutter={20}>
<Col span={12}> <Col span={12}>
<Form.Input <Form.Input
type={'number'} rules={[
{required: true, message: 'required error'},
{type: 'email', message: t('base.validate.email')}
]}
type={'email'}
showClear field="student_email" label="Email"
placeholder={t('base.please_enter')} style={{width: '100%'}}/>
</Col>
<Col span={12}>
<Form.Input
rules={[ rules={[
{required: true, message: 'required error'}, {required: true, message: 'required error'},
]} ]}
showClear field="amount" label={t('bill.title_amount')} showClear field="paid_area" label={t('bill.create.pay_area')}
placeholder={t('base.please_enter')} style={{width: '100%'}}/> placeholder={t('base.please_enter')} style={{width: '100%'}}/>
</Col> </Col>
</Row>
<Row gutter={20}>
<Col span={12}>
<Form.Select
rules={[
{required: true, message: 'required error'},
]}
optionList={paymentChannelList}
allowCreate filter showClear field="payment_channel" label={t('bill.title_pay_channel')}
placeholder={t('base.please_select')} style={{width: '100%'}}/>
</Col>
<Col span={12}> <Col span={12}>
<Form.Select <Form.Select
rules={[ rules={[
{required: true, message: 'required error'}, {required: true, message: 'required error'},
]} ]}
optionList={[ optionList={paymentMethodList}
{value: 'card', label: 'Card(VISA,MasterCard,UnionPay,JCB...)'},
{value: 'wechat', label: 'Wechat'},
{value: 'alipay', label: 'Alipay'},
{value: 'other', label: 'Other'},
]}
allowCreate filter showClear field="payment_method" label={t('bill.title_pay_method')} allowCreate filter showClear field="payment_method" label={t('bill.title_pay_method')}
placeholder={t('base.please_select')} style={{width: '100%'}}/> placeholder={t('base.please_select')} style={{width: '100%'}}/>
</Col> </Col>
</Row> </Row>
<Row gutter={20}> <Row gutter={20}>
<Col span={12}> <Col span={12}>
<Form.Input <Form.DatePicker
showClear
rules={[ rules={[
{required: true, message: 'required error'}, {required: true, message: 'required error'},
]} ]}
showClear field="merchant_ref" label="Merchant Ref" type={'date'}
placeholder={t('base.please_enter')} style={{width: '100%'}}/> field="initiated_paid_date"
label={t('bill.title_initiated_paid_at')}
style={{width: '100%'}}
/>
</Col> </Col>
<Col span={6}> <Col span={12}>
<Form.Input <Form.DatePicker
showClear
rules={[ rules={[
{required: true, message: 'required error'}, {required: true, message: 'required error'},
]} type={'number'} showClear ]}
field="payment_amount" label={t('bill.title_pay_amount')} type={'date'}
placeholder={t('base.please_enter')} style={{width: '100%'}}/> field="paid_date"
</Col> label={t('bill.title_paid_at')}
<Col span={6}> style={{width: '100%'}}
<Form.Input />
type={'number'}
showClear field="actual_payment_amount" label={t('bill.title_actual_payment_amount')}
placeholder={t('base.please_enter')} style={{width: '100%'}}/>
</Col> </Col>
</Row> </Row>
<Row gutter={20}> <Row gutter={20}>
<Col span={24}> <Col span={12}>
<Form.TextArea <Form.DatePicker
rules={[{required: true, message: 'required error'}]} showClear
showClear field="remark" label={t('bill.title_remark')} placeholder={t('base.please_enter')} rules={[
style={{width: '100%'}}/> {required: true, message: 'required error'},
]}
type={'date'}
field="delivered_date"
label={t('bill.title_delivered_at')}
style={{width: '100%'}}
/>
</Col> </Col>
</Row> </Row>
{/*<p style={{marginTop: 10}}>{t('bill.paid_confirm')}</p>*/} <div>
<div className={'text-right'} style={{margin: '10px 0 20px'}}> <BillTypeList onChange={(details) => setState({details})}/>
{state.errorMessage &&
<div className="semi-form-field-error-message align-center" style={{marginBottom: 10}}>
<IconAlertCircle/>
<span style={{marginLeft: 5}}>{state.errorMessage}</span>
</div>}
<Divider/>
</div>
<div className={'text-right'} style={{margin: '20px 0'}}>
<Space spacing={12}> <Space spacing={12}>
<Button onClick={props.onCancel} type={'tertiary'}>{t('base.cancel')}</Button> <Button onClick={handleClose} type={'tertiary'}>{t('base.cancel')}</Button>
<Button <Button
loading={state.loading} htmlType={'submit'} theme={'solid'} loading={state.loading} htmlType={'submit'} theme={'solid'}
type={'primary'}>{t('base.confirm_paid')}</Button> type={'primary'}>{t('bill.create.confirm')}</Button>
</Space> </Space>
</div> </div>
</Form> </Form>
</Modal> </Modal>
<Modal
visible={!!state.errorConfirmMessage}
title={t('base.confirm_information')}
closeOnEsc={true}
onCancel={handleCloseConfirm}
width={600}
maskClosable={false}
footer={
<div className={'text-center'}>
<Space>
<Button onClick={handleCloseConfirm} type={'tertiary'}>{t('base.cancel')}</Button>
{state.errorConfirmOk && <Button loading={state.confirmLoading} onClick={handleConfirmAdd} theme={'solid'}>
{t('base.confirm_and_add')}
</Button>}
</Space>
</div>
}
>
<Descriptions layout='horizontal' align='plain' data={details} column={2}/>
<Divider style={{margin: '10px 0'}}>{t('base.title_error_tip')}</Divider>
{state.errorConfirmMessage &&
<div className="semi-form-field-error-message align-center" style={{marginBottom: 10}}>
<IconAlertCircle/>
<span style={{marginLeft: 5}}>{state.errorConfirmMessage}</span>
</div>}
</Modal>
</>) </>)
} }

View File

@ -163,9 +163,12 @@ export const BillTypeConfirmModal: React.FC<BillTypeConfirmProps> = (props) => {
</div> </div>
<BillTypeConfirm bill={props.bill} onChange={detail_confirms => setState({detail_confirms})}/> <BillTypeConfirm bill={props.bill} onChange={detail_confirms => setState({detail_confirms})}/>
<div className={'text-center'} style={{paddingBottom: 20,marginTop:20}}> <div className={'text-center'} style={{paddingBottom: 20,marginTop:20}}>
<Space>
<Button onClick={() => props.onClose?.()}>{t('base.cancel')}</Button>
<Button <Button
onClick={onBillConfirm} loading={state.loading} type={'primary'} onClick={onBillConfirm} loading={state.loading} type={'primary'}
theme={'solid'}>{t('base.confirm')}</Button> theme={'solid'}>{t('base.confirm')}</Button>
</Space>
</div> </div>
</Modal>) </Modal>)
} }

View File

@ -0,0 +1,162 @@
import {Button, Modal, Select, Space, Upload} from "@douyinfe/semi-ui";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {useSetState} from "ahooks";
import {usePaymentChannels} from "@/hooks/usePaymentChannels.ts";
import {IconUpload} from "@douyinfe/semi-icons";
import readXlsxFile from "read-excel-file";
import dayjs from "dayjs";
import {OnChangeProps} from "@douyinfe/semi-ui/lib/es/upload/interface";
import Alert from "@/components/alert";
import {uploadBillingRecordFile} from "@/service/api/bill.ts";
type BillPaidModalProps = {
onConfirm: () => void
onCancel?: () => void
}
// type ImportRecordValue = number | string | null | Date;
export const ImportBillModal: React.FC<BillPaidModalProps> = (props) => {
const {t, i18n} = useTranslation()
const {paymentChannelList} = usePaymentChannels()
const [state, setState] = useSetState<{
loading?: boolean;
open?: boolean;
errorMessage?:string;
currentFile?: File;
paymentChannel?: string;
}>({})
const [records, setImportRecords] = useState<string[][]>([])
const closeModal = () => {
setImportRecords([])
setState({open: false, loading: false,errorMessage:undefined})
}
const onSubmit = () => {
if(!state.paymentChannel){
setState({errorMessage:'bill.import.error_require_payment_channel'})
return;
}
if(!state.currentFile){
setState({errorMessage:'bill.import.error_require_file'})
return;
}
if(records.length == 0){
setState({errorMessage:'bill.import.error_require_records'})
return;
}
setState({
loading: true,
errorMessage:undefined
})
uploadBillingRecordFile(state.currentFile,state.paymentChannel).then(()=>{
closeModal()
props.onConfirm()
}).catch(e => {
setState({errorMessage: e.message,loading: false})
})
}
const onFileChange = ({fileList, currentFile}: OnChangeProps) => {
if (fileList.length == 0) {
setImportRecords([])
return;
}
if(!currentFile.fileInstance || currentFile.fileInstance.size < 10){
setState({errorMessage:'bill.import.error_require_file'})
return;
}
if (currentFile.fileInstance) {
readXlsxFile(currentFile.fileInstance).then(rows => {
const records = rows.map((row) => {
return row.map((item) => {
if (item && item instanceof Date) return dayjs(item).format('YYYY-MM-DD HH:mm:ss');
return String(item)
})
})
if(records.length == 0){
setState({errorMessage:'bill.import.error_require_records'})
return;
}
setImportRecords(records)
setState({currentFile:currentFile.fileInstance})
}).catch(e=>{
console.log('parse error',e)
setState({errorMessage:'bill.import.error_require_file'} )
})
}
}
return (<>
<Button onClick={() => setState({open: true})} theme={'solid'}>{t('bill.import_bill')}</Button>
<Modal
title={t('bill.import_bill')}
visible={state.open}
closeOnEsc={true}
onCancel={closeModal}
footer={null}
width={650}
okText={t('base.confirm')}
maskClosable={false}
>
<Space vertical align={'start'} spacing={20}>
<Space>
<div style={{
width: i18n.language == 'en-US' ? 140 : 'auto',
textAlign: 'right'
}}>{t('bill.title_pay_channel')}
</div>
<Select
style={{width: 160}} placeholder={t('base.please_select')}
optionList={paymentChannelList}
onChange={v=>setState({paymentChannel: String(v)})}
/>
</Space>
<Space>
<div style={{
width: i18n.language == 'en-US' ? 140 : 'auto',
textAlign: 'right'
}}>{t('base.select_excel_file')}
</div>
<div className={"upload-wrapper"}>
<Upload
onChange={e => onFileChange(e)}
accept={'.xlsx,.csv'} uploadTrigger="custom" action={''}
>
<Button style={{width: 160}} icon={<IconUpload/>}
theme="light">{t('base.select_upload_file')}</Button>
</Upload>
</div>
</Space>
</Space>
<div className="import-record-wrapper">
<div className="table-list">
<table>
<tbody>
{records.map((record, index) => {
return <tr key={index}>
{record.map((item, index) => {
return <td key={index}>
<div className={'import-record-item'}>{item}</div>
</td>
})}
</tr>
})}
</tbody>
</table>
</div>
</div>
<Alert message={state.errorMessage?t(state.errorMessage):undefined} />
<div className={'text-right'} style={{margin: '50px 0 20px'}}>
<Space spacing={12}>
<Button onClick={closeModal} type={'tertiary'}>{t('base.cancel')}</Button>
<Button
loading={state.loading} htmlType={'submit'} theme={'solid'}
type={'primary'} onClick={onSubmit} disabled={records.length == 0}
>{t('base.confirm_import')}</Button>
</Space>
</div>
</Modal>
</>)
}

View File

@ -2,19 +2,21 @@ import {Button, ButtonGroup, Modal, Notification, Popconfirm, Space, Toast} from
import {useState} from "react"; import {useState} from "react";
import {useRequest, useSetState} from "ahooks"; import {useRequest, useSetState} from "ahooks";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import {saveAs} from "file-saver";
import {BillList} from "@/components/bill/list.tsx"; import {BillList} from "@/components/bill/list.tsx";
import SearchForm from "@/components/bill/search-form.tsx"; import SearchForm from "@/components/bill/search-form.tsx";
import BillDetail from "@/components/bill/detail.tsx"; import BillDetail from "@/components/bill/detail.tsx";
import {billList, BillQueryParams, exportBillList, modifyBillStatus} from "@/service/api/bill.ts"; import {billList, BillQueryParams, cancelConfirmBill, exportBillList, modifyBillStatus} from "@/service/api/bill.ts";
import {BillStatus, BizError} from "@/service/types.ts"; import {BillStatus, BizError} from "@/service/types.ts";
import {useDownloadReceiptPDF} from "@/service/generate-pdf.ts"; import {useDownloadReceiptPDF} from "@/service/generate-pdf.ts";
import {BillPaidModal} from "@/pages/bill/components/bill_paid_modal.tsx"; import {BillPaidModal} from "@/pages/bill/components/bill_paid_modal.tsx";
import {BillTypeConfirmModal} from "@/pages/bill/components/bill_type_confirm.tsx"; import {BillTypeConfirmModal} from "@/pages/bill/components/bill_type_confirm.tsx";
import {saveAs} from "file-saver";
// import {AddBillModal} from "@/pages/bill/components/add_bill_modal.tsx"; // import {AddBillModal} from "@/pages/bill/components/add_bill_modal.tsx";
import {BillTypeConfirmBatch} from "@/pages/bill/components/bill_type_confirm_batch.tsx"; import {BillTypeConfirmBatch} from "@/pages/bill/components/bill_type_confirm_batch.tsx";
import {AddBillModal} from "@/pages/bill/components/add_bill_modal.tsx"; import {AddBillModal} from "@/pages/bill/components/add_bill_modal.tsx";
import {ImportBillModal} from "@/pages/bill/components/import_bill_modal.tsx";
import {PermissionCheck} from "@/components/permission";
const DownloadButton = ({bill, text}: { bill: BillModel; text: string }) => { const DownloadButton = ({bill, text}: { bill: BillModel; text: string }) => {
@ -28,12 +30,15 @@ const DownloadButton = ({bill, text}: { bill: BillModel; text: string }) => {
// } // }
const BillQuery = () => { const BillQuery = () => {
// const {createPDF,downloadPDF} = useDownloadReceiptPDF()
const [state, setState] = useSetState<{ const [state, setState] = useSetState<{
updateBill?: BillModel updateBill?: BillModel;
confirmBill?: BillModel confirmBill?: BillModel;
previewPDFUrl?: string;
updateLoading?: boolean updateLoading?: boolean
exporting?: boolean exporting?: boolean;
confirmBillId: number confirmBillId: number;
}>({confirmBillId: 0}) }>({confirmBillId: 0})
const [showBill, setShowBill] = useState<BillModel>() const [showBill, setShowBill] = useState<BillModel>()
const [queryParams, setBillQueryParams] = useState<BillQueryParams>({ const [queryParams, setBillQueryParams] = useState<BillQueryParams>({
@ -42,8 +47,8 @@ const BillQuery = () => {
}); });
const {data, loading, refresh} = useRequest(() => billList(queryParams), { const {data, loading, refresh} = useRequest(() => billList(queryParams), {
refreshDeps: [queryParams], refreshDeps: [queryParams],
onSuccess:()=>{ onSuccess: () => {
document.documentElement.scrollTo({top:0}); document.documentElement.scrollTo({top: 0});
}, },
onError: (e) => { onError: (e) => {
Notification.error({title: 'Error', content: e.message}) Notification.error({title: 'Error', content: e.message})
@ -63,12 +68,35 @@ const BillQuery = () => {
}) })
} }
const onCancelBillConfirm = (id: number) => {
cancelConfirmBill(id).then(() => {
Toast.success({
content: `${t('base.operate_success')}`,
duration: 3
})
refresh()
}).catch((e: BizError) => {
Toast.error({
content: `${t('base.operate_fail')}:${e.message}`,
duration: 3
})
})
}
// const showBillPDF = (bill: BillModel) => {
// setState({
// previewPDFUrl: createPDF(bill).output('bloburi').toString()
// })
// }
const operation = (bill: BillModel) => { const operation = (bill: BillModel) => {
return (<div className={'table-operation-render'}> return (<div className={'table-operation-render'}>
<PermissionCheck permission={'complete_button'}>
{bill.status != BillStatus.PAID && {bill.status != BillStatus.PAID &&
<Button onClick={() => setState({updateBill: bill})} size={'small'} theme={'solid'} <Button onClick={() => setState({updateBill: bill})} size={'small'} theme={'solid'}
type={'danger'}>{t('bill.paid')}</Button> type={'danger'}>{t('bill.paid')}</Button>
} }
</PermissionCheck>
{bill.status == BillStatus.PENDING && <> {bill.status == BillStatus.PENDING && <>
<Popconfirm <Popconfirm
title={'Notice'} onConfirm={() => onCancelBill(bill)} position={'topRight'} title={'Notice'} onConfirm={() => onCancelBill(bill)} position={'topRight'}
@ -79,15 +107,28 @@ const BillQuery = () => {
<Button <Button
onClick={() => setShowBill(bill)} size={'small'} theme={'solid'} onClick={() => setShowBill(bill)} size={'small'} theme={'solid'}
type={'primary'}>{t('base.qr-code')}</Button> type={'primary'}>{t('base.qr-code')}</Button>
{AppMode == 'development' && <a href={`/pay?bill=${bill.id}`} target={'_blank'}></a>}
</>} </>}
{ {
bill.status == BillStatus.PAID && <> bill.status == BillStatus.PAID && <>
<DownloadButton bill={bill} text={t('bill.download_receipt')}/> <DownloadButton bill={bill} text={t('bill.download_receipt')}/>
{bill.confirm_status == 'UNCONFIRMED' && <Button {/*<Button*/}
{/* onClick={() => showBillPDF(bill)} size={'small'}>download pdf</Button>*/}
<PermissionCheck permission={'confirm_button'}>
{
bill.confirm_status == 'UNCONFIRMED'
? <Button
onClick={() => setState({confirmBill: bill})} onClick={() => setState({confirmBill: bill})}
size={'small'} theme={'solid'} size={'small'} theme={'solid'}
type={'primary'}>{t('bill.confirm_bill_type')}</Button>} type={'primary'}>{t('bill.confirm_bill_type')}</Button>
: <Popconfirm
title={'Warning'} onConfirm={() => onCancelBillConfirm(bill.id)} position={'topRight'}
content={`${t('base.confirm_next_operation')}?`}
>
<Button size={'small'} theme={'solid'} type={'danger'}>{t('bill.btn_cancel_confirm')}</Button>
</Popconfirm>
}
</PermissionCheck>
</> </>
} }
</div>) </div>)
@ -111,9 +152,7 @@ const BillQuery = () => {
}) })
} }
const onImportExcel = () => {
Toast.warning({content: 'Not implemented'})
}
const [selectKeys, setSelectedKeys] = useState<number[]>([]) const [selectKeys, setSelectedKeys] = useState<number[]>([])
@ -131,7 +170,8 @@ const BillQuery = () => {
<BillTypeConfirmBatch data={data} selectKeys={selectKeys} onConfirm={refresh}/> <BillTypeConfirmBatch data={data} selectKeys={selectKeys} onConfirm={refresh}/>
<AddBillModal onConfirm={refresh}/> <AddBillModal onConfirm={refresh}/>
<ButtonGroup style={{marginRight: 20}} theme={'solid'}> <ButtonGroup style={{marginRight: 20}} theme={'solid'}>
<Button onClick={onImportExcel}>{t('bill.import_excel')}</Button> {/*<Button onClick={onImportExcel}>{t('bill.import_excel')}</Button>*/}
<ImportBillModal onConfirm={refresh}/>
<Button loading={state.exporting} onClick={onExportExcel}>{t('bill.export_excel')}</Button> <Button loading={state.exporting} onClick={onExportExcel}>{t('bill.export_excel')}</Button>
</ButtonGroup> </ButtonGroup>
</Space>} </Space>}
@ -162,6 +202,21 @@ const BillQuery = () => {
> >
{showBill && <BillDetail bill={showBill} onCancel={() => setShowBill(undefined)}/>} {showBill && <BillDetail bill={showBill} onCancel={() => setShowBill(undefined)}/>}
</Modal> </Modal>
{/* preview and download pdf */}
{/*<Modal*/}
{/* title={'PDF Preview'} width={800} visible={!!state.previewPDFUrl}*/}
{/* maskClosable={false} onCancel={() => setState({previewPDFUrl: undefined})}*/}
{/* onOk={()=>{*/}
{/* if(state.previewPDFUrl) {*/}
{/* saveAs(state.previewPDFUrl, 'pdf.pdf')*/}
{/* }*/}
{/* }}*/}
{/*>*/}
{/* {state.previewPDFUrl && <div className={'bill-pdf-previewer'}>*/}
{/* <iframe allowFullScreen={false} src={state.previewPDFUrl + '#view=FitH,top&toolbar=0'}*/}
{/* className={'bill-pdf-container'}/>*/}
{/* </div>}*/}
{/*</Modal>*/}
<BillPaidModal <BillPaidModal
open={!!state.updateBill} open={!!state.updateBill}
onCancel={() => setState({updateBill: undefined})} onCancel={() => setState({updateBill: undefined})}

View File

@ -7,8 +7,10 @@ import SearchForm from "@/components/bill/search-form.tsx";
import {BillList} from "@/components/bill/list.tsx"; import {BillList} from "@/components/bill/list.tsx";
import {billList, BillQueryParams, confirmBills} from "@/service/api/bill.ts"; import {billList, BillQueryParams, confirmBills} from "@/service/api/bill.ts";
import {BizError} from "@/service/types.ts"; import {BizError} from "@/service/types.ts";
import useAuth from "@/hooks/useAuth.ts";
const BillReconciliation = () => { const BillReconciliation = () => {
const {user} = useAuth()
const {t} = useTranslation() const {t} = useTranslation()
const [queryParams, setBillQueryParams] = useState<BillQueryParams>({ const [queryParams, setBillQueryParams] = useState<BillQueryParams>({
apply_status: 'UNCHECKED' apply_status: 'UNCHECKED'
@ -108,8 +110,10 @@ const BillReconciliation = () => {
/> />
<BillList <BillList
source={data} type={'reconciliation'} source={data} type={'reconciliation'}
operationRender={queryParams.apply_status == 'CHECKED' ? undefined : operation} operationRender={queryParams.apply_status == 'CHECKED' ? undefined : (
beforeTotalAmount={<div>{queryParams.apply_status != 'CHECKED' && ( user && user.permissions?.apply_button ? operation : undefined
)}
beforeTotalAmount={<div>{queryParams.apply_status != 'CHECKED' && user && !!user.permissions?.apply_button && (
(selectKeys.length == 0) ? <Button theme={'solid'} disabled style={{marginRight: 10}}> (selectKeys.length == 0) ? <Button theme={'solid'} disabled style={{marginRight: 10}}>
{t('bill.confirm_batch')} {t('bill.confirm_batch')}
</Button> : </Button> :

View File

@ -12,7 +12,7 @@ import {IconExit, IconUser} from "@douyinfe/semi-icons";
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import useConfig from "@/hooks/useConfig.ts"; import useConfig from "@/hooks/useConfig.ts";
import {IconRoles} from "@/components/icons"; import {IconRoles} from "@/components/icons";
import {setCurrentRole} from "@/contexts/auth"; // import {setCurrentRole} from "@/contexts/auth";
const {Header, Content, Sider} = Layout; const {Header, Content, Sider} = Layout;
@ -42,15 +42,10 @@ export const HeaderUserAvatar = () => {
<Avatar color="orange" size="default"><IconUser /></Avatar> <Avatar color="orange" size="default"><IconUser /></Avatar>
<div> <div>
<Typography.Title heading={6}>{user?.username}</Typography.Title> <Typography.Title heading={6}>{user?.username}</Typography.Title>
<div style={{maxWidth:250,overflow:'hidden',whiteSpace:'nowrap',textOverflow:'ellipsis'}}> <div title={user?.department} style={{maxWidth:150,overflow:'hidden',whiteSpace:'nowrap',textOverflow:'ellipsis'}}>
<Typography.Text <Typography.Text
type="quaternary" type="quaternary"
size={'small'}>Department:{user?.department?.toUpperCase() || "N/A"}</Typography.Text> size={'small'}>{user?.department?.toUpperCase() || "N/A"}</Typography.Text>
</div>
<div>
<Typography.Text
type="quaternary"
size={'small'}>Role:{user?.role?.toUpperCase()}</Typography.Text>
</div> </div>
</div> </div>
</Space> </Space>
@ -66,36 +61,23 @@ export const HeaderUserAvatar = () => {
<Avatar color="orange" size="small"><IconUser /></Avatar> <Avatar color="orange" size="small"><IconUser /></Avatar>
</Dropdown>) </Dropdown>)
} }
const RoleList: UserRole[] = ['root', 'ro', 'fo','staff'] // const RoleList: UserRole[] = ['root', 'ro', 'fo','staff']
const RoleSwitcher = ()=>{ const RoleSwitcher = ()=>{
const {user, updateUser} = useAuth() const {user} = useAuth()
// update user role // update user role
const changeRole = (role:UserRole)=>{ // const changeRole = (role:UserRole)=>{
updateUser({role}).then(()=>{ // updateUser({role}).then(()=>{
setCurrentRole(role) // setCurrentRole(role)
}) // })
} // }
return (<> return (<>
{user?.origin_role == 'root' && (<Dropdown
clickToHide
render={
<Dropdown.Menu>
{RoleList.map((key) => (
<Dropdown.Item
active={user?.role == key} key={key}
onClick={() => changeRole(key)}
><span>{key.toUpperCase()}</span></Dropdown.Item>
))}
</Dropdown.Menu>
}
>
<Button theme="borderless"> <Button theme="borderless">
<Space style={{transform:'translateY(3px)'}}> <Space style={{transform:'translateY(3px)'}}>
<IconRoles size={20} color={'white'} /> <IconRoles size={20} color={'white'} />
<span style={{color:'white'}}>{user?.role?.toUpperCase()}</span> {/*{JSON.stringify(user)}*/}
<span style={{color:'white'}}>{user?.permissions?.role.toUpperCase()}</span>
</Space> </Space>
</Button> </Button>
</Dropdown>) }
</>) </>)
} }
export const CommonHeader: React.FC<CommonHeaderProps> = ({children, title, rightExtra}) => { export const CommonHeader: React.FC<CommonHeaderProps> = ({children, title, rightExtra}) => {

View File

@ -10,24 +10,25 @@ export const AllDashboardMenu = [
key: 'manual', key: 'manual',
icon: <IconQRCode/>, icon: <IconQRCode/>,
path: '/dashboard/manual', path: '/dashboard/manual',
permission: 'manual_payment',
}, },
{ {
key: 'bill', key: 'bill',
icon: <IconQuery/>, icon: <IconQuery/>,
path: '/dashboard/bill', path: '/dashboard/bill',
role: ['root', 'ro', 'fo'] permission: 'bill_page',
}, },
{ {
key: 'check', key: 'check',
icon: <IconReconciliation/>, icon: <IconReconciliation/>,
path: '/dashboard/reconciliation', path: '/dashboard/reconciliation',
role: ['root', 'fo'] permission: 'apply_page',
}, },
{ {
key: 'permission', key: 'permission',
icon: <IconPermission/>, icon: <IconPermission/>,
path: '/dashboard/permission', path: '/dashboard/permission',
role: ['root'] permission: 'permission_edit',
} }
] ]
@ -36,10 +37,11 @@ export function DashboardNavigation() {
const {user} = useAuth(); const {user} = useAuth();
const navItems = useMemo(() => { const navItems = useMemo(() => {
if (!user) return []; if (!user || !user.permissions) return [];
return AllDashboardMenu.filter(it => { return AllDashboardMenu.filter(it => {
return !it.role || it.role.includes(user.role) if(user.permissions && user.permissions[it.permission]) return true;
return false
}); });
}, [user]) }, [user])
return (<div className={'dashboard-menu-container'}> return (<div className={'dashboard-menu-container'}>

View File

@ -1,7 +1,9 @@
import {get, post, put} from "@/service/request.ts"; import {get, post, put, uploadFile} from "@/service/request.ts";
import dayjs from "dayjs"; import dayjs from "dayjs";
import {getAuthToken} from "@/hooks/useAuth.ts"; import {getAuthToken} from "@/hooks/useAuth.ts";
import {stringify} from "qs"; import {stringify} from "qs";
import utc from "dayjs/plugin/utc"
dayjs.extend(utc)
export type BillQueryResult = { export type BillQueryResult = {
result: BillModel[]; result: BillModel[];
@ -37,18 +39,19 @@ export function exportBillList(params: BillQueryParams) {
'Token': token || '' 'Token': token || ''
}); });
return fetch(`${AppConfig.API_PREFIX || '/api'}/bills/export?${stringify(params)}`,{ return fetch(`${AppConfig.API_PREFIX || '/api'}/bills/export?${stringify(params)}`, {
headers, headers,
method:'GET' method: 'GET'
}).then(r=>r.blob()) }).then(r => r.blob())
// return get<Blob>('/bills/export', params,true) // return get<Blob>('/bills/export', params,true)
} }
//现场支付创建账单接口 //现场支付创建账单接口
export function createManualBill(params: ManualCreateBillParam) { export function createManualBill(params: ManualCreateBillParam) {
return post<BillModel>('/manual_payment', params) return post<BillModel>('/manual_payment', params)
} }
export function selectBillTypeList(){ export function selectBillTypeList() {
return get<BillType[]>('/billing_types') return get<BillType[]>('/billing_types')
} }
@ -57,6 +60,14 @@ export function getBillDetail(id: number) {
return get<BillModel>('/bills/' + id) return get<BillModel>('/bills/' + id)
} }
export function addBillRecord(bill: CreateBillRecordModel) {
return post('/add_bill', bill);
}
export function uploadBillingRecordFile(file: File, payment_channel: string, check_student = 'true') {
return uploadFile('/bills/import', file, {payment_channel, check_student})
}
export function getAsiaPayData(id: number) { export function getAsiaPayData(id: number) {
return get<AsiaPayModel>(`/bills/${id}/asiapay`) return get<AsiaPayModel>(`/bills/${id}/asiapay`)
} }
@ -66,14 +77,18 @@ export function getFlywirePayUrl(id: number, return_cta: string, return_cta_name
} }
// 作废订单 // 作废订单
export function modifyBillStatus(id: number,status: BillStatus) { export function modifyBillStatus(id: number, status: BillStatus) {
return put(`/bills/${id}/cancel`,{status}) return put(`/bills/${id}/cancel`, {status})
} }
export function confirmBillType(bills:BillConfirmParams[]) { export function confirmBillType(bills: BillConfirmParams[]) {
return post<BillModel>(`/bills/confirm`, {bills}) return post<BillModel>(`/bills/confirm`, {bills})
} }
export function cancelConfirmBill(bill_id: number) {
return post<BillModel>(`/bill/cancel_confirm`, {bill_id})
}
export function confirmBills(bill_ids: number[] | string[]) { export function confirmBills(bill_ids: number[] | string[]) {
return post(`/bills/apply`, {bill_ids}) return post(`/bills/apply`, {bill_ids})
} }
@ -88,22 +103,23 @@ export function createExternalBill(params: ExternalCreateParamsType) {
} }
type BillUpdateFormParams = { type BillUpdateFormParams = {
bill:BillModel; bill: BillModel;
param:BillUpdateParams param: BillUpdateParams
} }
export async function finishAsiapay({param}: BillUpdateFormParams){
export async function finishAsiapay({param}: BillUpdateFormParams) {
const paramUrl = `?prc=0&src=0&Ord=12345678&Ref=${param.merchant_ref}&PayRef=123456&successcode=0&Amt=10.00&Cur=344&Holder=Test Card&AuthId=123456&AlertCode=&remark= const paramUrl = `?prc=0&src=0&Ord=12345678&Ref=${param.merchant_ref}&PayRef=123456&successcode=0&Amt=10.00&Cur=344&Holder=Test Card&AuthId=123456&AlertCode=&remark=
&eci=07&payerAuth=U&sourceIp=192.1.1.1&ipCountry=HK&payMethod=VISA &eci=07&payerAuth=U&sourceIp=192.1.1.1&ipCountry=HK&payMethod=VISA
x&cardIssuingCountry=HK&channelType=SPC&` x&cardIssuingCountry=HK&channelType=SPC&`
const ret = await post<string>(`/flywire/feedback?${paramUrl}`, {},true) const ret = await post<string>(`/asiapay/feedback?${paramUrl}`, {}, true)
return ret?.toLowerCase() == 'ok' return ret?.toLowerCase() == 'ok'
} }
export async function finishFlywire({bill,param}: BillUpdateFormParams){ export async function finishFlywire({bill, param}: BillUpdateFormParams) {
const eventData = { const eventData = {
"event_type": "guaranteed", "event_type": "guaranteed",
"event_date": dayjs().format('YYYY-MM-DDTHH:mm:ss[Z]'), "event_date": dayjs().utc().format('YYYY-MM-DDTHH:mm:ss[Z]'),
"event_resource": "payments", "event_resource": "payments",
"data": { "data": {
"remark": param.remark, "remark": param.remark,
@ -113,7 +129,7 @@ export async function finishFlywire({bill,param}: BillUpdateFormParams){
"amount_to": Number(param.payment_amount) * 100, "amount_to": Number(param.payment_amount) * 100,
"currency_to": "HKD", "currency_to": "HKD",
"status": "guaranteed", "status": "guaranteed",
"expiration_date": dayjs().format('YYYY-MM-DDTHH:mm:ss[Z]'), "expiration_date": dayjs().utc().format('YYYY-MM-DDTHH:mm:ss[Z]'),
"external_reference": bill.id, "external_reference": bill.id,
"country": "CN", "country": "CN",
"payment_method": { "payment_method": {
@ -130,13 +146,13 @@ export async function finishFlywire({bill,param}: BillUpdateFormParams){
} }
} }
} }
const ret = await post<string>('/flywire/feedback', eventData,true) const ret = await post<string>('/flywire/feedback', eventData, true)
return ret?.toLowerCase() == 'ok' return ret?.toLowerCase() == 'ok'
} }
export function finishBill(params: BillUpdateFormParams) { export function finishBill(params: BillUpdateFormParams) {
if(params.param.payment_channel === 'flywire'){ if (params.param.payment_channel.toLowerCase() === 'asiapay') {
return finishAsiapay(params) return finishAsiapay(params)
} }
return finishFlywire(params) return finishFlywire(params)

View File

@ -1,4 +1,4 @@
import {get,post} from "@/service/request.ts"; import {get, post, put, remove} from "@/service/request.ts";
export function getUserInfo() { export function getUserInfo() {
return get<UserProfile>('/userinfo') return get<UserProfile>('/userinfo')
@ -19,3 +19,17 @@ export function getPermissionList(){
export function savePermissionList(roles:PermissionUserList[]){ export function savePermissionList(roles:PermissionUserList[]){
return post('/roles',{roles}) return post('/roles',{roles})
} }
export function getUserPermissionList(){
return get<UserPermission[]>('/permissions')
}
export function createUserPermission(data:UserPermission){
return post<UserPermission>('/permission',data)
}
export function updateUserPermission(data:UserPermission){
return put<UserPermission>(`/permissions/${data.id}`,data)
}
export function removeUserPermission(id:number){
return remove(`/permissions/${id}`)
}

View File

@ -4,17 +4,17 @@ import {useState} from "react";
import dayjs from "dayjs"; import dayjs from "dayjs";
const PDF_Y_START = 40;
function drawItem(doc: JsPDF, item: { function drawItem(doc: JsPDF, item: {
title: string; title: string;
content?: string content?: string
}, y: number, align: 'left' | 'right' = 'left', fontSize: number = 13) { }, y: number, align: 'left' | 'right' = 'left', fontSize: number = 13) {
doc.setFontSize(fontSize); doc.setFontSize(fontSize);
// const width = doc.internal.pageSize.getWidth(); // const width = doc.internal.pageSize.getWidth();
doc.text(item.title, align == 'left' ? 20 : 180, y); doc.text(item.title, align == 'left' ? 30 : 170, y);
if (item.content && item.content.length > 0) doc.text(item.content, align == 'left' ? 65 : 230, y, {maxWidth: 150}); if (item.content && item.content.length > 0) doc.text(item.content, align == 'left' ? 75 : 220, y, {maxWidth: 150});
} }
export function GeneratePdf(bill: BillModel,save = true) {
export function GeneratePdf(bill: BillModel) {
const doc = new JsPDF({ const doc = new JsPDF({
orientation: 'landscape', orientation: 'landscape',
format: 'a4' format: 'a4'
@ -23,27 +23,29 @@ export function GeneratePdf(bill: BillModel) {
// const height = doc.internal.pageSize.getHeight(); // const height = doc.internal.pageSize.getHeight();
doc.setFont('Helvetica', 'normal', 'bold'); doc.setFont('Helvetica', 'normal', 'bold');
doc.setFontSize(20); doc.setFontSize(20);
doc.text('ACKNOWLEDGEMENT RECEIPT', 100, 20, {}); doc.text('ACKNOWLEDGEMENT RECEIPT', 100, PDF_Y_START, {});
doc.setFont('Helvetica', 'normal', 'normal'); doc.setFont('Helvetica', 'normal', 'normal');
drawItem(doc, {title: 'Student Name:', content: bill.student_english_name || bill.student_chinese_name || 'N/A'}, 40) drawItem(doc, {title: 'Student Name:', content: bill.student_english_name || bill.student_chinese_name || 'N/A'}, PDF_Y_START + 10)
drawItem(doc, {title: 'Reference Number:', content: `${bill.id}`}, 40, "right") drawItem(doc, {title: 'Reference Number:', content: `${bill.id}`}, PDF_Y_START + 10, "right")
drawItem(doc, {title: `${bill.student_number?"Student":"Bill"} Number:`, content: `${bill.student_number || bill.application_number}`}, 48) drawItem(doc, {title: `${bill.student_number?"Student":"Bill"} Number:`, content: `${bill.student_number || bill.application_number}`}, PDF_Y_START + 18)
drawItem(doc, {title: 'Print Date:', content: dayjs().format('YYYY-MM-DD')}, 48, "right") drawItem(doc, {title: 'Print Date:', content: dayjs().format('YYYY-MM-DD')}, PDF_Y_START + 18, "right")
const programme_english_name = bill.programme_english_name || ''
drawItem(doc, { drawItem(doc, {
title: 'Programme:', title: 'Programme:',
content: bill.programme_english_name || 'N/A' content: programme_english_name || 'N/A'
}, 56) }, PDF_Y_START + 26)
if(bill.programme_english_name && bill.programme_english_name.length > 0){
drawItem(doc, {title: 'Mode of Study:', content: bill.attendance_mode == 'FT' ? 'FULL-TIME': bill.attendance_mode}, bill.programme_english_name.length > 70?70:64) const programme_limit = programme_english_name.length > 76 ;
if(programme_english_name.length > 0){
drawItem(doc, {title: 'Mode of Study:', content: bill.attendance_mode == 'FT' ? 'FULL-TIME': bill.attendance_mode}, programme_limit? PDF_Y_START + 38 : PDF_Y_START + 34)
} }
// draw table // draw table
autoTable(doc, { autoTable(doc, {
startY: 80, startY: programme_limit ? PDF_Y_START + 44 : PDF_Y_START + 40,
theme: 'grid', theme: 'grid',
margin: {top: 37, left: 30, right: 30},
margin: {top: 37, left: 20, right: 20},
styles: { styles: {
fontSize: 13, fontSize: 13,
fillColor: [255, 255, 255], fillColor: [255, 255, 255],
@ -56,22 +58,25 @@ export function GeneratePdf(bill: BillModel) {
headStyles: { headStyles: {
fontSize: 15, fontSize: 15,
}, },
head: [['No.', 'Transaction Date', 'Payment Type', 'Payment Method', 'HK$']], head: [['No.', 'Transaction Date', 'Payment Type', 'Payment Method', {content:'HK$',styles:{minCellWidth:30,halign:'right'}}]],
body: [ body: [
...(bill.details.map(it=>{ ...(bill.details.map(it=>{
return [ return [
`#${it.id}`, `#${it.id}`,
bill.paid_at?dayjs(bill.paid_at).format('YYYY-MM-DD'):'', bill.paid_at?dayjs(bill.initiated_paid_at).format('YYYY-MM-DD'):'',
it.bill_type, it.bill_type,
`${bill.payment_channel}` + (bill.payment_method && bill.payment_channel != bill.payment_method ? `(${bill.payment_method})` : ''), `${bill.payment_channel}` + (bill.payment_method && bill.payment_channel != bill.payment_method ? `(${bill.payment_method})` : ''),
`${it.amount}` {content:`${it.amount}`,styles:{halign:'right'}} as any
]; ];
})), })),
[{ [{
colSpan: 4, colSpan: 4,
content: 'TOTAL:', content: 'TOTAL:',
styles: {valign: 'middle', halign: 'right'}, styles: {valign: 'middle', halign: 'right',fontStyle: 'bold'},
}, `${bill.amount}`], }, {
content:`${bill.amount}`,
styles:{halign:'right'}
}],
], ],
}) })
// draw foot // draw foot
@ -81,7 +86,8 @@ export function GeneratePdf(bill: BillModel) {
doc.setFont('Helvetica', 'italic'); doc.setFont('Helvetica', 'italic');
drawItem(doc, {title: 'Please retain this acknowledgement receipt for your record.'}, 185) drawItem(doc, {title: 'Please retain this acknowledgement receipt for your record.'}, 185)
doc.save(`Receipt-${bill.id}.pdf`); if(save) doc.save(`Receipt-${bill.id}.pdf`);
return doc
} }
@ -99,6 +105,9 @@ export function useDownloadReceiptPDF() {
return { return {
loading, loading,
downloadPDF downloadPDF,
createPDF(bill: BillModel){
return GeneratePdf(bill,false)
}
} }
} }

View File

@ -27,12 +27,13 @@ Axios.interceptors.request.use(config => {
return Promise.reject(err) return Promise.reject(err)
}) })
export function request<T>(url: string, method: RequestMethod, data: AllType = null, getOriginResult = false) { export function request<T>(url: string, method: RequestMethod, data: AllType = null, getOriginResult = false, headers: RequestHeaders = {}) {
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
Axios.request<APIResponse<T>>({ Axios.request<APIResponse<T>>({
url, url,
method, method,
data, data,
headers
}).then(res => { }).then(res => {
if (res.status != 200) { if (res.status != 200) {
reject(new BizError("Service Internal Exception,Please Try Later!", res.status)) reject(new BizError("Service Internal Exception,Please Try Later!", res.status))
@ -43,11 +44,11 @@ export function request<T>(url: string, method: RequestMethod, data: AllType = n
return; return;
} }
// const // const
const {code, message, data,request_id} = res.data const {code, message, data, request_id} = res.data
if (code == 0) { if (code == 0) {
resolve(data as unknown as T) resolve(data as unknown as T)
} else { } else {
reject(new BizError(message, code,request_id, data as unknown as AllType)) reject(new BizError(message, code, request_id, data as unknown as AllType))
} }
}).catch(e => { }).catch(e => {
reject(new BizError(e.message, 500)) reject(new BizError(e.message, 500))
@ -71,6 +72,62 @@ export function put<T>(url: string, data: AllType = {}) {
return request<T>(url, 'put', data) return request<T>(url, 'put', data)
} }
export function remove<T>(url: string, data: AllType = {}) {
return request<T>(url, 'delete', data)
}
type UploadFileModel = {
field_name?: string;
origin_file: File
}
export function uploadFile<T>(url: string, file: UploadFileModel | File, data: AllType = null, returnOrigin = false) {
const formData = new FormData();
if (file && file instanceof File) {
formData.append('file', file);
} else {
formData.append(file.field_name || 'file', file.origin_file);
}
if (data) {
Object.keys(data).forEach(key => {
formData.append(key, data[key]);
})
}
return request<T>(url, 'post', formData, returnOrigin, {
'Content-Type': 'multipart/form-data'
})
// return new Promise<T>((resolve, reject) => {
// Axios.request<APIResponse<T>>({
// url,
// method,
// data,
// headers:{
// 'Content-Type': 'multipart/form-data'
// }
// }).then(res => {
// if (res.status != 200) {
// reject(new BizError("Service Internal Exception,Please Try Later!", res.status))
// return;
// }
// if (getOriginResult) {
// resolve(res.data as unknown as T)
// return;
// }
// // const
// const {code, message, data,request_id} = res.data
// if (code == 0) {
// resolve(data as unknown as T)
// } else {
// reject(new BizError(message, code,request_id, data as unknown as AllType))
// }
// }).catch(e => {
// reject(new BizError(e.message, 500))
// })
// })
}
export function getFileBlob(url: string) { export function getFileBlob(url: string) {
return new Promise<Blob>((resolve, reject) => { return new Promise<Blob>((resolve, reject) => {
fetch(url).then(res => res.blob()).then(res => { fetch(url).then(res => res.blob()).then(res => {

3
src/types/api.d.ts vendored
View File

@ -1,5 +1,8 @@
// 请求方式 // 请求方式
declare type RequestMethod = 'get' | 'post' | 'put' | 'delete' declare type RequestMethod = 'get' | 'post' | 'put' | 'delete'
declare type RequestHeaders = {
[key:string]:string
}
// 接口返回数据类型 // 接口返回数据类型
declare interface APIResponse<T> { declare interface APIResponse<T> {

21
src/types/auth.d.ts vendored
View File

@ -1,5 +1,18 @@
declare type UserRole = 'root' | 'ro' | 'fo' | 'staff' declare type UserRole = 'root' | 'ro' | 'fo' | 'staff'
declare type UserPermission = {
id: number;
username: string;
role: string;
manual_payment: boolean;
bill_page: boolean;
apply_page: boolean;
complete_button: boolean;
confirm_button: boolean;
apply_button: boolean;
permission_edit: boolean;
}
declare type UserProfile = { declare type UserProfile = {
id: string | number; id: string | number;
token: string; token: string;
@ -14,6 +27,7 @@ declare type UserProfile = {
type: string; type: string;
roles: UserRole[]; roles: UserRole[];
role: UserRole; role: UserRole;
permissions:UserPermission | null;
origin_role?: UserRole; origin_role?: UserRole;
} }
@ -30,11 +44,12 @@ declare type AuthContextType = {
user?: UserProfile | null | undefined; user?: UserProfile | null | undefined;
logout: () => Promise<void>; logout: () => Promise<void>;
mockLogin: () => Promise<void>; mockLogin: () => Promise<void>;
login: (code:string,state:string) => Promise<void>; refreshUserInfo: () => Promise<void>;
updateUser: (user:Partial<UserProfile>) => Promise<void>; login: (code: string, state: string) => Promise<void>;
updateUser: (user: Partial<UserProfile>) => Promise<void>;
}; };
declare type PermissionUserList = { declare type PermissionUserList = {
role_name:string; role_name: string;
username_list: string[]; username_list: string[];
} }

83
src/types/bill.d.ts vendored
View File

@ -1,14 +1,13 @@
declare type BaseDate = number | Date | string | null; declare type BaseDate = number | Date | string | null;
declare type BillStatus = 'PAID' | 'CANCELLED' | 'PENDING' declare type BillStatus = 'PAID' | 'CANCELLED' | 'PENDING'
declare type ConfirmStatus = 'UNCONFIRMED' | 'CONFIRMED' declare type ConfirmStatus = 'UNCONFIRMED' | 'CONFIRMED'
declare type SortOrderType = 'ASC'|'DESC' | string; declare type SortOrderType = 'ASC' | 'DESC' | string;
// 现场支付账单数据 // 现场支付账单数据
declare type ManualCreateBillParam = { declare type ManualCreateBillParam = {
bill_type:string; bill_type: string;
amount:decimal; amount: decimal;
student_number:string; student_number: string;
} }
declare type BillDetail = { declare type BillDetail = {
id: number; id: number;
@ -19,7 +18,7 @@ declare type BillDetail = {
declare type ConfirmedBillDetail = { declare type ConfirmedBillDetail = {
id?: number; id?: number;
bill_detail_id: number; bill_detail_id?: number;
bill_type: string; bill_type: string;
amount: decimal; amount: decimal;
} }
@ -27,40 +26,40 @@ declare type ConfirmedBillDetail = {
* *
*/ */
declare type BillQueryParam = { declare type BillQueryParam = {
page_size:int; page_size: int;
page_number:int; page_number: int;
status:string; status: string;
apply_status:string; apply_status: string;
id:string|number; id: string | number;
merchant_ref:string; merchant_ref: string;
/** /**
* @deprecated * @deprecated
*/ */
bill_type:string; bill_type: string;
confirm_bill_type:string; confirm_bill_type: string;
student_number:string; student_number: string;
application_number:string; application_number: string;
payment_channel:string; payment_channel: string;
/** /**
* @deprecated * @deprecated
*/ */
start_date:string; start_date: string;
/** /**
* @deprecated * @deprecated
*/ */
end_date:string; end_date: string;
start_initiated:string; start_initiated: string;
end_initiated:string; end_initiated: string;
start_delivered:string; start_delivered: string;
end_delivered:string; end_delivered: string;
merchant_ref:string; merchant_ref: string;
/** /**
* @deprecated * @deprecated
*/ */
department:string; department: string;
confirm_status: ConfirmStatus; confirm_status: ConfirmStatus;
sort_field:string; sort_field: string;
sort_order:SortOrderType; sort_order: SortOrderType;
} }
declare type BillType = { declare type BillType = {
type: string; type: string;
@ -87,18 +86,20 @@ declare type BillModel = {
department_english_name: string; department_english_name: string;
intake_year: string; intake_year: string;
intake_semester: string; intake_semester: string;
amount: number|string; amount: number | string;
service_charge?: number; service_charge?: number;
payment_amount?: number; payment_amount?: number;
actual_payment_amount?: null | number; actual_payment_amount?: null | number;
payment_method?: null | string; payment_method?: null | string;
currency?: string; currency?: string;
payment_channel?: null | string ; payment_channel?: null | string;
expiration_time?: BaseDate; expiration_time?: BaseDate;
status: string; status: string;
apply_status: string; apply_status: string;
paid_area?: null | string | number; paid_area?: null | string | number;
paid_at?:BaseDate; paid_at?: BaseDate;
initiated_paid_at?: BaseDate;
delivered_at?: BaseDate;
merchant_ref?: string; merchant_ref?: string;
payment_id?: null | string | number; payment_id?: null | string | number;
create_at: BaseDate; create_at: BaseDate;
@ -140,13 +141,27 @@ type BillUpdateParams = {
payment_amount?: number | string; payment_amount?: number | string;
} }
type CreateBillRecordModel = {
merchant_ref: string;
application_number: string;
student_email: string;
paid_area: string;
initiated_paid_date: string;
paid_date: string;
delivered_date: string;
payment_method: string;
payment_channel: string;
check_student: 'true' | 'false' | string|boolean;
details: ConfirmedBillDetail[]
}
type BillTypeConfirm = { type BillTypeConfirm = {
bill_type: string; bill_type: string;
amount: number; amount: number;
} }
type BillConfirmParams = { type BillConfirmParams = {
id:number; id: number;
confirm_application_number:string; confirm_application_number: string;
confirm_student_number:string; confirm_student_number: string;
detail_confirms:ConfirmedBillDetail[] detail_confirms: ConfirmedBillDetail[]
} }

6
src/vite-env.d.ts vendored
View File

@ -21,6 +21,7 @@ declare const AppConfig: {
ldapApiUrl:string; ldapApiUrl:string;
ldapApiKey: string; ldapApiKey: string;
}; };
declare const buildVersion: string;
declare const AppMode: 'test' | 'production' | 'development'; declare const AppMode: 'test' | 'production' | 'development';
declare const AppMode: 'test' | 'production' | 'development'; declare const AppMode: 'test' | 'production' | 'development';
@ -28,3 +29,8 @@ declare type BasicComponentProps = {
children?: React.ReactNode; children?: React.ReactNode;
} }
declare type BasicComponent<T> = React.FC<BasicComponentProps & T> declare type BasicComponent<T> = React.FC<BasicComponentProps & T>
declare type OptionValue = {
value: string;
label: string;
}

View File

@ -22,6 +22,7 @@ export default defineConfig(({mode}) => {
AUTH_TOKEN_KEY: process.env.AUTH_TOKEN_KEY || 'payment-auth-token', AUTH_TOKEN_KEY: process.env.AUTH_TOKEN_KEY || 'payment-auth-token',
...configs ...configs
}), }),
buildVersion: JSON.stringify((new Date()).toLocaleString()),
AppMode: JSON.stringify(mode) AppMode: JSON.stringify(mode)
}, },
resolve: { resolve: {
@ -36,7 +37,7 @@ export default defineConfig(({mode}) => {
target: 'https://test-payment-be.hkchc.team', // target: 'https://test-payment-be.hkchc.team', //
// target: 'http://127.0.0.1:50000', // // target: 'http://127.0.0.1:50000', //
changeOrigin: true, changeOrigin: true,
//rewrite: (path) => path.replace(/^\/api/, '') rewrite: (path) => path.replace(/^\/api/, '/api/')
}, },
'/staff-api': { '/staff-api': {
target: 'https://test-api.hkchc.team', // target: 'https://test-api.hkchc.team', //

119
yarn.lock
View File

@ -1057,6 +1057,11 @@
"@types/babel__core" "^7.20.5" "@types/babel__core" "^7.20.5"
react-refresh "^0.14.0" react-refresh "^0.14.0"
"@xmldom/xmldom@^0.8.2":
version "0.8.10"
resolved "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99"
integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==
acorn-jsx@^5.3.2: acorn-jsx@^5.3.2:
version "5.3.2" version "5.3.2"
resolved "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" resolved "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@ -1169,6 +1174,11 @@ bezier-easing@^2.1.0:
resolved "https://registry.npmmirror.com/bezier-easing/-/bezier-easing-2.1.0.tgz#c04dfe8b926d6ecaca1813d69ff179b7c2025d86" resolved "https://registry.npmmirror.com/bezier-easing/-/bezier-easing-2.1.0.tgz#c04dfe8b926d6ecaca1813d69ff179b7c2025d86"
integrity sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig== integrity sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==
bluebird@~3.7.2:
version "3.7.2"
resolved "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
brace-expansion@^1.1.7: brace-expansion@^1.1.7:
version "1.1.11" version "1.1.11"
resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@ -1341,6 +1351,11 @@ core-js@^3.6.0, core-js@^3.8.3:
resolved "https://registry.npmmirror.com/core-js/-/core-js-3.37.1.tgz#d21751ddb756518ac5a00e4d66499df981a62db9" resolved "https://registry.npmmirror.com/core-js/-/core-js-3.37.1.tgz#d21751ddb756518ac5a00e4d66499df981a62db9"
integrity sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw== integrity sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==
core-util-is@~1.0.0:
version "1.0.3"
resolved "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
cosmiconfig@^7.0.0: cosmiconfig@^7.0.0:
version "7.1.0" version "7.1.0"
resolved "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" resolved "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6"
@ -1435,6 +1450,13 @@ dompurify@^2.2.0:
resolved "https://registry.npmmirror.com/dompurify/-/dompurify-2.5.3.tgz#bc901a9c40a7d97176c1d0ab9a24939db54270a2" resolved "https://registry.npmmirror.com/dompurify/-/dompurify-2.5.3.tgz#bc901a9c40a7d97176c1d0ab9a24939db54270a2"
integrity sha512-09uyBM2URzOfXMUAqGRnm9R9IUeSkzO9PktXc2eVQIsBmmJUqRmfL1xW2QPBxVJEtlEVs5d8ndrsIQsyAqs81g== integrity sha512-09uyBM2URzOfXMUAqGRnm9R9IUeSkzO9PktXc2eVQIsBmmJUqRmfL1xW2QPBxVJEtlEVs5d8ndrsIQsyAqs81g==
duplexer2@~0.1.4:
version "0.1.4"
resolved "https://registry.npmmirror.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
integrity sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==
dependencies:
readable-stream "^2.0.2"
electron-to-chromium@^1.4.668: electron-to-chromium@^1.4.668:
version "1.4.774" version "1.4.774"
resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.774.tgz#1017d1758aaeeefe5423aa9d67b4b1e5d1d0a856" resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.774.tgz#1017d1758aaeeefe5423aa9d67b4b1e5d1d0a856"
@ -1653,6 +1675,11 @@ fflate@^0.4.8:
resolved "https://registry.npmmirror.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" resolved "https://registry.npmmirror.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==
fflate@^0.7.3:
version "0.7.4"
resolved "https://registry.npmmirror.com/fflate/-/fflate-0.7.4.tgz#61587e5d958fdabb5a9368a302c25363f4f69f50"
integrity sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==
file-entry-cache@^6.0.1: file-entry-cache@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" resolved "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@ -1713,6 +1740,15 @@ form-data@^4.0.0:
combined-stream "^1.0.8" combined-stream "^1.0.8"
mime-types "^2.1.12" mime-types "^2.1.12"
fs-extra@^11.2.0:
version "11.2.0"
resolved "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b"
integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==
dependencies:
graceful-fs "^4.2.0"
jsonfile "^6.0.1"
universalify "^2.0.0"
fs.realpath@^1.0.0: fs.realpath@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" resolved "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@ -1801,7 +1837,7 @@ gopd@^1.0.1:
dependencies: dependencies:
get-intrinsic "^1.1.3" get-intrinsic "^1.1.3"
graceful-fs@^4.1.2: graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2:
version "4.2.11" version "4.2.11"
resolved "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" resolved "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
@ -1912,7 +1948,7 @@ inflight@^1.0.4:
once "^1.3.0" once "^1.3.0"
wrappy "1" wrappy "1"
inherits@2: inherits@2, inherits@~2.0.3:
version "2.0.4" version "2.0.4"
resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@ -1961,6 +1997,11 @@ is-what@^3.14.1:
resolved "https://registry.npmmirror.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" resolved "https://registry.npmmirror.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1"
integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA== integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
isexe@^2.0.0: isexe@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" resolved "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@ -2013,6 +2054,15 @@ json5@^2.2.3:
resolved "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" resolved "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
dependencies:
universalify "^2.0.0"
optionalDependencies:
graceful-fs "^4.1.6"
jspdf-autotable@^3.8.2: jspdf-autotable@^3.8.2:
version "3.8.2" version "3.8.2"
resolved "https://registry.npmmirror.com/jspdf-autotable/-/jspdf-autotable-3.8.2.tgz#44d4c4e18494ccd6e31765e4d2adadda25b9713e" resolved "https://registry.npmmirror.com/jspdf-autotable/-/jspdf-autotable-3.8.2.tgz#44d4c4e18494ccd6e31765e4d2adadda25b9713e"
@ -2181,6 +2231,11 @@ needle@^3.1.0:
iconv-lite "^0.6.3" iconv-lite "^0.6.3"
sax "^1.2.4" sax "^1.2.4"
node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.npmmirror.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==
node-releases@^2.0.14: node-releases@^2.0.14:
version "2.0.14" version "2.0.14"
resolved "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" resolved "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
@ -2310,6 +2365,11 @@ prelude-ls@^1.2.1:
resolved "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" resolved "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
prop-types@15.x, prop-types@^15.7.2, prop-types@^15.8.1: prop-types@15.x, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1" version "15.8.1"
resolved "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" resolved "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
@ -2440,6 +2500,28 @@ react@^18.2.0:
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
read-excel-file@^5.8.5:
version "5.8.5"
resolved "https://registry.npmmirror.com/read-excel-file/-/read-excel-file-5.8.5.tgz#42e3b0bc967bc4e90c64e51204cc7d8ab157cc62"
integrity sha512-KDDcSsI3VzXTNUBs8q7RwTYrGRE8RZgNwGUivYq13bQtMp1KJmocyBs/EiPTJaFk4I8Ri9iDF+ht2A4GUrudMg==
dependencies:
"@xmldom/xmldom" "^0.8.2"
fflate "^0.7.3"
unzipper "^0.12.2"
readable-stream@^2.0.2:
version "2.3.8"
resolved "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b"
integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~2.0.0"
safe-buffer "~5.1.1"
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
regenerator-runtime@^0.13.7: regenerator-runtime@^0.13.7:
version "0.13.11" version "0.13.11"
resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
@ -2518,6 +2600,11 @@ run-parallel@^1.1.9:
dependencies: dependencies:
queue-microtask "^1.2.2" queue-microtask "^1.2.2"
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
"safer-buffer@>= 2.1.2 < 3.0.0": "safer-buffer@>= 2.1.2 < 3.0.0":
version "2.1.2" version "2.1.2"
resolved "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" resolved "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
@ -2621,6 +2708,13 @@ stackblur-canvas@^2.0.0:
resolved "https://registry.npmmirror.com/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz#af931277d0b5096df55e1f91c530043e066989b6" resolved "https://registry.npmmirror.com/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz#af931277d0b5096df55e1f91c530043e066989b6"
integrity sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ== integrity sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
dependencies:
safe-buffer "~5.1.0"
strip-ansi@^6.0.1: strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
@ -2718,6 +2812,22 @@ undici-types@~5.26.4:
resolved "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" resolved "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
universalify@^2.0.0:
version "2.0.1"
resolved "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
unzipper@^0.12.2:
version "0.12.3"
resolved "https://registry.npmmirror.com/unzipper/-/unzipper-0.12.3.tgz#31958f5eed7368ed8f57deae547e5a673e984f87"
integrity sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==
dependencies:
bluebird "~3.7.2"
duplexer2 "~0.1.4"
fs-extra "^11.2.0"
graceful-fs "^4.2.2"
node-int64 "^0.4.0"
update-browserslist-db@^1.0.13: update-browserslist-db@^1.0.13:
version "1.0.16" version "1.0.16"
resolved "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz#f6d489ed90fb2f07d67784eb3f53d7891f736356" resolved "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz#f6d489ed90fb2f07d67784eb3f53d7891f736356"
@ -2733,6 +2843,11 @@ uri-js@^4.2.2:
dependencies: dependencies:
punycode "^2.1.0" punycode "^2.1.0"
util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
utility-types@^3.10.0: utility-types@^3.10.0:
version "3.11.0" version "3.11.0"
resolved "https://registry.npmmirror.com/utility-types/-/utility-types-3.11.0.tgz#607c40edb4f258915e901ea7995607fdf319424c" resolved "https://registry.npmmirror.com/utility-types/-/utility-types-3.11.0.tgz#607c40edb4f258915e901ea7995607fdf319424c"