diff --git a/src/assets/index.less b/src/assets/index.less index 0470733..cf2be41 100644 --- a/src/assets/index.less +++ b/src/assets/index.less @@ -105,6 +105,9 @@ body #root{ } /***************** semi overrides ****************/ +.semi-dropdown-item{ + max-width: 500%; +} .semi-dropdown-item-active { background-color: var(--semi-color-default-active); } @@ -145,6 +148,11 @@ body #root{ border: 1px solid var(--semi-color-focus-border); } } +.semi-tagInput-wrapper-input{ + &:hover{ + border-color: transparent; + } +} .semi-input-wrapper-focus, .semi-datepicker-range-input-active, diff --git a/src/components/bill/list.tsx b/src/components/bill/list.tsx index 7c311fc..77931d9 100644 --- a/src/components/bill/list.tsx +++ b/src/components/bill/list.tsx @@ -1,296 +1,370 @@ -import {Space, Table, Typography} from "@douyinfe/semi-ui"; +import {Button, Checkbox, CheckboxGroup, Space, Table, Typography} from "@douyinfe/semi-ui"; import {ColumnProps} from "@douyinfe/semi-ui/lib/es/table"; -import React, {useMemo, useState} from "react"; +import React, {ReactNode, useMemo, useState} from "react"; import {useTranslation} from "react-i18next"; import dayjs from "dayjs"; -import {IconCheckCircleStroked} from "@douyinfe/semi-icons"; +import {IconCheckCircleStroked, IconSetting} from "@douyinfe/semi-icons"; import MoneyFormat from "@/components/money-format.tsx"; import {Card} from "@/components/card"; import './bill.less' import {BillStatus} from "@/service/types.ts"; +import {useSetState} from "ahooks"; +import {clone} from "lodash"; type BillListProps = { - type: 'query' | 'reconciliation'; - operationRender?: (record: BillModel) => React.ReactNode; - operationRenderWidth?: number; - onRowSelection?: (selectedRowKeys: (string | number)[]) => void; - source?: RecordList; - onPageChange: (pageIndex:number) => void; - tableFooter?: React.ReactNode; - loading?: boolean; - beforeTotalAmount?: React.ReactNode; + type: 'query' | 'reconciliation'; + operationRender?: (record: BillModel) => React.ReactNode; + operationRenderWidth?: number; + onRowSelection?: (selectedRowKeys: (string | number)[]) => void; + source?: RecordList; + onPageChange: (pageIndex: number) => void; + tableFooter?: React.ReactNode; + loading?: boolean; + beforeTotalAmount?: React.ReactNode; } -const CheckNumberCorrect = ({origin,confirmed}:{origin: string,confirmed?:string}) => { - if(origin == confirmed && origin){ - return ({origin}) - } - return
-
{origin?.length ?origin: 'N/A'}
- {confirmed&&{confirmed}} -
+const CheckNumberCorrect = ({origin, confirmed}: { origin: string, confirmed?: string|null }) => { + if (origin == confirmed && origin) { + return ({origin}) + } + return
+
{origin?.length ? origin : 'N/A'}
+ {confirmed && + {confirmed}} +
} export const BillList: React.FC = (props) => { - const {t, i18n} = useTranslation() - const [currentTotalAmount,setCurrentTotalAmount] = useState(0) + const {t, i18n} = useTranslation() + const [currentTotalAmount, setCurrentTotalAmount] = useState(0) + const [state, setState] = useSetState<{ + showColumnsConfig?: boolean; + showCols: string[] + }>({ + showCols: [ + "id", "merchant_ref", "student_number", "application_number", "initiated_paid_at", "delivered_at", "paid_at", "student_english_name", "student_email", "programme_chinese_name", + "intake_year", "detail", "detail_confirms", "amount", "pay_amount", "actual_payment_amount", "pay_method", "status", "apply_status" + ] + }) + const billStatusText = (billStatus: string) => { + switch (billStatus) { + case 'PENDING': + 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 + } + } + const applyStatusText = (status: string) => { + switch (status) { + case 'UNCHECKED': + return t('bill.reconciliation_status_pending') + case 'CHECKED': + return t('bill.reconciliation_status_submitted') + default: + return status + } + } - const billStatusText = (billStatus: string) => { - switch (billStatus) { - case 'PENDING': - 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 - } - } - const applyStatusText = (status:string) => { - switch (status) { - case 'UNCHECKED': - return t('bill.reconciliation_status_pending') - case 'CHECKED': - return t('bill.reconciliation_status_submitted') - default: - return status - } - } + const allCols = useMemo[]>(() => { + const cols: ColumnProps[] = [ + { + title: '#ID', + dataIndex: 'id', + width: 120, + }, + { + title: 'Merchant Ref', + dataIndex: 'merchant_ref', + width: 200, + // render: (_) => (), + }, + { + title: t('base.student_number'), + dataIndex: 'student_number', + width: 150, + render: (value:string) => (value|| 'N/A') + }, + { + title: t('base.bill_number'), + dataIndex: 'application_number', + width: 150, + render: (value, record) => ( + ) + }, + // { + // title: t('bill.title_application_number_confirmed'), + // dataIndex: 'application_number', + // width: 150, + // render: (value, record) => ( + // ) + // }, + { + title:
{t('bill.title_initiated_paid_at')} +
(PPS Input Date)
+
, + dataIndex: 'initiated_paid_at', + width: 180, + render: (value) => value?.length ? value : 'N/A' + }, + { + title:
{t('bill.title_delivered_at')} +
(PPS Statement Date)
+
, + dataIndex: 'delivered_at', + width: 180, + render: (value) => value?.length ? value : 'N/A' + }, + { + title: t('bill.title_paid_at'), + dataIndex: 'paid_at', + width: 180, + render: (value) => value?.length ? value : 'N/A' + }, + // { + // title: t('bill.title_create_at'), + // dataIndex: 'create_at', + // width: 180, + // render: (value) => value?.length ?value: 'N/A' + // }, + { + title: t('bill.title_student_name'), + dataIndex: 'student_english_name', + width: 180, + render: (_, record) => (
{record.student_english_name}
{record.student_chinese_name}
) + }, + { + title: 'Email', + dataIndex: 'student_email', + width: 200, + render: (value) => value?.length ? value : 'N/A' + // render: (_, record) => (
{record.student_english_name}
{record.student_chinese_name}
) + }, + { + title: t('bill.title_program_name'), + width: 250, + dataIndex: i18n.language == 'en-US' ? 'programme_english_name' : 'programme_chinese_name', + }, + { + title: t('bill.title_department'), + width: 200, + dataIndex: '', + render: (_, record) => (i18n.language == 'en-US' ? record.department_english_name : record.department_chinese_name), + }, + { + title: t('bill.title_year'), + dataIndex: 'intake_year', + width: 120, + render: (_, record) => record.intake_year ? ( +
{record.intake_year}/{String(record.intake_semester).length == 1 ? '0' : ''}{record.intake_semester}
) : "N/A" + }, + // { + // title: t('bill.title_semester'), + // dataIndex: 'intake_semester', + // width: 120, + // }, + { + title: t('bill.title_bill_detail'), + dataIndex: 'detail', + ellipsis: {showTitle: true}, + width: 220, + render: (_, record) => (
+ {record.details.map((it, idx) => ( +
{it.bill_type}:
))} +
), + }, + { + title: t('bill.title_bill_type_confirm'), + dataIndex: 'detail_confirms', + ellipsis: {showTitle: true}, + width: 220, + render: (_, record) => (
+ {record.detail_confirms?.map((it) => ( +
{it.bill_type}:
))} +
), + }, + { + title: t('bill.title_amount'), + dataIndex: 'amount', + width: 150, + render: (_) => (), + }, + { + title: t('bill.title_pay_amount'), + dataIndex: 'pay_amount', + width: 190, + render: (_, record) => { - const columns = useMemo[]>(() => { - const cols: ColumnProps[] = [ - { - title: '#ID', - dataIndex: 'id', - width: 120, - }, - { - title: 'Merchant Ref', - dataIndex: 'merchant_ref', - width: 200, - // render: (_) => (), - }, - { - title: t('base.student_number'), - dataIndex: 'student_number', - width: 150, - render: (value,record) => () - }, - { - title: t('base.bill_number'), - dataIndex: 'application_number', - width: 150, - render: (value,record) => () - }, - { - title: t('bill.title_initiated_paid_at'), - dataIndex: 'initiated_paid_at', - width: 180, - render: (value) => value?.length ?value: 'N/A' - }, - { - title: t('bill.title_delivered_at'), - dataIndex: 'delivered_at', - width: 180, - render: (value) => value?.length ?value: 'N/A' - }, - { - title: t('bill.title_paid_at'), - dataIndex: 'paid_at', - width: 180, - render: (value) => value?.length ?value: 'N/A' - }, - { - title: t('bill.title_create_at'), - dataIndex: 'create_at', - width: 180, - render: (value) => value?.length ?value: 'N/A' - }, - { - title: t('bill.title_student_name'), - dataIndex: 'student_english_name', - width: 180, - render: (_, record) => (
{record.student_english_name}
{record.student_chinese_name}
) - }, - { - title: 'Email', - dataIndex: 'student_email', - width: 200, - render: (value) => value?.length ?value: 'N/A' - // render: (_, record) => (
{record.student_english_name}
{record.student_chinese_name}
) - }, - { - title: t('bill.title_program_name'), - width: 250, - dataIndex: i18n.language == 'en-US' ? 'programme_english_name' : 'programme_chinese_name', - }, - { - title: t('bill.title_department'), - width: 200, - dataIndex: i18n.language == 'en-US' ? 'department_english_name' : 'department_chinese_name', - }, - { - title: t('bill.title_year'), - dataIndex: 'intake_year', - width: 120, - render: (_, record) => record.intake_year?(
{record.intake_year}/{String(record.intake_semester).length == 1 ? '0':''}{record.intake_semester}
):"N/A" - }, - // { - // title: t('bill.title_semester'), - // dataIndex: 'intake_semester', - // width: 120, - // }, - { - title: t('bill.title_bill_detail'), - dataIndex: 'detail', - ellipsis: {showTitle: true}, - width: 220, - render: (_, record) => (
- {record.details.map((it, idx) => (
{it.bill_type}:
))} -
), - }, - { - title: t('bill.title_bill_type_confirm'), - dataIndex: '_detail', - ellipsis: {showTitle: true}, - width: 220, - render: (_, record) => (
- {record.details.filter(s=>s.confirm_status == 'CONFIRMED').map((it) => (
{it.confirm_type}:
))} -
), - }, - { - title: t('bill.title_amount'), - dataIndex: 'amount', - width: 150, - render: (_) => (), - }, - { - title: t('bill.title_pay_amount'), - dataIndex: 'pay_amount', - width: 190, - render: (_, record) => { - - if (record.service_charge && record.service_charge > 0) { - return
-
- - {t('bill.title_service_charge')}: - -
- } - return (
) - }, - }, - { - title: t('bill.title_actual_payment_amount'), - dataIndex: 'actual_payment_amount', - width: 150, - render: (_,record) => (), - }, - { - title: t('bill.title_pay_method'), - dataIndex: 'pay_method', - width: 130, - render: (_, {payment_method, payment_channel}) => (payment_channel?( -
- {payment_channel} - {payment_method && payment_method.length > 0 &&
+ if (record.service_charge && record.service_charge > 0) { + return
+
+ + {t('bill.title_service_charge')}: + +
+ } + return (
) + }, + }, + { + title: t('bill.title_actual_payment_amount'), + dataIndex: 'actual_payment_amount', + width: 150, + render: (_, record) => (), + }, + { + title: t('bill.title_pay_method'), + dataIndex: 'pay_method', + width: 130, + render: (_, {payment_method, payment_channel}) => (payment_channel ? ( +
+ {payment_channel} + {payment_method && payment_method.length > 0 &&
({payment_method})
} -
- ):'N/A'), - }, - { - title: t('bill.title_bill_status'), - dataIndex: 'status', - width: 150, - render: value => billStatusText(value), - }, - ] - if (props.type != 'reconciliation') { - cols.push({ - title: t('bill.title_reconciliation_status'), - dataIndex: 'apply_status', - width: 150, - render: value => applyStatusText(value), - }) - } - if (props.operationRender) { - cols.push({ - title: t('bill.title_operate'), - dataIndex: 'operate', - fixed: 'right', - width: props.operationRenderWidth || (props.type == 'reconciliation'?120:220), - render: (_, record) => props.operationRender?.(record), - }) - } - return cols; - }, [props.operationRender, props.type, i18n.language]); +
+ ) : 'N/A'), + }, + { + title: t('bill.title_bill_status'), + dataIndex: 'status', + width: 150, + render: value => billStatusText(value), + }, + ] + if (props.type != 'reconciliation') { + cols.push({ + title: t('bill.title_reconciliation_status'), + dataIndex: 'apply_status', + width: 150, + render: value => applyStatusText(value), + }) + } + return cols; + }, [props.type, i18n.language]) - const isExpired = (bill: BillModel) => { - return bill.status == BillStatus.PENDING && dayjs(bill.expiration_time).isBefore(Date.now()) - } - const currentList = useMemo(()=>{ - const originList = props.source?.list || []; - originList.forEach(s => { - if(isExpired(s)){ - s.status = BillStatus.EXPIRED; - } - }) - const _total = originList.map(s=>Number(s.amount)).reduce((s, c) => (s + c), 0) - setCurrentTotalAmount(_total) - return originList; - },[props.source]) + const columns = useMemo[]>(() => { + const _cols = clone(allCols); + const cols = state.showCols.length == 0 ? _cols : _cols.filter((it: ColumnProps) => !it.dataIndex || state.showCols.includes(it.dataIndex)) + if (props.operationRender) { + cols.push({ + title: t('bill.title_operate'), + dataIndex: 'operate', + fixed: 'right', + width: props.operationRenderWidth || (props.type == 'reconciliation' ? 120 : 220), + render: (_, record) => props.operationRender?.(record), + }) + } + return cols; + }, [props.operationRender, props.type, i18n.language, allCols, state.showCols]); - return - {props.beforeTotalAmount} -
-
- {t('bill.query_amount_total')} : - -
-
- {t('bill.query_amount_current_page')} : - -
+ const isExpired = (bill: BillModel) => { + return bill.status == BillStatus.PENDING && dayjs(bill.expiration_time).isBefore(Date.now()) + } + + const currentList = useMemo(() => { + const originList = props.source?.list || []; + originList.forEach(s => { + if (isExpired(s)) { + s.status = BillStatus.EXPIRED; + } + }) + const _total = originList.map(s => Number(s.amount)).reduce((s, c) => (s + c), 0) + setCurrentTotalAmount(_total) + return originList; + }, [props.source]) + + return + {t('bill.title_bill_list')} + setState({showColumnsConfig: true})}> + + + } + headerRight={ + {props.beforeTotalAmount} +
+
+ {t('bill.query_amount_total')} : + +
+
+ {t('bill.query_amount_current_page')} : + +
+
+
} + > + {state.showColumnsConfig &&
+
+ setState({showCols})}> + {allCols.map((it) => { + return ({it.title as ReactNode}) + })} +
- } - > -
- - bordered - columns={columns} - dataSource={currentList} - rowKey={'id'} - pagination={{ - currentPage: props.source?.pagination.current, - pageSize: props.source?.pagination.pageSize, - total: props.source?.pagination.total, - onPageChange: props.onPageChange, - formatPageText: (params) => ( -
- {props.tableFooter} - {props.source && props.source.pagination.recordTotal > 0 && {t('page.record-show',params)}} -
- ) - }} - loading={props.loading} - rowSelection={props.onRowSelection ? { - fixed: true, - onChange: (selectedRowKeys) => { - selectedRowKeys && props.onRowSelection?.(selectedRowKeys) - } - } : undefined} - /> -
- +
+ + + +
+ +
} +
+ + bordered + columns={columns} + dataSource={currentList} + rowKey={'id'} + pagination={{ + currentPage: props.source?.pagination.current, + pageSize: props.source?.pagination.pageSize, + total: props.source?.pagination.total, + onPageChange: props.onPageChange, + formatPageText: (params) => ( +
+ {props.tableFooter} + {props.source && props.source.pagination.recordTotal > 0 && + {t('page.record-show', params)}} +
+ ) + }} + loading={props.loading} + rowSelection={props.onRowSelection ? { + fixed: true, + onChange: (selectedRowKeys) => { + selectedRowKeys && props.onRowSelection?.(selectedRowKeys) + } + } : undefined} + /> +
+
} \ No newline at end of file diff --git a/src/components/bill/search-form.tsx b/src/components/bill/search-form.tsx index 9f7368f..7cbfaa1 100644 --- a/src/components/bill/search-form.tsx +++ b/src/components/bill/search-form.tsx @@ -4,7 +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"; +import {useBillTypes} from "@/hooks/useBillTypes.ts"; type SearchFormProps = { onSearch?: (params: BillQueryParams) => void; @@ -27,6 +27,7 @@ type SearchFormFields = { sort_by?: string; } const SearchForm: React.FC = (props) => { + const BillTypes = useBillTypes() const formSubmit = (value: SearchFormFields) => { const params: BillQueryParams = {} diff --git a/src/components/logo/index.tsx b/src/components/logo/index.tsx index ab12516..7e34566 100644 --- a/src/components/logo/index.tsx +++ b/src/components/logo/index.tsx @@ -105,4 +105,16 @@ export const IconReconciliation = ({style}: { style?: React.CSSProperties }) => +) + +export const IconPermission = ({style}: { style?: React.CSSProperties }) => ( + + + + ) \ No newline at end of file diff --git a/src/hooks/useBillTypes.ts b/src/hooks/useBillTypes.ts new file mode 100644 index 0000000..b4c1237 --- /dev/null +++ b/src/hooks/useBillTypes.ts @@ -0,0 +1,26 @@ +import {useEffect, useState} from "react"; +import {selectBillTypeList} from "@/service/api/bill.ts"; + + +type BillTypeItem = { + value: string; + label: string; +} + +const BillTypesCache:BillTypeItem[] = []; +export function useBillTypes(){ + const [BillTypes,setBillTypes] = useState(BillTypesCache) + + 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.push(...types) + }) + } + },[]) + + return BillTypes +} \ No newline at end of file diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json index 707ed08..e1e97d1 100644 --- a/src/i18n/translations/en.json +++ b/src/i18n/translations/en.json @@ -1,5 +1,6 @@ { "base": { + "add": "Add", "bill_number": "Bill Number", "btn_search_submit": "Search", "cancel": "Cancel", @@ -12,6 +13,8 @@ "please_select": "Please Select", "qr-code": "QRCode", "query_bill": "Failed to query bill:", + "remove": "Remove", + "save": "Save", "student_number": "Student Number" }, "bill": { @@ -22,6 +25,7 @@ "cancel_confirm_bills": "Confirm the check check bill?", "cancel_success": "Successful cancel bill", "confirm": "Check", + "confirm_amount_exceed_content": "Amount exceeds total amount", "confirm_batch": "Batch Confirm", "confirm_bill": "Confirm Bill Information", "confirm_bill_number": "Confirm Bill Number", @@ -35,6 +39,7 @@ "download-qr-code": "Download QR Code", "download_receipt": "Download receipt", "export_excel": "Export Excel", + "import_bill": "Add Bill", "import_excel": "Import Bill", "paid": "Paid", "paid_confirm": "Please confirm the order status is set to paid", @@ -90,7 +95,8 @@ "menu": { "bill": "Bill Query", "check": "Reconciliation", - "manual": "Manual Pay" + "manual": "Manual Pay", + "permission": "Permission" } }, "login": { diff --git a/src/i18n/translations/sc.json b/src/i18n/translations/sc.json index 7564236..dc05ebd 100644 --- a/src/i18n/translations/sc.json +++ b/src/i18n/translations/sc.json @@ -1,5 +1,6 @@ { "base": { + "add": "增加", "bill_number": "账单编号", "btn_search_submit": "搜索", "cancel": "取消", @@ -12,6 +13,8 @@ "please_select": "请选择", "qr-code": "二维码", "query_bill": "查询账单失败:", + "remove": "删除", + "save": "保存", "student_number": "学号" }, "bill": { @@ -22,6 +25,7 @@ "cancel_confirm_bills": "确认对账选中账单?", "cancel_success": "作废账单成功", "confirm": "对账", + "confirm_amount_exceed_content": "金额超出总金额", "confirm_batch": "批量对账", "confirm_bill": "确认账单信息", "confirm_bill_number": "确认账单编号", @@ -35,6 +39,7 @@ "download-qr-code": "下载二维码", "download_receipt": "下载收据", "export_excel": "导出账单", + "import_bill": "添加账单", "import_excel": "导入账单", "paid": "已支付", "paid_confirm": "是否将此订单状态设为已支付", @@ -62,9 +67,9 @@ "title_bill_type_confirm": "确认账单", "title_confirm_status": "确认状态", "title_create_at": "创建时间", - "title_delivered_at": "到账时间", + "title_delivered_at": "交付学院时间", "title_department": "学系", - "title_initiated_paid_at": "开始支付时间", + "title_initiated_paid_at": "渠道支付时间", "title_operate": "操作", "title_paid_at": "支付时间", "title_pay_amount": "应付金额", @@ -90,7 +95,8 @@ "menu": { "bill": "账单查询", "check": "对账", - "manual": "现场支付" + "manual": "现场支付", + "permission": "权限管理" } }, "login": { diff --git a/src/i18n/translations/tc.json b/src/i18n/translations/tc.json index 1435007..0d6d633 100644 --- a/src/i18n/translations/tc.json +++ b/src/i18n/translations/tc.json @@ -1,5 +1,6 @@ { "base": { + "add": "增加", "bill_number": "帳單編號", "btn_search_submit": "搜尋", "cancel": "取消", @@ -12,6 +13,8 @@ "please_select": "請選擇", "qr-code": "QRCode", "query_bill": "查詢帳單失敗:", + "remove": "刪除", + "save": "儲存", "student_number": "學號" }, "bill": { @@ -22,6 +25,7 @@ "cancel_confirm_bills": "確認對帳選取帳單?", "cancel_success": "作廢帳單成功", "confirm": "對帳", + "confirm_amount_exceed_content": "金額超出總金額", "confirm_batch": "批次對帳", "confirm_bill": "確認帳單資訊", "confirm_bill_number": "確認帳單編號", @@ -35,6 +39,7 @@ "download-qr-code": "下載二維碼", "download_receipt": "下載收據", "export_excel": "導出賬單", + "import_bill": "新增帳單", "import_excel": "導入賬單", "paid": "已支付", "paid_confirm": "是否將此訂單狀態設為已支付", @@ -64,7 +69,7 @@ "title_create_at": "創建時間", "title_delivered_at": "到帳時間", "title_department": "學系", - "title_initiated_paid_at": "開始支付時間", + "title_initiated_paid_at": "渠道支付時間", "title_operate": "操作", "title_paid_at": "付款時間", "title_pay_amount": "應付金額", @@ -90,7 +95,8 @@ "menu": { "bill": "帳單查詢", "check": "對帳", - "manual": "現場支付" + "manual": "現場支付", + "permission": "權限管理" } }, "login": { diff --git a/src/pages/auth/permission.tsx b/src/pages/auth/permission.tsx new file mode 100644 index 0000000..2fbc298 --- /dev/null +++ b/src/pages/auth/permission.tsx @@ -0,0 +1,88 @@ +import {Card} from "@/components/card"; +import {useSetState} from "ahooks"; +import {Button, Space, Spin, TagInput, Toast} from "@douyinfe/semi-ui"; +import {useTranslation} from "react-i18next"; +import {useEffect} from "react"; +import {getPermissionList, savePermissionList} from "@/service/api/user.ts"; + +const DEFAULT_ROLES = [ + 'root','ro','fo' +] + +const Permission = ()=>{ + const {t} = useTranslation() + const [state,setState] = useSetState<{ + list:PermissionUserList[]; + loading?:boolean; + }>({ + list:[] + }) + const onUsernameChange = (role_name:string,username_list: string[])=>{ + const index = state.list.findIndex(it=>it.role_name == role_name) + if(index != -1){ + state.list[index] = { + role_name,username_list + }; + setState({ + list:state.list + }) + } + } + const saveRoles = ()=>{ + setState({ + loading:true + }) + savePermissionList(state.list).then(()=>{ + Toast.success({content:`Save Success`,duration: 3,}) + }).catch(e=>{ + Toast.error({ + content:`Save Error:${e.message}`, + duration: 3, + }) + }).finally(()=>{ + setState({loading:false}) + }) + } + const loadAllPermission = ()=>{ + setState({ + loading:true + }) + getPermissionList().then(list=>{ + const roles:{ + [key:string]:string[] + } = {} + // array to object + list.forEach(it=>{ + roles[it.role_name] = it.username_list + }) + const permissionList:PermissionUserList[] = []; + DEFAULT_ROLES.forEach(role_name=>{ + permissionList.push({ + role_name,username_list: roles[role_name] + }) + }) + setState({list:permissionList}) + }).finally(()=>{ + setState({loading:false}) + }) + } + useEffect(loadAllPermission,[]) + + return ( + {state.list.map(it=>(
+
{it.role_name.toUpperCase()}
+ onUsernameChange(it.role_name,users)} + /> +
))} + + +
{state.message||''}
+
+
) +} +export default Permission \ No newline at end of file diff --git a/src/pages/bill/components/add_bill_modal.tsx b/src/pages/bill/components/add_bill_modal.tsx new file mode 100644 index 0000000..f67ec01 --- /dev/null +++ b/src/pages/bill/components/add_bill_modal.tsx @@ -0,0 +1,137 @@ +import {BillDetailItems} from "@/components/bill"; +import {Button, Col, Form, Modal, Row, Select, Space} from "@douyinfe/semi-ui"; +import React from "react"; +import {useTranslation} from "react-i18next"; +import {useSetState} from "ahooks"; +import {useBillTypes} from "@/hooks/useBillTypes.ts"; + +type BillPaidModalProps = { + onConfirm: () => void + onCancel?: () => void +} +export const AddBillModal: React.FC = (props) => { + const {t} = useTranslation() + const BillTypes = useBillTypes() + + const [state, setState] = useSetState<{ + loading?: boolean; + open?:boolean + }>({}) + + + const onSubmit = (values: BillUpdateParams) => { + setState({ + loading: true + }) + } + return (<> + + setState({open:false})} + footer={null} + width={600} + okText={t('base.confirm')} + maskClosable={false} + > + + onSubmit={onSubmit} initValues={{ + payment_channel: 'FLYWIRE', + payment_method: '', + merchant_ref: props.bill?.merchant_ref, + payment_amount: props.bill?.amount, + actual_payment_amount: props.bill?.amount + }}> + + + + { + BillTypes.map((it, idx) => ( + {it.label})) + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/*

{t('bill.paid_confirm')}

*/} +
+ + + + +
+ +
+ ) +} \ No newline at end of file diff --git a/src/pages/bill/components/bill_type_confirm.tsx b/src/pages/bill/components/bill_type_confirm.tsx index 7bac267..dfac250 100644 --- a/src/pages/bill/components/bill_type_confirm.tsx +++ b/src/pages/bill/components/bill_type_confirm.tsx @@ -1,57 +1,216 @@ -import {Button, Select, Popconfirm, Space, Tag} from "@douyinfe/semi-ui"; -import React, {useState} from "react"; +import {Button, Select, Space, Divider, InputNumber, Modal, Toast} from "@douyinfe/semi-ui"; +import React, {useMemo} from "react"; import {useSetState} from "ahooks"; import MoneyFormat from "@/components/money-format.tsx"; -import {confirmBillType} from "@/service/api/bill.ts"; import {useTranslation} from "react-i18next"; -import {BillTypes} from "@/service/bill-types.ts"; +import {useBillTypes} from "@/hooks/useBillTypes.ts"; +import {NumberConfirm} from "@/pages/bill/components/number_confirm.tsx"; +import {BillDetailItems} from "@/components/bill"; +import {BillDetailItem} from "@/components/bill/bill-detail-items.tsx"; +import {IconStudentId} from "@/components/icons"; +import {confirmBillType} from "@/service/api/bill.ts"; type BillTypeConfirmProps = { - data: BillDetail + bill: BillModel; + onClose?: (refresh?: boolean) => void; + onChange?: (confirms: ConfirmedBillDetail[]) => void; +} + +const BillTypeConfirmItem = (props: { data: BillDetail; onChange: (confirms: ConfirmedBillDetail[]) => void; }) => { + const it = props.data; + const BillTypes = useBillTypes() + const {t} = useTranslation() + + const [state, setState] = useSetState<{ + billTypeList: ConfirmedBillDetail[]; + loading?: boolean, + confirmed?: boolean, + }>({ + loading: false, + billTypeList: [ + {bill_type: props.data.bill_type, bill_detail_id: it.id, amount: Number(props.data.amount)} + ] + }) + const onChange = (value: string, index: number, type: 'type' | 'amount') => { + if (state.billTypeList.length <= index || !value) return; + const billTypeList = [...state.billTypeList] + if (type == 'type') { + billTypeList[index].bill_type = value + } else { + billTypeList[index].amount = Number(value) + } + + // 计算 confirmedTypes 中所有 amount 的总金额 + const totalAmount = billTypeList.reduce((total, item) => { + return total + Number(item.amount) + }, 0) + // 判断是否已经超出账单实际金额 + if (totalAmount > Number(it.amount)) { + Modal.warning({ + title: 'Warning', + content: t('bill.confirm_amount_exceed_content'), + hasCancel: false + }) + return; + } + setState({billTypeList}) + props.onChange(billTypeList) + } + const onRemove = (index: number) => { + if (state.billTypeList.length <= 1) return; + const billTypeList = [...state.billTypeList] + billTypeList.splice(index, 1) + setState({billTypeList}) + } + const onAdd = () => { + const billTypeList = [ + ...state.billTypeList, + {bill_type: props.data.bill_type, amount: 0, bill_detail_id: it.id} + ] + setState({ + billTypeList + }) + props.onChange(billTypeList) + } + + return (<> +
+
+
{it.bill_type}
+ Total Amount: + +
+ {state.billTypeList.map((item, index) => { + return ( +
+ + + + onChange(String(v), index, 'amount')} style={{width: 120}}/> + + +
) + })} +
+ +
+
+ + ) } export const BillTypeConfirm: React.FC = (props) => { - const [it,setItem] = useState(props.data) const {t} = useTranslation() - const [state,setState] = useSetState({ - loading:false, - bill_type: props.data.bill_type + const [state, setState] = useSetState<{ + confirm_application_number: string; + confirmed: { + [key: number]: ConfirmedBillDetail[] + } + }>({ + confirm_application_number: '', confirmed: {} }) - const onConfirmBill = () => { - setState({loading:true}) - confirmBillType({id:it.id,type:state.bill_type}).then(() => { - setState({loading:false}) - setItem({...it,confirm_status:'CONFIRMED'}) - }).catch(() => { - setState({loading:false}) - }) + const details = useMemo(() => { + const {details, detail_confirms} = props.bill; + if (!details) return []; + details.forEach(it => { + if (!detail_confirms) it.confirmed = []; + else it.confirmed = detail_confirms.filter(s => s.bill_detail_id == it.id) + }) + return details; + }, [props.bill]) + + const onChange = (id: number, confirmedTypes: ConfirmedBillDetail[]) => { + const confirmed = { + ...state.confirmed + }; + confirmed[id] = confirmedTypes + setState({confirmed}) + // trigger + const allConfirmed: ConfirmedBillDetail[] = []; + Object.keys(confirmed).forEach(key => { + allConfirmed.push(...confirmed[Number(key)]) + }) + props.onChange?.(allConfirmed) } - return
-
-
{it.bill_type}
-
- -
+ + return (<> + Bill Type Confirm + { + details.map((it, idx) => ( onChange(it.id, confirmed)} data={it} key={idx}/>)) + } + ) +} + +export const BillTypeConfirmModal: React.FC = (props) => { + const {t} = useTranslation() + const [state, setState] = useSetState<{ + confirm_application_number: string; + detail_confirms: ConfirmedBillDetail[]; + loading?: boolean; + }>({ + confirm_application_number: '', + detail_confirms: [] + }) + + const onBillConfirm = () => { + setState({loading: true}) + confirmBillType([{ + id: props.bill.id, + confirm_student_number: props.bill.student_number, + ...state + }]).then(() => { + props.onClose?.(true) + }).finally(() => { + setState({loading: false}) + }) + } + return ( props.onClose?.()} + > +
+ + } title={t('manual.student_number')} + value={props.bill.student_number || '-'}/> + } title={t('base.bill_number')} + value={props.bill.application_number || '-'}/> + }/>
-
- - {it.confirm_status != 'CONFIRMED' && } - { - it.confirm_status == 'CONFIRMED' ? {state.bill_type} : <> - onConfirmBill()} - position={'topRight'} - content={`${t('bill.confirm_bill_type')}?`} - > - - } - +
+ Bill Number Confirm + {/*{*/} + {/* !state.confirmBill.student_number_confirm &&*/} + {/* */} + {/*}*/} + setState({confirm_application_number})} + bill={props.bill} type={'application_number'}/>
-
+ setState({detail_confirms})}/> +
+ +
+
) } \ No newline at end of file diff --git a/src/pages/bill/components/number_confirm.tsx b/src/pages/bill/components/number_confirm.tsx index bb9ddd1..c32a6e2 100644 --- a/src/pages/bill/components/number_confirm.tsx +++ b/src/pages/bill/components/number_confirm.tsx @@ -1,55 +1,52 @@ -import {Button, Space, Tag, Input} from "@douyinfe/semi-ui"; -import React from "react"; +import {Space, Input} from "@douyinfe/semi-ui"; +import React, {useEffect} from "react"; import {useSetState} from "ahooks"; import {useTranslation} from "react-i18next"; type NumberConfirmProps = { bill: BillModel; - type: 'student_number' | 'application_number' + type: 'student_number' | 'application_number'; + onChange: (value: string) => void; } -export const NumberConfirm: React.FC = (props) => { +export const NumberConfirm: React.FC = ({bill, type, onChange}) => { const {t} = useTranslation() + const [state, setState] = useSetState({ loading: false, confirmed: false, - confirmNumber: (props.type == 'application_number' ? props.bill.application_number : props.bill.student_number) || '', + confirmNumber: '', }) - 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}) - // }) + const onValueChange = (confirmNumber: string) => { + setState({confirmNumber}) + onChange(confirmNumber) } + useEffect(() => { + const confirmNumber = (type == 'application_number' ? (bill.application_number_confirm || bill.application_number) : bill.student_number) || ''; + onValueChange(confirmNumber) + }, []) + return
+ style={{marginBottom: 15, marginTop: 15}}>
-
{t(props.type == 'student_number' ? 'bill.confirm_student_number' : 'bill.confirm_bill_number')}
+
{t(type == 'student_number' ? 'bill.confirm_student_number' : 'bill.confirm_bill_number')}
- {!state.confirmed && setState({confirmNumber: String(v)})} - defaultValue={state.confirmNumber} - style={{width: 180}} placeholder={t('base.please_enter')}/>} - { - state.confirmed ? -
{state.confirmNumber}
- CONFIRMED -
: - } + + + {/*{*/} + {/* state.confirmed ? */} + {/*
{state.confirmNumber}
*/} + {/* CONFIRMED*/} + {/*
: {t('base.confirm')}*/} + {/*}*/}
diff --git a/src/pages/bill/query.tsx b/src/pages/bill/query.tsx index 5ed6ea3..c18df83 100644 --- a/src/pages/bill/query.tsx +++ b/src/pages/bill/query.tsx @@ -1,4 +1,4 @@ -import {Button, ButtonGroup, Divider, Modal, Notification, Popconfirm, Toast} from "@douyinfe/semi-ui"; +import {Button, ButtonGroup, Modal, Notification, Popconfirm, Space, Toast} from "@douyinfe/semi-ui"; import {useState} from "react"; import {useRequest, useSetState} from "ahooks"; import {useTranslation} from "react-i18next"; @@ -9,13 +9,10 @@ import BillDetail from "@/components/bill/detail.tsx"; import {billList, BillQueryParams, exportBillList, modifyBillStatus} from "@/service/api/bill.ts"; import {BillStatus, BizError} from "@/service/types.ts"; import {useDownloadReceiptPDF} from "@/service/generate-pdf.ts"; -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 {BillTypeConfirmModal} 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"; +import {AddBillModal} from "@/pages/bill/components/add_bill_modal.tsx"; const DownloadButton = ({bill, text}: { bill: BillModel; text: string }) => { @@ -116,12 +113,17 @@ const BillQuery = () => { console.log(selectKeys) } + const onBillConfirm = (reload?: boolean) => { + setState({confirmBill: undefined}) + if (reload) refresh() + } + return (
+ beforeTotalAmount={ { (selectKeys.length == 0) ? : { } - - - } + + + + + + } onRowSelection={setSelectedKeys} onPageChange={(page_number) => { @@ -164,44 +169,7 @@ const BillQuery = () => { refresh() }} /> - { - refresh() - setState({confirmBill: undefined}) - }} - width={550} - footer={null} - > - {state.confirmBill && <> -
- } title={t('manual.student_number')} - value={state.confirmBill.student_number || '-'}/> - } title={t('base.bill_number')} - value={state.confirmBill.application_number || '-'}/> - }/>
-
- { - !state.confirmBill.student_number_confirm && - - } - { - !state.confirmBill.application_number_confirm && - - } - { - state.confirmBill.details.map((it, idx) => (
- - -
)) - } -
- } -
+ {state.confirmBill && }
) } export default BillQuery \ No newline at end of file diff --git a/src/pages/manual/index.tsx b/src/pages/manual/index.tsx index ac0dc1c..0d352fa 100644 --- a/src/pages/manual/index.tsx +++ b/src/pages/manual/index.tsx @@ -3,7 +3,7 @@ import {useTranslation} from "react-i18next"; import {useRef, useState} from "react"; import {Card} from "@/components/card"; -import {BillTypes} from "@/service/bill-types.ts"; +import {useBillTypes} from "@/hooks/useBillTypes.ts"; import {BillDetailItems, useBillQRCode} from "@/components/bill"; import styles from './manual.module.less' @@ -43,6 +43,7 @@ export default function Index() { // useEffect(()=>{ // getBillDetail(100009).then(setBillInfo); // },[]) + const BillTypes = useBillTypes() const BillInfo = ({bill}: { bill?: BillModel }) => { if (!bill) return null; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index b0e60cc..42c36be 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -21,6 +21,7 @@ import ManualIndex from "@/pages/manual/index.tsx"; import BillQuery from "@/pages/bill/query.tsx"; import BillReconciliation from "@/pages/bill/reconciliation.tsx"; import ExternalCreate from "@/pages/bill/external_create.tsx"; +import Permission from "@/pages/auth/permission.tsx"; const routes: RouteObject[] = [ @@ -76,6 +77,10 @@ const routes: RouteObject[] = [ path: 'reconciliation', element: }, + { + path: 'permission', + element: + }, ] }, ] diff --git a/src/routes/layout/dashboard-navigation.tsx b/src/routes/layout/dashboard-navigation.tsx index 801035d..f43de70 100644 --- a/src/routes/layout/dashboard-navigation.tsx +++ b/src/routes/layout/dashboard-navigation.tsx @@ -3,7 +3,7 @@ import {useMemo} from "react"; import {useTranslation} from "react-i18next"; import useAuth from "@/hooks/useAuth.ts"; -import {IconQRCode, IconQuery, IconReconciliation} from "@/components/logo"; +import {IconPermission, IconQRCode, IconQuery, IconReconciliation} from "@/components/logo"; export const AllDashboardMenu = [ { @@ -22,6 +22,12 @@ export const AllDashboardMenu = [ icon: , path: '/dashboard/reconciliation', role: ['root', 'fo'] + }, + { + key: 'permission', + icon: , + path: '/dashboard/permission', + role: ['root'] } ] diff --git a/src/service/api/bill.ts b/src/service/api/bill.ts index 50a82ee..0d32a2a 100644 --- a/src/service/api/bill.ts +++ b/src/service/api/bill.ts @@ -48,6 +48,10 @@ export function createManualBill(params: ManualCreateBillParam) { return post('/manual_payment', params) } +export function selectBillTypeList(){ + return get('/billing_types') +} + // 获取账单详情 export function getBillDetail(id: number) { return get('/bills/' + id) @@ -66,8 +70,8 @@ export function modifyBillStatus(id: number,status: BillStatus) { return put(`/bills/${id}/cancel`,{status}) } -export function confirmBillType({id,type}: {id:number,type: string}) { - return post(`/bill/detail/${id}/confirm`, {confirm_type:type}) +export function confirmBillType(bills:BillConfirmParams[]) { + return post(`/bills/confirm`, {bills}) } export function confirmBills(bill_ids: number[]) { diff --git a/src/service/api/user.ts b/src/service/api/user.ts index 6c4c714..c4b5765 100644 --- a/src/service/api/user.ts +++ b/src/service/api/user.ts @@ -11,4 +11,11 @@ export function getUserInfo() { */ export function auth(code:string,state:string){ return post('/auth', {code, state}) +} + +export function getPermissionList(){ + return get('/roles') +} +export function savePermissionList(roles:PermissionUserList[]){ + return post('/roles',{roles}) } \ No newline at end of file diff --git a/src/service/generate-pdf.ts b/src/service/generate-pdf.ts index cf7aba9..0dab758 100644 --- a/src/service/generate-pdf.ts +++ b/src/service/generate-pdf.ts @@ -61,7 +61,7 @@ export function GeneratePdf(bill: BillModel) { ...(bill.details.map(it=>{ return [ `#${it.id}`, - dayjs(bill.paid_at).format('YYYY-MM-DD'), + bill.paid_at?dayjs(bill.paid_at).format('YYYY-MM-DD'):'', it.bill_type, `${bill.payment_channel}` + (bill.payment_method && bill.payment_channel != bill.payment_method ? `(${bill.payment_method})` : ''), `${it.amount}` diff --git a/src/types/auth.d.ts b/src/types/auth.d.ts index f894438..9d298eb 100644 --- a/src/types/auth.d.ts +++ b/src/types/auth.d.ts @@ -31,4 +31,9 @@ declare type AuthContextType = { mockLogin: () => Promise; login: (code:string,state:string) => Promise; updateUser: (user:Partial) => Promise; -}; \ No newline at end of file +}; + +declare type PermissionUserList = { + role_name:string; + username_list: string[]; +} \ No newline at end of file diff --git a/src/types/bill.d.ts b/src/types/bill.d.ts index 929cf52..5f7227b 100644 --- a/src/types/bill.d.ts +++ b/src/types/bill.d.ts @@ -13,8 +13,14 @@ declare type ManualCreateBillParam = { declare type BillDetail = { id: number; bill_type: string; - confirm_status: ConfirmStatus; - confirm_type: string; + amount: decimal; + confirmed?: ConfirmedBillDetail[]; +} + +declare type ConfirmedBillDetail = { + id?: number; + bill_detail_id: number; + bill_type: string; amount: decimal; } /** @@ -38,6 +44,10 @@ declare type BillQueryParam = { sort_field:string; sort_order:SortOrderType; } +declare type BillType = { + type: string; + description: string; +} /** * 账单模型 */ @@ -46,7 +56,7 @@ declare type BillModel = { student_number: string; student_number_confirm?: string; application_number: null | string; - application_number_confirm?: null | string; + confirm_application_number?: null | string; student_email: string; student_tc_name?: string; student_sc_name?: string; @@ -80,6 +90,7 @@ declare type BillModel = { remark: string; confirm_status: ConfirmStatus; details: BillDetail[] + detail_confirms: ConfirmedBillDetail[] | null } @@ -109,4 +120,15 @@ type BillUpdateParams = { remark?: string; merchant_ref?: string; payment_amount?: number | string; +} + +type BillTypeConfirm = { + bill_type: string; + amount: number; +} +type BillConfirmParams = { + id:number; + confirm_application_number:string; + confirm_student_number:string; + detail_confirms:ConfirmedBillDetail[] } \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 83e68da..3c7d82b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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/, '') }