Compare commits

...

4 Commits

Author SHA1 Message Date
f50cc00d84 feat add bill/student number confirm 2024-08-05 12:37:06 +08:00
1f16e05c01 feat: add query condition 2024-08-03 23:51:08 +08:00
3f7d5dad92 feat: add docker compose file 2024-08-03 14:20:48 +08:00
770f320372 feat: add logout 2024-08-02 21:50:27 +08:00
21 changed files with 337 additions and 134 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
netlify.toml
node_modules
dist

View File

@ -20,7 +20,7 @@ COPY *.json .
# Install dependencies and build the app
RUN yarn install
RUN yarn build-test
RUN yarn build-prod
# Step 2. Production image, copy all the files and run nginx

View File

@ -1,44 +0,0 @@
# 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-prod
# 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;"]

27
compose.yaml Normal file
View File

@ -0,0 +1,27 @@
version: "3.8"
x-common: &common
pull_policy: always # always, never, missing, build
restart: unless-stopped
stop_signal: SIGINT
stop_grace_period: 1m
logging:
driver: "json-file"
options:
max-file: "10"
max-size: "100m"
services:
hkchc-payment-frontend-server:
image: registry.hkchc.team/hkchc-payment-frontend:latest
container_name: hkchc-payment-frontend
environment:
APP_API_URL: "10.10.0.152:50000" # payment backend service
working_dir: /etc/nginx/conf.d/
ports:
- "50001:80"
command: [
"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;"
]
<<: *common

View File

@ -130,6 +130,9 @@ body #root{
}
}
}
.text-nowrap{
white-space: nowrap;
}
// input
.semi-input-wrapper, .semi-select,.semi-datepicker-range-input,.semi-input-textarea-wrapper {

View File

@ -1,25 +1,29 @@
import React, {useMemo} from "react";
import {useTranslation} from "react-i18next";
import {IconBillType, IconMoney, IconStudentEmail, IconStudentId} from "@/components/icons";
import {IconBillType, IconMoney, IconStudentEmail, IconStudentId, IconStudentName} from "@/components/icons";
import MoneyFormat from "@/components/money-format.tsx";
import './bill.less'
const BillDetailItem = (item: { title: React.ReactNode; value: React.ReactNode, icon?: React.ReactNode }) => {
export const BillDetailItem = (item: { title: React.ReactNode; value: React.ReactNode, icon?: React.ReactNode }) => {
return <div className={'bill-detail-item'}>
<div className={'detail-item-title'}>{item.icon} <span className={'item-title'}>{item.title}</span> :</div>
<div className={'detail-item-value'}>{item.value}</div>
</div>
}
const BillDetailItems = (prop: { bill: BillModel }) => {
type BillDetailItemsProps = {
bill: BillModel;
studentNumberRender?: React.ReactNode;
}
const BillDetailItems = (prop: BillDetailItemsProps) => {
const {t} = useTranslation();
const billType = useMemo(()=>{
return prop.bill.details[0].bill_type
},[prop.bill])
return (<>
<BillDetailItem icon={<IconBillType/>} title={t('manual.bill_type')} value={billType}/>
<BillDetailItem icon={<IconStudentId/>} title={t('manual.student_number')} value={prop.bill.student_number || prop.bill.application_number}/>
<BillDetailItem icon={<IconStudentId/>} title={t('bill.title_student_name')}
{prop.studentNumberRender?prop.studentNumberRender:<BillDetailItem icon={<IconStudentId/>} title={t('manual.student_number')} value={prop.bill.student_number || prop.bill.application_number || '-'}/>}
<BillDetailItem icon={<IconStudentName/>} title={t('bill.title_student_name')}
value={`${prop.bill.student_english_name||'-'}${prop.bill.student_chinese_name?' / '+ prop.bill.student_chinese_name : ''}`}/>
<BillDetailItem icon={<IconStudentEmail/>} title={'Email'} value={prop.bill.student_email||'-'}/>
<BillDetailItem icon={<IconMoney/>} title={t('manual.amount')} value={<MoneyFormat money={prop.bill.amount}/>}/>

View File

@ -32,7 +32,10 @@ export const BillList: React.FC<BillListProps> = (props) => {
return t('bill.pay_status_pending')
case 'PAID':
return t('bill.pay_status_paid')
case 'EXPIRED':
return t('bill.pay_status_expired')
case 'CANCELED':
case 'CANCELLED':
return t('bill.pay_status_canceled')
default:
return billStatus
@ -56,6 +59,12 @@ export const BillList: React.FC<BillListProps> = (props) => {
dataIndex: 'id',
width: 120,
},
{
title: 'Merchant Ref',
dataIndex: 'merchant_ref',
width: 200,
// render: (_) => (<MoneyFormat money={_}/>),
},
{
title: t('base.student_number'),
dataIndex: 'student_number',
@ -68,33 +77,33 @@ export const BillList: React.FC<BillListProps> = (props) => {
width: 150,
},
{
title: '开始支付时间',
title: t('bill.title_initiated_paid_at'),
dataIndex: 'initiated_paid_at',
width: 150,
width: 180,
render: (value) => value?.length ?value: 'N/A'
},
{
title: '到账时间',
title: t('bill.title_delivered_at'),
dataIndex: 'delivered_at',
width: 150,
width: 180,
render: (value) => value?.length ?value: 'N/A'
},
{
title: t('bill.title_paid_at'),
dataIndex: 'paid_at',
width: 150,
width: 180,
render: (value) => value?.length ?value: 'N/A'
},
{
title: t('bill.title_create_at'),
dataIndex: 'create_at',
width: 150,
width: 180,
render: (value) => value?.length ?value: 'N/A'
},
{
title: t('bill.title_student_name'),
dataIndex: 'student_english_name',
width: 150,
width: 180,
render: (_, record) => (<div>{record.student_english_name}<br/>{record.student_chinese_name}</div>)
},
{
@ -118,7 +127,7 @@ export const BillList: React.FC<BillListProps> = (props) => {
title: t('bill.title_year'),
dataIndex: 'intake_year',
width: 120,
render: (_, record) => (<div>{record.intake_year}/{record.intake_semester}</div>)
render: (_, record) => record.intake_year?(<div>{record.intake_year}/{String(record.intake_semester).length == 1 ? '0':''}{record.intake_semester}</div>):"N/A"
},
// {
// title: t('bill.title_semester'),
@ -134,6 +143,15 @@ export const BillList: React.FC<BillListProps> = (props) => {
{record.details.map((it, idx) => (<div key={idx}>{it.bill_type}: <MoneyFormat money={it.amount}/></div>))}
</div>),
},
{
title: t('bill.title_bill_type_confirm'),
dataIndex: '_detail',
ellipsis: {showTitle: true},
width: 220,
render: (_, record) => (<div style={{fontSize: 13, lineHeight: 1.2}}>
{record.details.filter(s=>s.confirm_status == 'CONFIRMED').map((it) => (<div key={it.id}>{it.confirm_type}: <MoneyFormat money={it.amount}/></div>))}
</div>),
},
{
title: t('bill.title_amount'),
dataIndex: 'amount',
@ -169,19 +187,15 @@ export const BillList: React.FC<BillListProps> = (props) => {
width: 130,
render: (_, {payment_method, payment_channel}) => (payment_channel?(
<div>
{payment_channel}<br/>
{payment_channel}
{payment_method && payment_method.length > 0 && <div>
<Typography.Text type={'quaternary'} size={'small'} style={{position: "absolute"}}>
({payment_method})
</Typography.Text>
</div>}
</div>
):'N/A'),
},
{
title: 'Merchant Ref',
dataIndex: 'merchant_ref',
width: 250,
// render: (_) => (<MoneyFormat money={_}/>),
},
{
title: t('bill.title_bill_status'),
dataIndex: 'status',

View File

@ -4,6 +4,7 @@ import dayjs from "dayjs";
import {useTranslation} from "react-i18next";
import {Card} from "@/components/card";
import {BillQueryParams} from "@/service/api/bill.ts";
import {BillTypes} from "@/service/bill-types.ts";
type SearchFormProps = {
onSearch?: (params: BillQueryParams) => void;
@ -15,10 +16,12 @@ type SearchFormProps = {
type SearchFormFields = {
dateRange?: Date[];
student_number?: string;
merchant_ref?: string;
id?: string;
application_number?: string;
bill_number?: string;
payment_channel?: string;
confirm_status?: ConfirmStatus;
bill_status?: string;
apply_status?: string;
sort_by?: string;
@ -44,6 +47,10 @@ const SearchForm: React.FC<SearchFormProps> = (props) => {
if (value.apply_status) {
params.apply_status = value.apply_status;
}
// 确认状态
if (value.confirm_status) {
params.confirm_status = value.confirm_status;
}
// 账单状态
if (value.bill_status) {
params.status = value.bill_status;
@ -86,32 +93,29 @@ const SearchForm: React.FC<SearchFormProps> = (props) => {
{props.searchHeader}
<div className="bill-search-form">
<Form<SearchFormFields> onSubmit={formSubmit}>
<Row type={'flex'} gutter={20}>
<Row type={'flex'} gutter={16}>
<Col xxl={6} xl={6} md={8}>
<Form.Input showClear field='id' label="ID" trigger='blur' placeholder={t('base.please_enter')}/>
</Col>
<Col xxl={6} xl={6} md={8}>
<Form.DatePicker showClear type={'dateRange'} field="dateRange" label={t('bill.bill_date')}
<Form.DatePicker showClear type={'dateRange'} field="dateRange" label={t('bill.title_initiated_paid_at')}
style={{width: '100%'}}>
</Form.DatePicker>
</Col>
<Col xxl={6} xl={6} md={8}>
<Form.DatePicker showClear type={'dateRange'} field="delivered_at" label={t('bill.title_delivered_at')}
style={{width: '100%'}}>
</Form.DatePicker>
</Col>
<Col xxl={4} xl={6} md={8}>
<Form.Input showClear field='id' label="ID / Merchant Ref" trigger='blur' placeholder={t('base.please_enter')}/>
</Col>
<Col xxl={4} xl={6} md={8}>
<Form.Input showClear field='student_number' label={t('base.student_number')} trigger='blur'
placeholder={t('base.please_enter')}/>
</Col>
<Col xxl={6} xl={6} md={8}>
<Col xxl={4} xl={6} md={8}>
<Form.Input showClear field='application_number' label={t('base.bill_number')} trigger='blur'
placeholder={t('base.please_enter')}/>
</Col>
<Col xxl={6} xl={6} md={8}>
<Form.Select showClear field="payment_channel" label={t('bill.title_pay_channel')}
placeholder={t('base.please_select')} style={{width: '100%'}}>
<Form.Select.Option value="FLYWIRE">FLYWIRE</Form.Select.Option>
<Form.Select.Option value="CBP">CBP</Form.Select.Option>
<Form.Select.Option value="PPS">PPS</Form.Select.Option>
</Form.Select>
</Col>
<Col xxl={6} xl={6} md={8}>
<Col xxl={4} xl={6} md={8}>
<Form.Select showClear field="sort_by" label={t('bill.title_pay_sort')}
placeholder={t('base.please_select')} style={{width: '100%'}}>
<Form.Select.Option value="id desc">ID {t('bill.sort_desc')}</Form.Select.Option>
@ -127,15 +131,42 @@ const SearchForm: React.FC<SearchFormProps> = (props) => {
<Form.Select.Option value="create_at asc">{t('bill.title_create_at')} {t('bill.sort_asc')}</Form.Select.Option>
</Form.Select>
</Col>
<Col xxl={4} xl={6} md={8}>
<Form.Select showClear field="payment_channel" label={t('bill.title_pay_channel')}
placeholder={t('base.please_select')} style={{width: '100%'}}>
<Form.Select.Option value="FLYWIRE">FLYWIRE</Form.Select.Option>
<Form.Select.Option value="CBP">CBP</Form.Select.Option>
<Form.Select.Option value="PPS">PPS</Form.Select.Option>
</Form.Select>
</Col>
<Col xxl={4} xl={6} md={8}>
<Form.Select
field="bill_type" style={{width: '100%'}}
label={t('manual.bill_type')}
placeholder={t('manual.bill_type')}
>
{
BillTypes.map((it, idx) => (
<Form.Select.Option key={idx} value={it.label}>{it.label}</Form.Select.Option>))
}
</Form.Select>
</Col>
{props.showApply && <>
<Col xxl={6} xl={6} md={8}>
<Col xxl={4} xl={6} md={8}>
<Form.Select showClear field="confirm_status" label={t('bill.title_confirm_status')}
placeholder={t('base.please_select')} style={{width: '100%'}}>
<Form.Select.Option value="CONFIRMED">{t('bill.status_confirmed')}</Form.Select.Option>
<Form.Select.Option value="UNCONFIRMED">{t('bill.status_unconfirmed')}</Form.Select.Option>
</Form.Select>
</Col>
<Col xxl={4} xl={6} md={8}>
<Form.Select showClear field="bill_status" label={t('bill.pay_status')}
placeholder={t('base.please_select')} style={{width: '100%'}}>
{billStatusOptions.map((item, index) => (
<Form.Select.Option key={index} value={item.value}>{item.label}</Form.Select.Option>))}
</Form.Select>
</Col>
<Col xxl={6} xl={6} md={8}>
<Col xxl={4} xl={6} md={8}>
<Form.Select showClear field="apply_status" label={t('bill.title_reconciliation_status')}
placeholder={t('base.please_select')} style={{width: '100%'}}>
{applyStatusOptions.map((item, index) => (
@ -143,7 +174,7 @@ const SearchForm: React.FC<SearchFormProps> = (props) => {
</Form.Select>
</Col>
</>}
<Col xxl={6} xl={6} md={8} style={{display: 'flex', alignItems: 'flex-end', paddingBottom: 12}}>
<Col xxl={4} xl={6} md={8} style={{display: 'flex', alignItems: 'flex-end', paddingBottom: 12}}>
<Button loading={props.loading} style={{width: 100}} htmlType={'submit'} theme={'solid'}
type={'primary'}>{t('base.btn_search_submit')}</Button>
</Col>

View File

@ -60,6 +60,22 @@ export const IconMoney = ({style}: IconProps) => {
}
export const IconStudentId = ({style}: IconProps) => {
return (
<svg
className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" style={style}>
<path
d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z m-40 632H136V232h752v560z"
fill="#00C479"
></path>
<path
d="M610.3 476h123.4c1.3 0 2.3-3.6 2.3-8v-48c0-4.4-1-8-2.3-8H610.3c-1.3 0-2.3 3.6-2.3 8v48c0 4.4 1 8 2.3 8zM615.1 620h185.7c3.9 0 7.1-3.6 7.1-8v-48c0-4.4-3.2-8-7.1-8H615.1c-3.9 0-7.1 3.6-7.1 8v48c0 4.4 3.2 8 7.1 8zM224 673h43.9c4.2 0 7.6-3.3 7.9-7.5 3.8-50.5 46-90.5 97.2-90.5s93.4 40 97.2 90.5c0.3 4.2 3.7 7.5 7.9 7.5H522c4.6 0 8.2-3.8 8-8.4-2.8-53.3-32-99.7-74.6-126.1 18.1-19.9 29.1-46.4 29.1-75.5 0-61.9-49.9-112-111.4-112s-111.4 50.1-111.4 112c0 29.1 11 55.5 29.1 75.5-42.7 26.5-71.8 72.8-74.6 126.1-0.4 4.6 3.2 8.4 7.8 8.4z m149-262c28.5 0 51.7 23.3 51.7 52s-23.2 52-51.7 52-51.7-23.3-51.7-52 23.2-52 51.7-52z"
></path>
</svg>
)
}
export const IconStudentName = ({style}: IconProps) => {
return (
<svg className="icon" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" style={style}>
@ -88,6 +104,7 @@ export const IconStudentId = ({style}: IconProps) => {
</g>
</svg>)
}
export const IconStudentEmail = ({style}: IconProps) => {
return (
<svg className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"

View File

@ -110,6 +110,12 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
}
// 登出
const logout = async () => {
setTimeout(()=>{
const a = document.createElement('a')
a.setAttribute('href','https://portal.chuhai.edu.hk/signout')
a.setAttribute('target','_blank')
a.click()
},0)
setAuthToken(null)
removeRoleStorage()
dispatch({

View File

@ -15,7 +15,7 @@
"student_number": "Student Number"
},
"bill": {
"bill_date": "Date",
"bill_date": "In",
"bill_number": "Bill Number",
"cancel": "Cancel",
"cancel_confirm": "Please make sure to cancel the bill",
@ -23,9 +23,13 @@
"cancel_success": "Successful cancel bill",
"confirm": "Check",
"confirm_batch": "Batch Confirm",
"confirm_bill": "Confirm Bill Information",
"confirm_bill_number": "Confirm Bill Number",
"confirm_bill_type": "Confirm Bill",
"confirm_bill_type_batch": "Batch confirm Bill Type",
"confirm_confirm_title": "Confirm check the Bill?",
"confirm_select_empty": "Require confirm bill data",
"confirm_student_number": "Confirm Student Number",
"confirm_success": "Confirm success!",
"confirmed": "Confirmed",
"download-qr-code": "Download QR Code",
@ -36,6 +40,7 @@
"paid_confirm": "Please confirm the order status is set to paid",
"pay_status": "Bill Status",
"pay_status_canceled": "CANCELED",
"pay_status_expired": "EXPIRED",
"pay_status_paid": "PAID",
"pay_status_pending": "PENDING",
"query_amount_current_page": "The total amount of current page",
@ -46,22 +51,29 @@
"set_bill_paid": "Set Bill Paid",
"sort_asc": "ASC",
"sort_desc": "DESC",
"status_confirmed": "CONFIRMED",
"status_unconfirmed": "UNCONFIRMED",
"title_actual_payment_amount": "Actually Paid",
"title_amount": "Amount",
"title_bill_detail": "Bill Detail",
"title_bill_list": "Bill List",
"title_bill_status": "Bill Status",
"title_create_at": "Input Date",
"title_bill_type": "Bill Type",
"title_bill_type_confirm": "Confirm Bill Type",
"title_confirm_status": "Confirm Status",
"title_create_at": "Input Time",
"title_delivered_at": "Delivered Time",
"title_department": "Department",
"title_initiated_paid_at": "Initiated Time",
"title_operate": "Operation",
"title_paid_at": "Transaction Date",
"title_paid_at": "Transaction Time",
"title_pay_amount": "Pay Amount",
"title_pay_channel": "Payment Channel",
"title_pay_method": "Pay Method",
"title_pay_sort": "Sort By",
"title_program_id": "Program ID",
"title_program_name": "Program",
"title_reconciliation_status": "Reconciliation",
"title_reconciliation_status": "Reconciliation Status",
"title_remark": "Remark",
"title_semester": "Semester",
"title_service_charge": "Service Charge",

View File

@ -15,7 +15,7 @@
"student_number": "学号"
},
"bill": {
"bill_date": "支付日期",
"bill_date": "开始支付时间",
"bill_number": "账单编号",
"cancel": "作废",
"cancel_confirm": "确定作废此账单",
@ -23,9 +23,13 @@
"cancel_success": "作废账单成功",
"confirm": "对账",
"confirm_batch": "批量对账",
"confirm_bill": "确认账单信息",
"confirm_bill_number": "确认账单编号",
"confirm_bill_type": "确认账单",
"confirm_bill_type_batch": "批量确认账单",
"confirm_confirm_title": "请确定对账此账单?",
"confirm_select_empty": "对账账单为空",
"confirm_student_number": "确认学号",
"confirm_success": "对账成功!",
"confirmed": "已对账",
"download-qr-code": "下载二维码",
@ -36,6 +40,7 @@
"paid_confirm": "是否将此订单状态设为已支付",
"pay_status": "账单状态",
"pay_status_canceled": "已作废",
"pay_status_expired": "已过期",
"pay_status_paid": "已支付",
"pay_status_pending": "未支付",
"query_amount_current_page": "当前页总金额",
@ -46,13 +51,20 @@
"set_bill_paid": "设置账单支付完成",
"sort_asc": "升序",
"sort_desc": "降序",
"status_confirmed": "已确认",
"status_unconfirmed": "未确认",
"title_actual_payment_amount": "实付金额",
"title_amount": "账单金额",
"title_bill_detail": "账单详情",
"title_bill_list": "账单列表",
"title_bill_status": "账单状态",
"title_bill_type": "账单类型",
"title_bill_type_confirm": "确认账单",
"title_confirm_status": "确认状态",
"title_create_at": "创建时间",
"title_delivered_at": "到账时间",
"title_department": "学系",
"title_initiated_paid_at": "开始支付时间",
"title_operate": "操作",
"title_paid_at": "支付时间",
"title_pay_amount": "应付金额",

View File

@ -15,7 +15,7 @@
"student_number": "學號"
},
"bill": {
"bill_date": "支付日期",
"bill_date": "開始支付時間",
"bill_number": "帳單編號",
"cancel": "作廢",
"cancel_confirm": "確定作廢此帳單",
@ -23,9 +23,13 @@
"cancel_success": "作廢帳單成功",
"confirm": "對帳",
"confirm_batch": "批次對帳",
"confirm_bill": "確認帳單資訊",
"confirm_bill_number": "確認帳單編號",
"confirm_bill_type": "確認賬單",
"confirm_bill_type_batch": "批次確認帳單",
"confirm_confirm_title": "請確定對帳此帳單?",
"confirm_select_empty": "對帳帳單為空",
"confirm_student_number": "確認學號",
"confirm_success": "對帳成功!",
"confirmed": "已對帳",
"download-qr-code": "下載二維碼",
@ -36,6 +40,7 @@
"paid_confirm": "是否將此訂單狀態設為已支付",
"pay_status": "帳單狀態",
"pay_status_canceled": "已作廢",
"pay_status_expired": "已過期",
"pay_status_paid": "已付款",
"pay_status_pending": "未付款",
"query_amount_current_page": "目前頁總金額",
@ -46,13 +51,20 @@
"set_bill_paid": "設定帳單支付完成",
"sort_asc": "升序",
"sort_desc": "降序",
"status_confirmed": "已確認",
"status_unconfirmed": "未確認",
"title_actual_payment_amount": "實付金額",
"title_amount": "帳單金額",
"title_bill_detail": "帳單詳情",
"title_bill_list": "帳單清單",
"title_bill_status": "帳單狀態",
"title_bill_type": "帳單類型",
"title_bill_type_confirm": "確認帳單",
"title_confirm_status": "確認狀態",
"title_create_at": "創建時間",
"title_delivered_at": "到帳時間",
"title_department": "學系",
"title_initiated_paid_at": "開始支付時間",
"title_operate": "操作",
"title_paid_at": "付款時間",
"title_pay_amount": "應付金額",

View File

@ -27,15 +27,15 @@ export const BillTypeConfirm: React.FC<BillTypeConfirmProps> = (props) => {
})
}
return <div className="confirm-item align-center space-between"
style={{marginBottom: 10}}>
<div>
style={{marginBottom: 20}}>
<div style={{lineHeight:1.1}}>
<div>{it.bill_type}</div>
<div>
<div style={{fontSize:12}}>
<MoneyFormat money={it.amount}/>
</div>
</div>
<div className="confirm-item-btn">
<Space spacing={20}>
<Space spacing={15}>
{it.confirm_status != 'CONFIRMED' && <Select onChange={v=>setState({bill_type:String(v)})} defaultValue={it.bill_type} style={{width:180}} placeholder={t('manual.bill_type')}>
{
BillTypes.map((it, idx) => (
@ -43,12 +43,12 @@ export const BillTypeConfirm: React.FC<BillTypeConfirmProps> = (props) => {
}
</Select>}
{
it.confirm_status == 'CONFIRMED' ? <Tag color='light-blue'>{state.bill_type}</Tag> : <>
it.confirm_status == 'CONFIRMED' ? <Tag size={'large'} color='light-blue'>{state.bill_type}</Tag> : <>
<Popconfirm
title={'Notice'} onConfirm={() => onConfirmBill()}
position={'topRight'}
content={`${t('bill.confirm_bill_type')}?`}
><Button loading={state.loading} theme={'solid'}>{t('base.confirm')}</Button></Popconfirm>
><Button style={{width:80}} loading={state.loading} theme={'solid'}>{t('base.confirm')}</Button></Popconfirm>
</>
}
</Space>

View File

@ -0,0 +1,56 @@
import {Button, Space, Tag, Input} from "@douyinfe/semi-ui";
import React from "react";
import {useSetState} from "ahooks";
import {useTranslation} from "react-i18next";
type NumberConfirmProps = {
bill: BillModel;
type: 'student_number' | 'application_number'
}
export const NumberConfirm: React.FC<NumberConfirmProps> = (props) => {
const {t} = useTranslation()
const [state, setState] = useSetState({
loading: false,
confirmed: false,
confirmNumber: (props.type == 'application_number' ? props.bill.application_number : props.bill.student_number) || '',
})
const onConfirm = () => {
if (!state.confirmNumber.length) return
setState({loading: true})
setTimeout(() => {
setState({loading: false, confirmed: true})
}, 500)
// confirmBillType({id:it.id,type:state.bill_type}).then(() => {
// setState({loading:false})
// setItem({...it,confirm_status:'CONFIRMED'})
// }).catch(() => {
// setState({loading:false})
// })
}
return <div
className="confirm-item align-center space-between"
style={{marginBottom: 15}}>
<div>
<div>{t(props.type == 'student_number' ? 'bill.confirm_student_number' : 'bill.confirm_bill_number')}</div>
</div>
<div className="confirm-item-btn">
<Space spacing={15}>
{!state.confirmed && <Input
onChange={v => setState({confirmNumber: String(v)})}
defaultValue={state.confirmNumber}
style={{width: 180}} placeholder={t('base.please_enter')}/>}
{
state.confirmed ? <Space>
<div>{state.confirmNumber}</div>
<Tag size={'large'} color='light-blue'>CONFIRMED</Tag>
</Space> : <Button
style={{width: 80}} disabled={!state.confirmNumber} onClick={onConfirm}
loading={state.loading} theme={'solid'}>{t('base.confirm')}</Button>
}
</Space>
</div>
</div>
}

View File

@ -63,6 +63,13 @@ const ExternalCreate = () => {
if (!params.details || params.details.length == 0) {
return setState({error: 'params error: require detail',loading: false})
}
let tempAmount = 0;
params.details.forEach((d:BillDetail) => {
tempAmount += Number(d.amount)
})
if(tempAmount != Number(params.amount)){
return setState({error: 'params error: amount not equal to detail amount',loading: false})
}
createBill(params)
return;
}

View File

@ -13,6 +13,9 @@ import {BillDetailItems} from "@/components/bill";
import {BillPaidModal} from "@/pages/bill/components/bill_paid_modal.tsx";
import {BillTypeConfirm} from "@/pages/bill/components/bill_type_confirm.tsx";
import {saveAs} from "file-saver";
import {IconStudentId} from "@/components/icons";
import {BillDetailItem} from "@/components/bill/bill-detail-items.tsx";
import {NumberConfirm} from "@/pages/bill/components/number_confirm.tsx";
const DownloadButton = ({bill, text}: { bill: BillModel; text: string }) => {
@ -86,27 +89,31 @@ const BillQuery = () => {
}
</div>)
}
const onExportExcel = ()=>{
const onExportExcel = () => {
// const downloadUrl = `${AppConfig.API_PREFIX || '/api'}/bills/export?${stringify(queryParams)}`
//
//
// saveAs(downloadUrl, 'bill-result-excel.xlsx')
setState({
exporting:true
exporting: true
})
exportBillList(queryParams).then(ret=>{
exportBillList(queryParams).then(ret => {
console.log(ret)
const blob = new Blob([ret], {type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
const blob = new Blob([ret], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
saveAs(blob, 'bill-result-excel.xlsx')
}).finally(()=>{
}).finally(() => {
setState({
exporting:false
exporting: false
})
})
}
const onImportExcel = ()=>{
Toast.warning({content:'Not implemented'})
const onImportExcel = () => {
Toast.warning({content: 'Not implemented'})
}
const [selectKeys, setSelectedKeys] = useState<(string | number)[]>([])
const confirmBillTypeBatch = () => {
console.log(selectKeys)
}
return (<div>
@ -114,10 +121,22 @@ const BillQuery = () => {
<BillList
type={'query'} loading={loading} source={data}
operationRender={operation} operationRenderWidth={180}
beforeTotalAmount={<ButtonGroup style={{marginRight:20}} theme={'solid'}>
beforeTotalAmount={<ButtonGroup style={{marginRight: 20}} theme={'solid'}>
{
(selectKeys.length == 0) ? <Button disabled>{t('bill.confirm_bill_type_batch')}</Button> :
<Popconfirm
title={'Notice'}
content={`${t('bill.cancel_confirm_bills')}?`}
onConfirm={() => confirmBillTypeBatch()}
>
<Button theme={'solid'}>{t('bill.confirm_bill_type_batch')}</Button>
</Popconfirm>
}
<Button onClick={onImportExcel}>{t('bill.import_excel')}</Button>
<Button loading={state.exporting} onClick={onExportExcel}>{t('bill.export_excel')}</Button>
</ButtonGroup>}
onRowSelection={setSelectedKeys}
onPageChange={(page_number) => {
setBillQueryParams({
...queryParams,
@ -146,19 +165,34 @@ const BillQuery = () => {
}}
/>
<Modal
title="Confirm Bill Type"
title={t('bill.confirm_bill')}
visible={!!state.confirmBill}
closeOnEsc={true}
onCancel={() => {
refresh()
setState({confirmBill: undefined})
}}
width={500}
width={550}
footer={null}
>
{state.confirmBill && <>
<div><BillDetailItems bill={state.confirmBill}/></div>
<div className="confirm-container" style={{padding: '15px 0'}}>
<div><BillDetailItems bill={state.confirmBill} studentNumberRender={<>
<BillDetailItem
icon={<IconStudentId/>} title={t('manual.student_number')}
value={state.confirmBill.student_number || '-'}/>
<BillDetailItem
icon={<IconStudentId/>} title={t('base.bill_number')}
value={state.confirmBill.application_number || '-'}/>
</>}/></div>
<div className="confirm-container" style={{padding: '15px 0',marginTop:20}}>
{
!state.confirmBill.student_number_confirm &&
<NumberConfirm bill={state.confirmBill} type={'student_number'}/>
}
{
!state.confirmBill.application_number_confirm &&
<NumberConfirm bill={state.confirmBill} type={'application_number'}/>
}
{
state.confirmBill.details.map((it, idx) => (<div key={idx}>
<Divider margin='12px'/>

View File

@ -86,6 +86,20 @@ const BillReconciliation = () => {
<BillList
source={data} type={'reconciliation'}
operationRender={queryParams.apply_status == 'CHECKED' ? undefined : operation}
beforeTotalAmount={<div>{queryParams.apply_status != 'CHECKED' && (
(selectKeys.length == 0 )? <Button theme={'solid'} disabled style={{marginRight: 10}}>
{t('bill.confirm_batch')}
</Button> :
<Popconfirm
title={'Notice'}
content={`${t('bill.cancel_confirm_bills')}?`}
onConfirm={() => confirmBill(selectKeys as number[])}
>
<Button theme={'solid'} style={{marginRight: 10}}>
{t('bill.confirm_batch')}
</Button>
</Popconfirm>
)}</div>}
onRowSelection={queryParams.apply_status == 'CHECKED' ? undefined : (keys: (number | string)[]) => {
setSelectedKeys(keys);
}}
@ -94,17 +108,6 @@ const BillReconciliation = () => {
...queryParams,
page_number
})}
tableFooter={queryParams.apply_status != 'CHECKED' && selectKeys?.length > 0 && (
<Popconfirm
title={'Notice'}
content={`${t('bill.cancel_confirm_bills')}?`}
onConfirm={() => confirmBill(selectKeys as number[])}
>
<Button style={{marginRight: 10}}>
{t('bill.confirm_batch')}
</Button>
</Popconfirm>
)}
/>
</div>)
}

View File

@ -26,16 +26,18 @@ export function GeneratePdf(bill: BillModel) {
doc.text('ACKNOWLEDGEMENT RECEIPT', 100, 20, {});
doc.setFont('Helvetica', 'normal', 'normal');
drawItem(doc, {title: 'Student Name:', content: bill.student_english_name || bill.student_chinese_name}, 40)
drawItem(doc, {title: 'Student Name:', content: bill.student_english_name || bill.student_chinese_name || 'N/A'}, 40)
drawItem(doc, {title: 'Reference Number:', content: `${bill.id}`}, 40, "right")
drawItem(doc, {title: 'Student 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}`}, 48)
drawItem(doc, {title: 'Print Date:', content: dayjs().format('YYYY-MM-DD')}, 48, "right")
drawItem(doc, {
title: 'Programme:',
content: bill.programme_english_name
content: bill.programme_english_name || 'N/A'
}, 56)
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)
}
// draw table
autoTable(doc, {
startY: 80,
@ -61,7 +63,7 @@ export function GeneratePdf(bill: BillModel) {
`#${it.id}`,
dayjs(bill.paid_at).format('YYYY-MM-DD'),
it.bill_type,
`${bill.payment_channel}` + (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}`
];
})),

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

@ -14,6 +14,7 @@ declare type BillDetail = {
id: number;
bill_type: string;
confirm_status: ConfirmStatus;
confirm_type: string;
amount: decimal;
}
/**
@ -25,6 +26,8 @@ declare type BillQueryParam = {
status:string;
apply_status:string;
id:string|number;
merchant_ref:string;
bill_type:string;
student_number:string;
application_number:string;
payment_channel:string;
@ -41,7 +44,9 @@ declare type BillQueryParam = {
declare type BillModel = {
id: number;
student_number: string;
application_number?: null | string | number;
student_number_confirm?: string;
application_number: null | string;
application_number_confirm?: null | string;
student_email: string;
student_tc_name?: string;
student_sc_name?: string;
@ -93,7 +98,8 @@ declare type AsiaPayModel = {
}
type ExternalCreateParamsType = {
[key: string]: string | null | BillDetail[];
details: BillDetail[];
[key: string]: string | null;
}
type BillUpdateParams = {

View File

@ -28,8 +28,8 @@ export default defineConfig(({mode}) => {
port:10086,
proxy: {
'/api': {
target: 'https://test-payment-be.hkchc.team', //
// target: 'http://127.0.0.1:50000', //
// target: 'https://test-payment-be.hkchc.team', //
target: 'http://127.0.0.1:50000', //
changeOrigin: true,
//rewrite: (path) => path.replace(/^\/api/, '')
}