diff --git a/frontend/packages/ui/src/components/Select/index.tsx b/frontend/packages/ui/src/components/Select/index.tsx index 81aac5f83f41..c023a0ff48cc 100644 --- a/frontend/packages/ui/src/components/Select/index.tsx +++ b/frontend/packages/ui/src/components/Select/index.tsx @@ -7,7 +7,8 @@ import { Button, useDisclosure, useOutsideClick, - MenuButton + MenuButton, + Flex } from '@chakra-ui/react'; import type { ButtonProps } from '@chakra-ui/react'; import { ChevronDownIcon } from '@chakra-ui/icons'; @@ -68,7 +69,7 @@ const MySelect = ( ref={ref} display={'flex'} alignItems={'center'} - justifyContent={'space-between'} + justifyContent={'center'} border={'1px solid #E8EBF0'} borderRadius={'md'} fontSize={'12px'} @@ -93,15 +94,7 @@ const MySelect = ( })} {...props} > - {activeMenu ? ( - <> - {activeMenu.label} - > - ) : ( - <> - {placeholder} - > - )} + {activeMenu ? activeMenu.label : placeholder} { if (onchange && value !== item.value) { onchange(item.value); diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 1495f79e0836..889f56492283 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -847,6 +847,9 @@ importers: '@kubernetes/client-node': specifier: 0.18.0 version: 0.18.0 + '@larksuiteoapi/node-sdk': + specifier: ^1.32.0 + version: 1.32.0 '@sealos/ui': specifier: workspace:^ version: link:../../packages/ui @@ -895,6 +898,9 @@ importers: js-yaml: specifier: ^4.1.0 version: 4.1.0 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -944,6 +950,9 @@ importers: '@types/js-yaml': specifier: ^4.0.6 version: 4.0.9 + '@types/jsonwebtoken': + specifier: ^9.0.3 + version: 9.0.5 '@types/lodash': specifier: ^4.14.199 version: 4.14.202 @@ -7371,6 +7380,23 @@ packages: - utf-8-validate dev: false + /@larksuiteoapi/node-sdk@1.32.0: + resolution: {integrity: sha512-Ix5gPyD0Qk3rvH97r86LyYH3VnWPESrendyNq1iMusC0VIKAjeu/J0ieVKXwvhntyeNEDGraaqh8hj1vRUFt5Q==} + dependencies: + axios: 0.28.1 + lodash.get: 4.4.2 + lodash.identity: 3.0.0 + lodash.merge: 4.6.2 + lodash.pick: 4.4.0 + lodash.pickby: 4.6.0 + protobufjs: 7.3.2 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + dev: false + /@lezer/common@1.1.1: resolution: {integrity: sha512-aAPB9YbvZHqAW+bIwiuuTDGB4DG0sYNRObGLxud8cW7osw1ZQxfDuTZ8KQiqfZ0QJGcR34CvpTMDXEyo/+Htgg==} dev: false @@ -8493,6 +8519,49 @@ packages: '@babel/runtime': 7.24.0 dev: false + /@protobufjs/aspromise@1.1.2: + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + dev: false + + /@protobufjs/base64@1.1.2: + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + dev: false + + /@protobufjs/codegen@2.0.4: + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + dev: false + + /@protobufjs/eventemitter@1.1.0: + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + dev: false + + /@protobufjs/fetch@1.1.0: + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + dev: false + + /@protobufjs/float@1.0.2: + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + dev: false + + /@protobufjs/inquire@1.1.0: + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + dev: false + + /@protobufjs/path@1.1.2: + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + dev: false + + /@protobufjs/pool@1.1.0: + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + dev: false + + /@protobufjs/utf8@1.1.0: + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + dev: false + /@rc-component/color-picker@1.5.2(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-YJXujYzYFAEtlXJXy0yJUhwzUWPTcniBZto+wZ/vnACmFnUTNR7dH+NOeqSwMMsssh74e9H5Jfpr5LAH2PYqUw==} peerDependencies: @@ -10852,6 +10921,16 @@ packages: resolution: {integrity: sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==} engines: {node: '>=4'} + /axios@0.28.1: + resolution: {integrity: sha512-iUcGA5a7p0mVb4Gm/sy+FSECNkPFT4y7wt6OM/CDpO/OnNCvSs3PoMG8ibrC9jRoGYU0gUK5pXVC4NPXq6lHRQ==} + dependencies: + follow-redirects: 1.15.3 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /axios@1.2.1: resolution: {integrity: sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==} dependencies: @@ -16534,6 +16613,14 @@ packages: /lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + /lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + dev: false + + /lodash.identity@3.0.0: + resolution: {integrity: sha512-AupTIzdLQxJS5wIYUQlgGyk2XRTfGXA+MCghDHqZk0pzUNYvd3EESS6dkChNauNYVIutcb0dfHw1ri9Q1yPV8Q==} + dev: false + /lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} dev: false @@ -16569,6 +16656,14 @@ packages: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} dev: false + /lodash.pick@4.4.0: + resolution: {integrity: sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==} + dev: false + + /lodash.pickby@4.6.0: + resolution: {integrity: sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==} + dev: false + /lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} dev: false @@ -16583,6 +16678,10 @@ packages: chalk: 2.4.2 dev: true + /long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + dev: false + /longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} dev: false @@ -18576,6 +18675,25 @@ packages: resolution: {integrity: sha512-9t5qARVofg2xQqKtytzt+lZ4d1Qvj8t5B8fEwXK6qOfgRLgH/b13QlgEyDh033NOS31nXeFbYv7CLUDG1CeifQ==} dev: false + /protobufjs@7.3.2: + resolution: {integrity: sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==} + engines: {node: '>=12.0.0'} + requiresBuild: true + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 20.10.0 + long: 5.2.3 + dev: false + /protocol-buffers-schema@3.6.0: resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} dev: false @@ -22272,6 +22390,19 @@ packages: utf-8-validate: optional: true + /ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} diff --git a/frontend/providers/costcenter/package.json b/frontend/providers/costcenter/package.json index 94120473469c..1836d8ed45f8 100644 --- a/frontend/providers/costcenter/package.json +++ b/frontend/providers/costcenter/package.json @@ -20,6 +20,7 @@ "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@kubernetes/client-node": "0.18.0", + "@larksuiteoapi/node-sdk": "^1.32.0", "@sealos/ui": "workspace:^", "@stripe/stripe-js": "^1.54.2", "@tanstack/query-sync-storage-persister": "^4.35.3", @@ -36,6 +37,7 @@ "immer": "^9.0.21", "js-cookie": "^3.0.5", "js-yaml": "^4.1.0", + "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "mongodb": "^5.9.0", "next": "13.1.6", @@ -54,6 +56,7 @@ "devDependencies": { "@types/js-cookie": "^3.0.4", "@types/js-yaml": "^4.0.6", + "@types/jsonwebtoken": "^9.0.3", "@types/lodash": "^4.14.199", "@types/node": "18.15.5", "@types/nprogress": "^0.2.1", diff --git a/frontend/providers/costcenter/public/locales/en/applist.json b/frontend/providers/costcenter/public/locales/en/applist.json index 8047f931105e..467fb03a914c 100644 --- a/frontend/providers/costcenter/public/locales/en/applist.json +++ b/frontend/providers/costcenter/public/locales/en/applist.json @@ -8,5 +8,6 @@ "APP": "APP", "All APP": "ALL APP", "APP-STORE": "APP-STORE", + "DB-BACKUP": "DB-BACKUP", "all_app_type": "ALL APP TYPE" } \ No newline at end of file diff --git a/frontend/providers/costcenter/public/locales/en/common.json b/frontend/providers/costcenter/public/locales/en/common.json index e7fa66ac53e5..4b9e30b29309 100644 --- a/frontend/providers/costcenter/public/locales/en/common.json +++ b/frontend/providers/costcenter/public/locales/en/common.json @@ -95,12 +95,14 @@ "invoiceAmount": "Invoice Amount", "invoice": "Generate Invoice", "invoiceOrder": "Invoice Order", + "invoiceRecord": "Invoice Record", "phoneValidation": "Please enter a valid phone number", "taxNumberValidation": "Please enter a valid tax registration number", "bankAccountValidation": "Please enter a valid bank account number", "emailValidation": "Please enter a valid email address", "submit success": "Submit success", "submit fail": "Submit fail", + "require": "Please enter value", "code success": "Verification code sent successfully", "code error": "Verification code sent failed", "Name": "Name", @@ -134,7 +136,15 @@ "fax": { "name": "Fax", "placeholder": "Enter fax number" - } + }, + "type":{ + "name": "Type", + "placeholder": "Enter type", + "list": { + "special": "VAT invoice (Special)", + "normal": "VAT invoice (Normal)" + } + } }, "contract": { "person": { @@ -157,8 +167,19 @@ "Invoice Details": "Invoice Details", "Invoice Content": "Invoice Content", "Electronic Computer Service Fee": "Electronic Computer Service Fee", - "Contact Information": "Contact Information" + "Contact Information": "Contact Information", + "Apply Inovice Tips": "Apply for an invoice need to 3 to 5 days", + "Apply Invoice": "Apply Invoice", + + "status":{ + "PENDING": "pending", + "COMPLETED": "completed", + "REJECTED": "rejected" + } }, + "Invoice Status": "Invoice Status", + "Invoice Create Time": "invoice create time", + "Invoice Update Time": "invoice update time", "pay with stripe": "Pay With Stripe", "pay with wechat": "Pay With Wechat", "Pay Minimum Tips": "The amount need to be more than 10 when paying with Stripe", diff --git a/frontend/providers/costcenter/public/locales/zh/applist.json b/frontend/providers/costcenter/public/locales/zh/applist.json index 49e7d9d96b0d..2f4f1a5a27e3 100644 --- a/frontend/providers/costcenter/public/locales/zh/applist.json +++ b/frontend/providers/costcenter/public/locales/zh/applist.json @@ -8,5 +8,6 @@ "APP": "应用", "All APP": "所有应用", "APP-STORE": "应用商店", + "DB-BACKUP":"数据库备份", "all_app_type": "所有应用类型" } \ No newline at end of file diff --git a/frontend/providers/costcenter/public/locales/zh/common.json b/frontend/providers/costcenter/public/locales/zh/common.json index cd8badeaf21f..3e42c48eeff8 100644 --- a/frontend/providers/costcenter/public/locales/zh/common.json +++ b/frontend/providers/costcenter/public/locales/zh/common.json @@ -94,8 +94,10 @@ "list": "订单列表", "invoiceAmount": "发票金额", "invoice": "开发票", + "invoiceRecord": "开票记录", "invoiceOrder": "发票订单", "phoneValidation": "请输入有效的手机号码", + "require": "请填写此字段", "taxNumberValidation": "请输入有效的税务登记证号", "bankAccountValidation": "请输入有效的银行账号", "emailValidation": "请输入有效的邮箱地址", @@ -131,7 +133,16 @@ "fax": { "name": "传真", "placeholder": "填写传真" - } + }, + "type": { + "name": "发票类型", + "placeholder": "填写发票类型", + "list": { + "special": "增值税专用发票", + "normal": "增值税普通发票" + + } + } }, "contract": { "person": { @@ -154,8 +165,19 @@ "Invoice Content": "发票内容", "Electronic Computer Service Fee": "电子计算机服务费", "Invoice Details": "发票明细", - "Contact Information": "联系方式" + "Contact Information": "联系方式", + "Apply Inovice Tips":"开票需要三到五个工作日处理", + "Apply Invoice": "提交开票申请", + + "status":{ + "PENDING": "开票中", + "COMPLETED": "已完成", + "REJECTED": "已拒绝" + } }, + "Invoice Status": "开票状态", + "Invoice Create Time": "申请开票时间", + "Invoice Update Time": "开票时间", "pay with wechat": "微信支付", "pay with stripe": "Stripe 支付", "Total Expenditure": "总支出", diff --git a/frontend/providers/costcenter/src/assert/article.svg b/frontend/providers/costcenter/src/assert/article.svg index a158fce88c9a..d6410cfe009e 100644 --- a/frontend/providers/costcenter/src/assert/article.svg +++ b/frontend/providers/costcenter/src/assert/article.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/frontend/providers/costcenter/src/assert/job.svg b/frontend/providers/costcenter/src/assert/job.svg index 770ad61864b2..ee910714e2cd 100644 --- a/frontend/providers/costcenter/src/assert/job.svg +++ b/frontend/providers/costcenter/src/assert/job.svg @@ -1,25 +1,17 @@ - - - - + + + - - - - - - - - - - - - - - - - + + + + + + + - - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/providers/costcenter/src/assert/left2.svg b/frontend/providers/costcenter/src/assert/left2.svg new file mode 100644 index 000000000000..204c1bacb8b8 --- /dev/null +++ b/frontend/providers/costcenter/src/assert/left2.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/providers/costcenter/src/assert/magnifyingGlass.svg b/frontend/providers/costcenter/src/assert/magnifyingGlass.svg index 9cd3847c50da..4921fe0f1c83 100644 --- a/frontend/providers/costcenter/src/assert/magnifyingGlass.svg +++ b/frontend/providers/costcenter/src/assert/magnifyingGlass.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/frontend/providers/costcenter/src/assert/mdi_email-receive-outline.svg b/frontend/providers/costcenter/src/assert/mdi_email-receive-outline.svg index 07053c84926d..138d634215bb 100644 --- a/frontend/providers/costcenter/src/assert/mdi_email-receive-outline.svg +++ b/frontend/providers/costcenter/src/assert/mdi_email-receive-outline.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/frontend/providers/costcenter/src/assert/triangle.svg b/frontend/providers/costcenter/src/assert/triangle.svg new file mode 100644 index 000000000000..6892468fea8f --- /dev/null +++ b/frontend/providers/costcenter/src/assert/triangle.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/providers/costcenter/src/components/RechargeModal.tsx b/frontend/providers/costcenter/src/components/RechargeModal.tsx index 27d63fd35a2b..ab76daa58c63 100644 --- a/frontend/providers/costcenter/src/components/RechargeModal.tsx +++ b/frontend/providers/costcenter/src/components/RechargeModal.tsx @@ -459,11 +459,8 @@ const RechargeModal = forwardRef( {stripeEnabled && ( handleWechatConfirm()} > - + {t('pay with wechat')} diff --git a/frontend/providers/costcenter/src/components/TransferModal.tsx b/frontend/providers/costcenter/src/components/TransferModal.tsx index d0c3548f1b19..3398b8992eb4 100644 --- a/frontend/providers/costcenter/src/components/TransferModal.tsx +++ b/frontend/providers/costcenter/src/components/TransferModal.tsx @@ -265,17 +265,12 @@ const TransferModal = forwardRef( {formatMoney(balance).toFixed(2)} handleConfirm()} isLoading={mutation.isLoading} > diff --git a/frontend/providers/costcenter/src/components/billing/InOutTabPanel.tsx b/frontend/providers/costcenter/src/components/billing/InOutTabPanel.tsx index 55a5484db5af..39ba90f8a0e1 100644 --- a/frontend/providers/costcenter/src/components/billing/InOutTabPanel.tsx +++ b/frontend/providers/costcenter/src/components/billing/InOutTabPanel.tsx @@ -48,21 +48,6 @@ export default function InOutTabPanel() { page, pageSize }; - // useEffect(() => { - // if (!router.query) return - // const { - // appNameIdx, - // appTypeIdx, - // namespaceIdx, - // regionIdx, - // } = router.query - // if (isNumber(appNameIdx) && isNumber(appTypeIdx) && isNumber(namespaceIdx) && isNumber(regionIdx)) { - // setRegion(regionIdx) - // setNamespace(namespaceIdx) - // setAppType(appTypeIdx) - // setAppName(appNameIdx) - // } - // }, [router]) const { data, isPreviousData, isFetching } = useQuery({ queryFn() { diff --git a/frontend/providers/costcenter/src/components/billing/RechargeTabPanel.tsx b/frontend/providers/costcenter/src/components/billing/RechargeTabPanel.tsx index 51fcc0906878..1da1d4069030 100644 --- a/frontend/providers/costcenter/src/components/billing/RechargeTabPanel.tsx +++ b/frontend/providers/costcenter/src/components/billing/RechargeTabPanel.tsx @@ -47,9 +47,9 @@ export default function RechargeTabPanel() { ); const { t } = useTranslation(); const tableResult = useMemo(() => { - if (data?.data?.payment) return data.data.payment; + if (data?.data?.payments) return data.data.payments; else return []; - }, [data?.data?.payment]); + }, [data?.data?.payments]); const currency = useEnvStore((s) => s.currency); const columns = useMemo(() => { const columnHelper = createColumnHelper(); diff --git a/frontend/providers/costcenter/src/components/cost_overview/components/quotaPieChart.tsx b/frontend/providers/costcenter/src/components/cost_overview/components/quotaPieChart.tsx index 2eaa2dac10d2..0f251236abdf 100644 --- a/frontend/providers/costcenter/src/components/cost_overview/components/quotaPieChart.tsx +++ b/frontend/providers/costcenter/src/components/cost_overview/components/quotaPieChart.tsx @@ -6,7 +6,6 @@ import { CanvasRenderer } from 'echarts/renderers'; import { useMemo } from 'react'; import { useBreakpointValue } from '@chakra-ui/react'; import { UserQuotaItemType } from '@/pages/api/getQuota'; -import { useTranslation } from 'next-i18next'; echarts.use([PieChart, CanvasRenderer, LabelLayout]); diff --git a/frontend/providers/costcenter/src/components/invoice/PaymentPanel.tsx b/frontend/providers/costcenter/src/components/invoice/PaymentPanel.tsx new file mode 100644 index 000000000000..55424c76fcdd --- /dev/null +++ b/frontend/providers/costcenter/src/components/invoice/PaymentPanel.tsx @@ -0,0 +1,80 @@ +import useOverviewStore from '@/stores/overview'; +import { useEffect, useMemo, useState } from 'react'; +import { ApiResp, RechargeBillingData, RechargeBillingItem, ReqGenInvoice } from '@/types'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import request from '@/service/request'; +import { useTranslation } from 'next-i18next'; +import { Flex, Heading, HStack, Img, TabPanel, Text, useMediaQuery } from '@chakra-ui/react'; +import SwitchPage from '@/components/billing/SwitchPage'; +import { InvoicePaymentTable } from '../table/InovicePaymentTable'; +import { END_TIME, START_TIME } from '@/constants/payment'; + +export default function PaymentPanel({ + selectbillings, + orderID, + setSelectBillings +}: { + orderID: string; + selectbillings: ReqGenInvoice['billings']; + setSelectBillings: (list: RechargeBillingItem[]) => void; +}) { + const { t } = useTranslation(); + const [page, setPage] = useState(1); + const [totalPage, setTotalPage] = useState(1); + const [totalItem, setTotalItem] = useState(0); + const [pageSize, setPageSize] = useState(10); + + // const { startTime, endTime } = useOverviewStore() + const endTime = END_TIME; + const startTime = START_TIME; + const body = { + startTime, + endTime, + page, + pageSize, + paymentID: orderID + }; + const { data } = useQuery(['billing', 'invoice', body], () => { + return request>('/api/billing/rechargeBillingList', { + data: body, + method: 'POST' + }); + }); + useEffect(() => { + if (!data?.data) { + return; + } + const { total, totalPage } = data.data; + if (totalPage === 0) { + // search reset + setTotalPage(1); + setTotalItem(1); + } else { + setTotalItem(total); + setTotalPage(totalPage); + } + if (totalPage < page) { + setPage(1); + } + }, [data?.data]); + return ( + + !item.InvoicedAt)} + setSelectBillings={setSelectBillings} + > + + + ); +} diff --git a/frontend/providers/costcenter/src/components/invoice/RecordPanel.tsx b/frontend/providers/costcenter/src/components/invoice/RecordPanel.tsx new file mode 100644 index 000000000000..c9a76fabc2de --- /dev/null +++ b/frontend/providers/costcenter/src/components/invoice/RecordPanel.tsx @@ -0,0 +1,88 @@ +import useOverviewStore from '@/stores/overview'; +import { useEffect, useMemo, useState } from 'react'; +import { + ApiResp, + APPBillingItem, + AppOverviewBilling, + BillingData, + BillingSpec, + BillingType, + InvoiceListData, + InvoicePayload, + RechargeBillingData, + RechargeBillingItem, + ReqGenInvoice +} from '@/types'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import request from '@/service/request'; +import { useTranslation } from 'next-i18next'; +import { Flex, Heading, HStack, Img, TabPanel, Text, useMediaQuery } from '@chakra-ui/react'; +import SwitchPage from '@/components/billing/SwitchPage'; +import { InvoiceTable } from '../table/InoviceTable'; + +export default function RecordPanel({ toInvoiceDetail }: { toInvoiceDetail: () => void }) { + const { t } = useTranslation(); + const [page, setPage] = useState(1); + const [totalPage, setTotalPage] = useState(1); + const [totalItem, setTotalItem] = useState(0); + const [pageSize, setPageSize] = useState(10); + const { startTime, endTime } = useOverviewStore(); + const queryBody = { + startTime, + endTime, + page, + pageSize + }; + const { data, isLoading, isSuccess, isPreviousData } = useQuery( + ['billing', 'invoicelist', queryBody], + () => { + return request>('/api/invoice/list', { + data: queryBody, + method: 'POST' + }); + }, + { + keepPreviousData: true + } + ); + useEffect(() => { + if (!data?.data) { + return; + } + const { total, totalPage } = data.data; + if (totalPage === 0) { + // search reset + setTotalPage(1); + setTotalItem(1); + } else { + setTotalItem(total); + setTotalPage(totalPage); + } + if (totalPage < page) { + setPage(1); + } + }, [data?.data]); + + return ( + + + + + {/* */} + + ); +} diff --git a/frontend/providers/costcenter/src/components/invoice/Status.tsx b/frontend/providers/costcenter/src/components/invoice/Status.tsx new file mode 100644 index 000000000000..3e30f8aac56a --- /dev/null +++ b/frontend/providers/costcenter/src/components/invoice/Status.tsx @@ -0,0 +1,35 @@ +import { InvoicePayload } from '@/types'; +import { Flex, Circle, Text, FlexProps } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; + +export function InvoiceStatus({ + status, + ...props +}: { status: InvoicePayload['status'] } & FlexProps) { + const { t } = useTranslation(); + return ( + + + + {t('orders.status.' + status)} + + + ); +} diff --git a/frontend/providers/costcenter/src/components/invoice/invoiceTable.tsx b/frontend/providers/costcenter/src/components/invoice/invoiceTable.tsx deleted file mode 100644 index f8f98f5272d0..000000000000 --- a/frontend/providers/costcenter/src/components/invoice/invoiceTable.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { InvoiceTableHeaders } from '@/constants/billing'; -import { Checkbox, Flex, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react'; -import { format } from 'date-fns'; -import { useTranslation } from 'next-i18next'; -import { RechargeBillingItem, ReqGenInvoice } from '@/types'; -import CurrencySymbol from '../CurrencySymbol'; -import useEnvStore from '@/stores/env'; - -export function InvoiceTable({ - data, - selectbillings, - onSelect -}: { - data: ReqGenInvoice['billings']; - selectbillings: ReqGenInvoice['billings']; - onSelect?: (type: boolean, item: RechargeBillingItem) => void; -}) { - const { t } = useTranslation(); - const needSelect = !!onSelect; - const currency = useEnvStore((s) => s.currency); - return ( - - - - - {InvoiceTableHeaders?.map((item) => ( - - - {t(item)} - {item === 'True Amount' && ( - <> - () - > - )} - - - ))} - - - - {data.map((item) => ( - - - - {needSelect && ( - b.ID).includes(item.ID)} - onChange={(v) => { - onSelect(v.target.checked, item); - }} - mr="13px" - w="12px" - h="12px" - /> - )} - {item.ID} - - - {format(new Date(item.CreatedAt), 'MM-dd HH:mm')} - {item.Amount} - - ))} - - - - ); -} diff --git a/frontend/providers/costcenter/src/components/menu/BaseMenu.tsx b/frontend/providers/costcenter/src/components/menu/BaseMenu.tsx index c5685e0cc85e..bd235d876235 100644 --- a/frontend/providers/costcenter/src/components/menu/BaseMenu.tsx +++ b/frontend/providers/costcenter/src/components/menu/BaseMenu.tsx @@ -115,6 +115,9 @@ export default function BaseMenu({ p={'4px 6px'} borderRadius={'4px'} justifyContent={'flex-start'} + overflowX={'hidden'} + whiteSpace={'nowrap'} + textOverflow={'ellipsis'} onClick={() => onClick(idx)} > {v} diff --git a/frontend/providers/costcenter/src/components/table/AppBillingTable.tsx b/frontend/providers/costcenter/src/components/table/AppBillingTable.tsx index c1d84f811a26..eba4b6a60ad3 100644 --- a/frontend/providers/costcenter/src/components/table/AppBillingTable.tsx +++ b/frontend/providers/costcenter/src/components/table/AppBillingTable.tsx @@ -16,13 +16,7 @@ import CurrencySymbol from '../CurrencySymbol'; import { AppImg, BaseTable } from './BaseTable'; import useAppTypeStore from '@/stores/appType'; import BillingDetails from './billingDetails'; -import appIcon from '@/assert/app.svg'; -import jobIcon from '@/assert/job.svg'; -import osIcon from '@/assert/objectstorage.svg'; -import cvmIcon from '@/assert/cvm.svg'; -import terminalIcon from '@/assert/terminal.svg'; -import dbIcon from '@/assert/db.svg'; -import sealosIcon from '@/assert/sealos.svg'; + import { subHours, parseISO, format } from 'date-fns'; import useBillingStore from '@/stores/billing'; const getCustomTh = (data?: { tNs?: string; needCurrency?: boolean }) => diff --git a/frontend/providers/costcenter/src/components/table/BaseTable.tsx b/frontend/providers/costcenter/src/components/table/BaseTable.tsx index d53cfa5fc502..866e64a6fd52 100644 --- a/frontend/providers/costcenter/src/components/table/BaseTable.tsx +++ b/frontend/providers/costcenter/src/components/table/BaseTable.tsx @@ -54,11 +54,9 @@ export function BaseTable({ ...(pinState === 'right' ? { right: '100%' - // boxShadow: 'rgba(5, 5, 5, 0.06) -10px 0px 8px -8px inset' } : { left: '100%' - // boxShadow: 'rgba(5, 5, 5, 0.06) 10px 0px 8px -8px inset' }) }, bgColor: 'white', diff --git a/frontend/providers/costcenter/src/components/table/InoviceBaseTable.tsx b/frontend/providers/costcenter/src/components/table/InoviceBaseTable.tsx new file mode 100644 index 000000000000..e79bf416e640 --- /dev/null +++ b/frontend/providers/costcenter/src/components/table/InoviceBaseTable.tsx @@ -0,0 +1,176 @@ +import { + TableContainerProps, + TableContainer, + Table, + Thead, + Tr, + Th, + Tbody, + Td, + Img, + ImgProps, + TableRowProps +} from '@chakra-ui/react'; +import { flexRender, Table as ReactTable } from '@tanstack/react-table'; +import appIcon from '@/assert/app.svg'; +import jobIcon from '@/assert/job.svg'; +import osIcon from '@/assert/objectstorage.svg'; +import cvmIcon from '@/assert/cvm.svg'; +import terminalIcon from '@/assert/terminal.svg'; +import dbIcon from '@/assert/db.svg'; +import sealosIcon from '@/assert/sealos.svg'; +import { useTranslation } from 'next-i18next'; +import useAppTypeStore from '@/stores/appType'; + +export function BaseTable({ + table, + ...styles +}: { table: ReactTable } & TableContainerProps) { + return ( + + + + {table.getHeaderGroups().map((headers) => { + return ( + + {headers.headers.map((header, i) => { + const pinState = header.column.getIsPinned(); + return ( + + {flexRender(header.column.columnDef.header, header.getContext())} + + ); + })} + + ); + })} + + + {table.getRowModel().rows.map((item) => { + return ( + + {item.getAllCells().map((cell, i) => { + const pinState = cell.column.getIsPinned(); + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ); + })} + + ); + })} + {} + + + + ); +} +export function AppImg({ app_type, ...props }: { app_type: string } & ImgProps) { + const { getAppType } = useAppTypeStore(); + let uri = ''; + if (getAppType(app_type) === 'DB' || app_type === 'DB') { + uri = dbIcon.src; + } else if (getAppType(app_type) === 'APP' || app_type === 'APP') { + uri = appIcon.src; + } else if (getAppType(app_type) === 'TERMINAL' || app_type === 'TERMINAL') { + uri = terminalIcon.src; + } else if (getAppType(app_type) === 'JOB' || app_type === 'JOB') { + uri = jobIcon.src; + } else if (getAppType(app_type) === 'OBJECT-STORAGE' || app_type === 'OBJECT-STORAGE') { + uri = osIcon.src; + } else if (getAppType(app_type) === 'CLOUD-VM' || app_type === 'CLOUD-VM') { + uri = cvmIcon.src; + } else { + uri = sealosIcon.src; + } + return ; +} diff --git a/frontend/providers/costcenter/src/components/table/InovicePaymentTable.tsx b/frontend/providers/costcenter/src/components/table/InovicePaymentTable.tsx new file mode 100644 index 000000000000..2f7d97ef8f93 --- /dev/null +++ b/frontend/providers/costcenter/src/components/table/InovicePaymentTable.tsx @@ -0,0 +1,142 @@ +import { InvoiceTableHeaders, TableHeaderID } from '@/constants/billing'; +import useEnvStore from '@/stores/env'; +import { ReqGenInvoice, RechargeBillingItem, BillingType, BillingItem } from '@/types'; +import { + TableContainer, + Table, + Thead, + Tr, + Th, + Flex, + Tbody, + Td, + Checkbox, + Text, + useStatStyles +} from '@chakra-ui/react'; +import { format, parseISO, subHours } from 'date-fns'; +import { useTranslation } from 'next-i18next'; +import CurrencySymbol from '../CurrencySymbol'; +import { BaseTable } from './InoviceBaseTable'; +import { + createColumnHelper, + HeaderContext, + CellContext, + useReactTable, + getCoreRowModel, + RowSelectionState +} from '@tanstack/react-table'; +import { useEffect, useMemo, useState } from 'react'; +import { formatMoney } from '@/utils/format'; + +export function InvoicePaymentTable({ + data, + selectbillings, + setSelectBillings +}: { + data: ReqGenInvoice['billings']; + selectbillings: ReqGenInvoice['billings']; + setSelectBillings?: (items: RechargeBillingItem[]) => void; +}) { + const { t } = useTranslation(); + const needSelect = !!setSelectBillings; + const currency = useEnvStore((s) => s.currency); + const [rowSelection, setRowSelection] = useState( + Object.fromEntries(selectbillings.map((v) => [v.ID, true])) + ); + + useEffect(() => { + const billings = Object.keys(rowSelection).flatMap((id) => { + const rows = table.getRowModel().rowsById[id]; + if (!rows) return []; + if (!rows.getIsSelected()) return []; + return [rows.original]; + }); + console.log(billings); + setSelectBillings?.(billings); + }, [rowSelection]); + + const columns = useMemo(() => { + const columnHelper = createColumnHelper(); + const customTh = (needCurrency?: boolean) => + function CustomTh({ header }: HeaderContext) { + return ( + + {t(header.id)} + {!!needCurrency && ( + + + + )} + + ); + }; + return [ + columnHelper.accessor((row) => row.ID, { + header: customTh(), + id: TableHeaderID.OrderNumber, + cell({ row }) { + const item = row.original; + + return ( + + {needSelect && ( + e.preventDefault()} + /> + )} + {item.ID} + + ); + }, + enablePinning: true + }), + columnHelper.accessor((row) => row.CreatedAt, { + id: TableHeaderID.TransactionTime, + header: customTh(), + cell(props) { + const time = props.cell.getValue(); + return format(new Date(time), 'MM-dd HH:mm'); + } + }), + columnHelper.accessor((row) => row.Amount, { + id: TableHeaderID.TotalAmount, + header: customTh(true), + cell(props) { + const amount = props.cell.getValue(); + return ( + + {formatMoney(amount)} + + ); + }, + enablePinning: true + }) + ]; + }, [t, currency, needSelect, selectbillings]); + + const table = useReactTable({ + data, + getRowId: (row) => row.ID, + onRowSelectionChange: setRowSelection, //hoist up the row selection state to your own scope + state: { + columnPinning: { + left: [TableHeaderID.APPName], + right: [TableHeaderID.TotalAmount] + }, + rowSelection + }, + columns, + getCoreRowModel: getCoreRowModel() + }); + return ; +} diff --git a/frontend/providers/costcenter/src/components/table/InoviceTable.tsx b/frontend/providers/costcenter/src/components/table/InoviceTable.tsx new file mode 100644 index 000000000000..0b73730dbfa9 --- /dev/null +++ b/frontend/providers/costcenter/src/components/table/InoviceTable.tsx @@ -0,0 +1,129 @@ +import { InvoiceTableHeaders, TableHeaderID } from '@/constants/billing'; +import useEnvStore from '@/stores/env'; +import { ReqGenInvoice, InvoicePayload, BillingType, BillingItem } from '@/types'; +import { + TableContainer, + Table, + Thead, + Tr, + Th, + Flex, + Tbody, + Td, + Checkbox, + Text, + Button, + Circle, + Badge, + useStatStyles, + useStyleConfig +} from '@chakra-ui/react'; +import { format, parseISO, subHours } from 'date-fns'; +import { useTranslation } from 'next-i18next'; +import CurrencySymbol from '../CurrencySymbol'; +import { BaseTable } from './InoviceBaseTable'; +import { + createColumnHelper, + HeaderContext, + CellContext, + useReactTable, + getCoreRowModel +} from '@tanstack/react-table'; +import { useMemo } from 'react'; +import InvoiceDetails from './InvoiceDetails'; +import { formatMoney } from '@/utils/format'; +import { InvoiceStatus } from '../invoice/Status'; +export function InvoiceTable({ + data, + onSelect, + toInvoiceDetail +}: { + toInvoiceDetail: () => void; + data: InvoicePayload[]; + onSelect?: (type: boolean, item: InvoicePayload) => void; +}) { + const { t } = useTranslation(); + const needSelect = !!onSelect; + const currency = useEnvStore((s) => s.currency); + const columns = useMemo(() => { + const columnHelper = createColumnHelper(); + const customTh = (needCurrency?: boolean) => + function CustomTh({ header }: HeaderContext) { + return ( + + {t(header.id)} + {!!needCurrency && ( + + + + )} + + ); + }; + return [ + columnHelper.accessor((row) => row.updatedAt, { + id: TableHeaderID.InvoiceCreateTime, + header: customTh(), + cell(props) { + const time = props.cell.getValue(); + return format(new Date(time), 'MM-dd HH:mm'); + } + }), + columnHelper.accessor((row) => row.status, { + id: TableHeaderID.Status, + header: customTh(), + cell(props) { + const status = props.cell.getValue(); + return ; + } + }), + columnHelper.accessor((row) => row.updatedAt, { + id: TableHeaderID.InvoiceUpdateTime, + header: customTh(), + cell(props) { + const time = props.cell.getValue(); + const isFinish = props.row.original.status === 'COMPLETED'; + if (!isFinish) return '-'; + return format(new Date(time), 'MM-dd HH:mm'); + } + }), + columnHelper.accessor((row) => row.totalAmount, { + id: TableHeaderID.TotalAmount, + header: customTh(true), + cell(props) { + const amount = props.cell.getValue(); + return ( + + {formatMoney(amount)} + + ); + } + }), + columnHelper.accessor((row) => row.detail, { + id: TableHeaderID.Handle, + header: customTh(), + cell(props) { + return ( + + ); + }, + enablePinning: true + }) + ]; + }, [t, currency, needSelect, toInvoiceDetail]); + const table = useReactTable({ + data, + state: { + columnPinning: { + left: [TableHeaderID.APPName], + right: [TableHeaderID.Handle] + } + }, + columns, + getCoreRowModel: getCoreRowModel() + }); + return ; +} diff --git a/frontend/providers/costcenter/src/components/table/InvoiceDetails.tsx b/frontend/providers/costcenter/src/components/table/InvoiceDetails.tsx new file mode 100644 index 000000000000..684d4fe8508d --- /dev/null +++ b/frontend/providers/costcenter/src/components/table/InvoiceDetails.tsx @@ -0,0 +1,56 @@ +import { Text, Button, ButtonProps } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import DetailsIcon from '../icons/DetailsIcon'; +import useInvoiceStore from '@/stores/invoce'; +import { InvoicePayload } from '@/types'; + +export default function InvoiceDetails({ + invoice, + toInvoiceDetail, + ...props +}: { + invoice: InvoicePayload; + toInvoiceDetail: () => void; +} & ButtonProps) { + const { setInvoiceDetail, setData } = useInvoiceStore(); + const toInvoiceDetailPage = () => { + setInvoiceDetail(invoice.detail); + setData(invoice); + toInvoiceDetail(); + }; + const { t } = useTranslation(); + return ( + { + e.preventDefault(); + toInvoiceDetailPage(); + }} + _disabled={{ + opacity: '0.5', + pointerEvents: 'none' + }} + {...props} + > + + {t('Details')} + + ); +} diff --git a/frontend/providers/costcenter/src/components/table/billingDetails.tsx b/frontend/providers/costcenter/src/components/table/billingDetails.tsx index 7ba5a80e6c83..6f6f55108448 100644 --- a/frontend/providers/costcenter/src/components/table/billingDetails.tsx +++ b/frontend/providers/costcenter/src/components/table/billingDetails.tsx @@ -151,7 +151,7 @@ export function BillingDetailsModal({ {t('Transaction Time')}: {format(query.startTime, 'yyyy-MM-dd HH:MM')} ~ - {format(query.endTime, ' HH:MM')} + {format(query.endTime, 'yyyy-MM-dd HH:MM')} diff --git a/frontend/providers/costcenter/src/components/table/billingTable.tsx b/frontend/providers/costcenter/src/components/table/billingTable.tsx index b7aca70e1955..11e470e2c8f6 100644 --- a/frontend/providers/costcenter/src/components/table/billingTable.tsx +++ b/frontend/providers/costcenter/src/components/table/billingTable.tsx @@ -19,13 +19,6 @@ import Amount from '@/components/billing/AmountTableHeader'; import { AppImg, BaseTable } from '../table/BaseTable'; import { valuationMap } from '@/constants/payment'; import useAppTypeStore from '@/stores/appType'; -import appIcon from '@/assert/app.svg'; -import jobIcon from '@/assert/job.svg'; -import osIcon from '@/assert/objectstorage.svg'; -import cvmIcon from '@/assert/cvm.svg'; -import terminalIcon from '@/assert/terminal.svg'; -import dbIcon from '@/assert/db.svg'; -import sealosIcon from '@/assert/sealos.svg'; const getAmountCell = (data?: { isTotal?: boolean }) => function AmountCell(props: CellContext) { const isTotal = data?.isTotal || false; @@ -130,12 +123,12 @@ export function BillingDetailsTable({ columnHelper.accessor((row) => row.used[3], { id: TableHeaderID.Network, header: customTh(), - cell: customCell() + cell: getUnit('network') }), columnHelper.accessor((row) => row.used_amount[3], { id: TableHeaderID.NetworkAmount, header: customTh(), - cell: getUnit('network') + cell: customCell() }), columnHelper.accessor((row) => row.used[4], { id: TableHeaderID.Port, diff --git a/frontend/providers/costcenter/src/components/valuation/quota.tsx b/frontend/providers/costcenter/src/components/valuation/quota.tsx index a8a94af2276c..c55b87a5098a 100644 --- a/frontend/providers/costcenter/src/components/valuation/quota.tsx +++ b/frontend/providers/costcenter/src/components/valuation/quota.tsx @@ -29,8 +29,13 @@ export default function Quota(props: StackProps) { const quota = (data?.data?.quota || []) .filter((d) => d.type !== 'gpu') .map((d) => { + const _limit = Number.parseInt(d.limit * 1000 + ''); + const _used = Number.parseInt(d.used * 1000 + ''); return { ...d, + limit: _limit / 1000, + used: _used / 1000, + remain: (_limit - _used) / 1000, title: t(d.type), unit: valuationMap.get(d.type)?.unit, bg: valuationMap.get(d.type)?.bg @@ -58,7 +63,6 @@ export default function Quota(props: StackProps) { - {' '} {t('Used')}: {item.used} {item.unit} @@ -70,8 +74,7 @@ export default function Quota(props: StackProps) { borderWidth={'1px'} /> - {' '} - {t('Remain')}: {item.limit - item.used} + {t('Remain')}: {item.remain} {item.unit} - {' '} {t('Total')}: {item.limit} {item.unit} diff --git a/frontend/providers/costcenter/src/constants/billing.ts b/frontend/providers/costcenter/src/constants/billing.ts index b75b9151d568..84b81276f8cd 100644 --- a/frontend/providers/costcenter/src/constants/billing.ts +++ b/frontend/providers/costcenter/src/constants/billing.ts @@ -46,7 +46,10 @@ export enum TableHeaderID { 'Region' = 'Region', 'Namespace' = 'workspace', 'TransferType' = 'Transfer Type', - 'TraderID' = 'Trader ID' + 'TraderID' = 'Trader ID', + 'Status' = 'Invoice Status', + 'InvoiceCreateTime' = 'Invoice Create Time', + 'InvoiceUpdateTime' = 'Invoice Update Time' } export const resourceType = ['cpu', 'memory', 'storage', 'network', 'nodeports'] as const; diff --git a/frontend/providers/costcenter/src/constants/payment.ts b/frontend/providers/costcenter/src/constants/payment.ts index a2568acc78cd..dcee58efec84 100644 --- a/frontend/providers/costcenter/src/constants/payment.ts +++ b/frontend/providers/costcenter/src/constants/payment.ts @@ -93,7 +93,7 @@ export const valuationMap = new Map([ ['storage', { unit: 'GB', scale: 1024, bg: '#9A8EE0', idx: 2 }], ['gpu', { unit: 'GPU', scale: 1000, bg: '#6FCA88', idx: 3 }], ['network', { unit: 'M', scale: 1, bg: '#F182AA', idx: 4 }], - ['services.nodeports', { unit: '', scale: 1, bg: '#F182AA', idx: 5 }] + ['services.nodeports', { unit: '', scale: 1000, bg: '#F182AA', idx: 5 }] ]); // export const BillingUnitMap = new Map([ // ['cpu', { unit: ''}] diff --git a/frontend/providers/costcenter/src/pages/api/billing/appOverview.ts b/frontend/providers/costcenter/src/pages/api/billing/appOverview.ts index c0db20b8e480..25ce673ebd12 100644 --- a/frontend/providers/costcenter/src/pages/api/billing/appOverview.ts +++ b/frontend/providers/costcenter/src/pages/api/billing/appOverview.ts @@ -1,8 +1,8 @@ import { authSession } from '@/service/backend/auth'; import { jsonRes } from '@/service/backend/response'; import type { NextApiRequest, NextApiResponse } from 'next'; -import { formatISO, subMonths } from 'date-fns'; -import { getRegionByUid, getRegionList, makeAPIURL } from '@/service/backend/region'; +import { formatISO } from 'date-fns'; +import { getRegionByUid, makeAPIURL } from '@/service/backend/region'; export default async function handler(req: NextApiRequest, resp: NextApiResponse) { try { const kc = await authSession(req.headers); @@ -10,10 +10,6 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse if (user === null) { return jsonRes(resp, { code: 403, message: 'user null' }); } - // return jsonRes(resp, { - // code: 200, - // data: {"overviews":[{"amount":60492,"namespace":"ns-5uxfy8jl","regionDomain":"","appType":2,"appName":"hello-world"}],"total":1,"totalPage":1} - // }); const { endTime = formatISO(new Date(), { representation: 'complete' @@ -74,10 +70,12 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse method: 'POST', body }); + const result = await response.json(); if (!response.ok) { + console.log(result); throw Error('get cost overview error'); } - const result = await response.json(); + return jsonRes(resp, { code: 200, data: result.data diff --git a/frontend/providers/costcenter/src/pages/api/billing/consumption.ts b/frontend/providers/costcenter/src/pages/api/billing/consumption.ts index 9b4f1adeabcc..ea0045209d50 100644 --- a/frontend/providers/costcenter/src/pages/api/billing/consumption.ts +++ b/frontend/providers/costcenter/src/pages/api/billing/consumption.ts @@ -52,7 +52,7 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse const data = await results.json(); if (!results.ok) { console.log(data); - throw Error(); + throw Error('get consumption error'); } return jsonRes(resp, { code: 200, diff --git a/frontend/providers/costcenter/src/pages/api/billing/rechargeBillingList.ts b/frontend/providers/costcenter/src/pages/api/billing/rechargeBillingList.ts index 9225553f0b9c..4268f189d856 100644 --- a/frontend/providers/costcenter/src/pages/api/billing/rechargeBillingList.ts +++ b/frontend/providers/costcenter/src/pages/api/billing/rechargeBillingList.ts @@ -1,9 +1,7 @@ import { authSession } from '@/service/backend/auth'; import { NextApiRequest, NextApiResponse } from 'next'; import { jsonRes } from '@/service/backend/response'; -import { GetUserDefaultNameSpace } from '@/service/backend/kubernetes'; import { BillingSpec, RechargeBillingData } from '@/types'; -import { parseISO } from 'date-fns'; import { makeAPIURL } from '@/service/backend/region'; export default async function handler(req: NextApiRequest, resp: NextApiResponse) { @@ -14,9 +12,18 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse if (user === null) { return jsonRes(resp, { code: 403, message: 'user null' }); } - const { endTime, startTime } = req.body as { + const { + endTime, + startTime, + paymentID, + page = 1, + pageSize = 100 + } = req.body as { endTime?: Date; startTime?: Date; + paymentID?: string; + page?: number; + pageSize?: number; }; if (!endTime) return jsonRes(resp, { @@ -32,7 +39,10 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse endTime, kubeConfig: kc.exportConfig(), owner: user.name, - startTime + paymentID, + startTime, + page, + pageSize }; const url = makeAPIURL(null, '/account/v1alpha1/payment'); const response = await fetch(url, { @@ -46,9 +56,9 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse payment: [] } }); - const res = (await response.clone().json()) as RechargeBillingData; + const res = (await response.clone().json()) as { data: RechargeBillingData }; return jsonRes(resp, { - data: res + data: res.data }); } catch (error) { console.log(error); diff --git a/frontend/providers/costcenter/src/pages/api/billing/rechargeList.ts b/frontend/providers/costcenter/src/pages/api/billing/rechargeList.ts index f4d0ded8bdd7..6bf17c209250 100644 --- a/frontend/providers/costcenter/src/pages/api/billing/rechargeList.ts +++ b/frontend/providers/costcenter/src/pages/api/billing/rechargeList.ts @@ -46,9 +46,12 @@ export default async function handler(req: NextApiRequest, resp: NextApiResponse payment: [] } }); - const res = (await response.clone().json()) as RechargeBillingData; + const res = (await response.clone().json()) as { data: RechargeBillingData }; return jsonRes(resp, { - data: res.payment.map((payment) => [parseISO(payment.CreatedAt).getTime(), payment.Amount]) + data: res.data.payments.map((payment) => [ + parseISO(payment.CreatedAt).getTime(), + payment.Amount + ]) }); } catch (error) { console.log(error); diff --git a/frontend/providers/costcenter/src/pages/api/bot/callback.ts b/frontend/providers/costcenter/src/pages/api/bot/callback.ts new file mode 100644 index 000000000000..0f6ae974d0f4 --- /dev/null +++ b/frontend/providers/costcenter/src/pages/api/bot/callback.ts @@ -0,0 +1,117 @@ +import Invoice from '@/pages/create_invoice'; +import { authSession } from '@/service/backend/auth'; +import { getRegionByUid, makeAPIURL } from '@/service/backend/region'; +import { jsonRes } from '@/service/backend/response'; +import { + getInvoicePayments, + updateTenantAccessToken, + callbackToUpdateBot +} from '@/service/sendToBot'; +import { InvoiceListData, InvoicePayload } from '@/types'; +import { getToken } from '@chakra-ui/system'; +import type { NextApiRequest, NextApiResponse } from 'next'; +export default async function handler(req: NextApiRequest, resp: NextApiResponse) { + try { + await updateTenantAccessToken(); + const body = req.body as { + type?: 'url_verification'; + }; + if (body.type === 'url_verification') { + const { token, challenge } = body as { + challenge?: string; + token?: string; + }; + if (!token || !challenge) { + throw Error(''); + } + + // !todo + if (token === global.AppConfig.costCenter.invoice.feishApp.token) { + return resp.json({ challenge }); + } + } else { + const { event, header, schema } = body as { + schema: '2.0'; + header: { + event_id: string; + token: string; + create_time: string; + event_type: 'card.action.trigger'; + tenant_key: string; + app_id: string; + }; + event?: { + token: string; + action: { + value: { + status: '1' | '0'; + id: string; // invoiceId + }; + }; + }; + }; + // !todo verify + if ( + !event || + schema !== '2.0' || + !header || + header.token !== global.AppConfig.costCenter.invoice.feishApp.token || + header.app_id !== global.AppConfig.costCenter.invoice.feishApp.appId + ) { + throw Error('feishu request error'); + } + const value = event.action.value; + + // const url = makeAPIURL(null, '/account/v1alpha1/invoice/set-status') + let status: InvoicePayload['status'] = 'PENDING'; + if (value.status === '1') { + status = 'COMPLETED'; + } else if (value.status === '0') { + status = 'PENDING'; + } else { + throw Error(''); + } + const invoiceId = value.id; + if (!invoiceId) { + throw Error(`invoiceId is null`); + } + if (!(await updateTenantAccessToken())) throw Error('updateTenantAccessToken error'); + const url = makeAPIURL(null, '/account/v1alpha1/invoice/set-status'); + const setStatusRes = await fetch(url, { + method: 'post', + body: JSON.stringify({ + invoiceIDList: [invoiceId], + status, + token: AppConfig.costCenter.invoice.serviceToken + }) + }); + if (!setStatusRes.ok) { + console.log(await setStatusRes.json()); + throw Error('set invoice status error'); + } + + const getUrl = makeAPIURL(null, '/account/v1alpha1/invoice/get'); + const getInvoiceRes = await fetch(getUrl, { + method: 'post', + body: JSON.stringify({ + token: AppConfig.costCenter.invoice.serviceToken, + invoiceID: invoiceId, + page: 1, + pageSize: 10, + startTime: '2023-01-01T00:00:00Z', + endTime: new Date() + }) + }); + const invoiceListData = (await getInvoiceRes.json()) as { + data: InvoiceListData; + }; + const payments = await getInvoicePayments(invoiceId); + // @ts-ignore + + return callbackToUpdateBot(resp, { invoice: invoiceListData.data.invoices[0], payments }); + } + } catch (error) { + console.log(error); + return resp.json('error'); + } +} diff --git a/frontend/providers/costcenter/src/pages/api/invoice/list.ts b/frontend/providers/costcenter/src/pages/api/invoice/list.ts new file mode 100644 index 000000000000..2146ecf47112 --- /dev/null +++ b/frontend/providers/costcenter/src/pages/api/invoice/list.ts @@ -0,0 +1,61 @@ +import { authSession } from '@/service/backend/auth'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/service/backend/response'; +import { GetUserDefaultNameSpace } from '@/service/backend/kubernetes'; +import { BillingSpec, InvoiceListData, RechargeBillingData } from '@/types'; +import { parseISO } from 'date-fns'; +import { makeAPIURL } from '@/service/backend/region'; + +export default async function handler(req: NextApiRequest, resp: NextApiResponse) { + try { + const kc = await authSession(req.headers); + // get user account payment amount + const user = kc.getCurrentUser(); + if (user === null) { + return jsonRes(resp, { code: 403, message: 'user null' }); + } + const { + endTime, + startTime, + pageSize = 10, + page = 1 + } = req.body as { + endTime?: Date; + startTime?: Date; + pageSize?: number; + page?: number; + }; + if (!endTime) + return jsonRes(resp, { + code: 400, + message: 'endTime is invalid' + }); + if (!startTime) + return jsonRes(resp, { + code: 400, + message: 'endTime is invalid' + }); + const data = { + endTime, + kubeConfig: kc.exportConfig(), + startTime, + page, + pageSize + }; + const url = makeAPIURL(null, '/account/v1alpha1/invoice/get'); + const response = await fetch(url, { + method: 'POST', + body: JSON.stringify(data) + }); + const res = await response.json(); + if (!response.ok) { + throw Error(res); + } + return jsonRes(resp, { + data: res.data + }); + } catch (error) { + console.log(error); + jsonRes(resp, { code: 500, message: 'get invoice error' }); + } +} diff --git a/frontend/providers/costcenter/src/pages/api/invoice/sms.ts b/frontend/providers/costcenter/src/pages/api/invoice/sms.ts index c44940951d9c..b8d069e58646 100644 --- a/frontend/providers/costcenter/src/pages/api/invoice/sms.ts +++ b/frontend/providers/costcenter/src/pages/api/invoice/sms.ts @@ -8,26 +8,6 @@ import { getClientIPFromRequest, retrySerially } from '@/utils/tools'; import { authSession } from '@/service/backend/auth'; import * as process from 'process'; const requestTimestamps: Record = {}; - -function checkRequestFrequency(ipAddress: string) { - const accessKeyId = global.AppConfig.costCenter.invoice.aliSms.accessKeyID; - const accessKeySecret = global.AppConfig.costCenter.invoice.aliSms.accessKeySecret; - const templateCode = global.AppConfig.costCenter.invoice.aliSms.templateCode; - const signName = global.AppConfig.costCenter.invoice.aliSms.signName; - const currentTime = Date.now(); - const lastRequestTime = requestTimestamps[ipAddress] || 0; - const timeDiff = currentTime - lastRequestTime; - - const requestInterval = 60 * 1000; - - if (timeDiff < requestInterval) { - return false; - } else { - requestTimestamps[ipAddress] = currentTime; - return true; - } -} - export default async function handler(req: NextApiRequest, res: NextApiResponse) { const accessKeyId = global.AppConfig.costCenter.invoice.aliSms.accessKeyID; const accessKeySecret = global.AppConfig.costCenter.invoice.aliSms.accessKeySecret; @@ -42,16 +22,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (user === null) { return jsonRes(res, { code: 401, message: 'user null' }); } + if (process.env.NODE_ENV === 'development') { + return jsonRes(res, { + message: 'successfully', + code: 200 + }); + } const { phoneNumbers } = req.body; let ip = getClientIPFromRequest(req); if (!ip) { - if (process.env.NODE_ENV === 'development') ip = '127.0.0.1'; - else - return jsonRes(res, { - message: 'The IP is null', - code: 403 - }); + // if (process.env.NODE_ENV === 'development') { + // // ip = '127.0.0.1'; + // } + // else + return jsonRes(res, { + message: 'The IP is null', + code: 403 + }); } if ( !(await checkSendable({ diff --git a/frontend/providers/costcenter/src/pages/api/invoice/verify.ts b/frontend/providers/costcenter/src/pages/api/invoice/verify.ts index dcc089095e4d..8f9376fd198a 100644 --- a/frontend/providers/costcenter/src/pages/api/invoice/verify.ts +++ b/frontend/providers/costcenter/src/pages/api/invoice/verify.ts @@ -1,10 +1,15 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { jsonRes } from '@/service/backend/response'; import { checkCode } from '@/service/backend/db/verifyCode'; -import { addInvoice } from '@/service/backend/db/invoice'; -import { ReqGenInvoice } from '@/types'; +import { InvoicePayload, RechargeBillingItem, ReqGenInvoice } from '@/types'; import { authSession } from '@/service/backend/auth'; -import { sendToBot } from '@/service/sendToBot'; +import { + getInvoicePayments, + sendToBot, + sendToUpdateBot, + sendToWithdrawBot, + updateTenantAccessToken +} from '@/service/sendToBot'; import { isValidBANKAccount, isValidCNTaxNumber, @@ -12,6 +17,8 @@ import { isValidPhoneNumber, retrySerially } from '@/utils/tools'; +import { makeAPIURL } from '@/service/backend/region'; +import { AxiosError } from 'axios'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { @@ -25,7 +32,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (user === null) { return jsonRes(res, { code: 401, message: 'user null' }); } - + await updateTenantAccessToken(); const { detail, contract, billings } = req.body as ReqGenInvoice; if ( !detail || @@ -44,18 +51,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) code: 400 }); } - const url = - global.AppConfig.costCenter.components.accountService.url + - '/account/v1alpha1/payment/set-invoice'; - const setInvoiceRes = await fetch(url, { - method: 'POST', - body: JSON.stringify({ - kubeConfig: kc.exportConfig(), - owner: user.name, - paymentIDList: billings.map((b) => b.ID) - }) - }); - if (!setInvoiceRes.ok) throw Error('setInvocice error'); if ( process.env.NODE_ENV !== 'development' && !(await checkCode({ phone: contract.phone, code: contract.code })) @@ -77,39 +72,64 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) code: 400 }); } - const document = { - k8s_user: user.name, + const invoiceDetail: { + detail: ReqGenInvoice['detail']; + contract: Omit; + } = { detail, - contract, - billings: billings.map((item) => ({ - order_id: item.ID, - amount: item.Amount, - regionUID: item.RegionUID, - userUID: item.UserUID, - createdTime: new Date(item.CreatedAt) - })) + contract: { + person: contract.person, + phone: contract.phone, + email: contract.email + } }; - - const result = await addInvoice(document); - if (!result.acknowledged) { - return jsonRes(res, { - data: { - status: false - }, - message: 'update data error', - code: 500 - }); + const bodyRaw = { + kubeConfig: kc.exportConfig(), + paymentIDList: billings.map((item) => item.ID), + detail: JSON.stringify(invoiceDetail) + }; + const message_id = await sendToBot({ + invoiceDetail, + payments: billings + }); + if (!message_id) { + console.log('sendMessage Error'); + throw Error(''); } - await retrySerially(async () => { - try { - const result = await sendToBot(document); - if (result.StatusCode !== 0) { - throw new Error(result.msg); - } - } catch (error) { - console.error(error); + const url = makeAPIURL(null, '/account/v1alpha1/invoice/apply'); + const result = await fetch(url, { + method: 'post', + body: JSON.stringify(bodyRaw) + }); + const invoiceRes = (await result.json()) as { + data: { + invoice: InvoicePayload; + payments: RechargeBillingItem[]; + }; + }; + if (!result.ok) { + console.log(invoiceRes); + await sendToWithdrawBot({ message_id }); + if (result.status === 403) + return jsonRes(res, { + message: 'You have no permission to apply for the invoice.', + data: { + status: false + }, + code: 403 + }); + else if (result.status === 500) { + throw new Error('Failed to apply for the invoice.'); } - }, 3); + } + const invoice = invoiceRes.data.invoice; + const payments = await getInvoicePayments(invoice.id); + const botResult = await sendToUpdateBot({ + invoice, + payments, + message_id + }); + if (!botResult) throw Error('botResult is null'); return jsonRes(res, { data: { @@ -120,7 +140,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); } catch (error) { console.log(error); - jsonRes(res, { + return jsonRes(res, { data: { status: false }, diff --git a/frontend/providers/costcenter/src/pages/api/platform/getAppConfig.ts b/frontend/providers/costcenter/src/pages/api/platform/getAppConfig.ts index a301321b32c4..e08e1416f4c6 100644 --- a/frontend/providers/costcenter/src/pages/api/platform/getAppConfig.ts +++ b/frontend/providers/costcenter/src/pages/api/platform/getAppConfig.ts @@ -32,16 +32,17 @@ function getAppConfig(defaultConfig: AppConfigType, initConfig: AppConfigType): } return mergeConfig(defaultConfig, initConfig); } - +export function initAppConfig() { + if (!global.AppConfig || process.env.NODE_ENV !== 'production') { + const filename = + process.env.NODE_ENV === 'development' ? 'data/config.yaml.local' : '/app/data/config.yaml'; + const yamlResult: any = yaml.load(readFileSync(filename, 'utf-8')); + global.AppConfig = getAppConfig(DefaultAppConfig, yamlResult); + } +} export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { - if (!global.AppConfig || process.env.NODE_ENV !== 'production') { - const filename = - process.env.NODE_ENV === 'development' ? 'data/config.yaml.local' : '/app/data/config.yaml'; - const yamlResult: any = yaml.load(readFileSync(filename, 'utf-8')); - global.AppConfig = getAppConfig(DefaultAppConfig, yamlResult); - console.log(global.AppConfig); - } + initAppConfig(); jsonRes(res, { data: { RECHARGE_ENABLED: global.AppConfig.costCenter.recharge.enabled, diff --git a/frontend/providers/costcenter/src/pages/billing/index.tsx b/frontend/providers/costcenter/src/pages/billing/index.tsx index e12863d2050a..95c0f8df461e 100644 --- a/frontend/providers/costcenter/src/pages/billing/index.tsx +++ b/frontend/providers/costcenter/src/pages/billing/index.tsx @@ -15,44 +15,11 @@ function Billing() { return ( - - - - {t('Expenditure')} - - - {t('Charge')} - - - {t('Transfer')} - + + + {t('Expenditure')} + {t('Charge')} + {t('Transfer')} { diff --git a/frontend/providers/costcenter/src/pages/create_invoice/InvoicdForm.tsx b/frontend/providers/costcenter/src/pages/create_invoice/InvoicdForm.tsx index 8a61061ab8bf..9cb619bd5fb6 100644 --- a/frontend/providers/costcenter/src/pages/create_invoice/InvoicdForm.tsx +++ b/frontend/providers/costcenter/src/pages/create_invoice/InvoicdForm.tsx @@ -5,6 +5,7 @@ import { isValidEmail, isValidPhoneNumber } from '@/utils/tools'; +import trigIcon from '@/assert/triangle.svg'; import { Flex, Img, @@ -17,131 +18,54 @@ import { Text, Box, InputGroup, - InputRightAddon, Link, useToast, - useDisclosure, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalHeader, - ModalOverlay + InputRightElement, + Badge, + Image } from '@chakra-ui/react'; -import { Formik, FieldArray, Form, Field } from 'formik'; +import { + Formik, + FieldArray, + Form, + Field, + FieldInputProps, + ErrorMessage, + FormikErrors +} from 'formik'; import request from '@/service/request'; -import { useRef, useState, useEffect } from 'react'; +import { useState, useEffect, ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import artical_icon from '@/assert/article.svg'; -import arrow_right from '@/assert/material-symbols_expand-more-rounded.svg'; -import arrow_left_icon from '@/assert/toleft.svg'; -import arrow_icon from '@/assert/Vector.svg'; +import arrow_icon from '@/assert/left2.svg'; import email_icon from '@/assert/mdi_email-receive-outline.svg'; -import listIcon from '@/assert/list.svg'; -import { InvoiceTable } from '@/components/invoice/invoiceTable'; +import { MySelect, MyTooltip as STooltip } from '@sealos/ui'; +import { formatMoney } from '@/utils/format'; +import uil_info_circle from '@/assert/uil_info-circle.svg'; +import { TriangleUpIcon } from '@chakra-ui/icons'; -const BillingModal = ({ - billings, - t, - invoiceAmount, - invoiceCount -}: { - billings: ReturnType>; - t: (key: string) => string; - invoiceAmount: number; - invoiceCount: number; -}) => { - const [pageSize, setPageSize] = useState(10); - const totalPage = Math.floor((billings?.current?.length || 0) / pageSize) + 1; - const [currentPage, setcurrentPage] = useState(1); +function MyTooltip({ errorMessage, children }: { errorMessage: string; children: ReactNode }) { return ( - <> - - - {t('orders.invoiceOrder')}({invoiceCount}) - ¥ {invoiceAmount} - - - - - - - - {t('orders.list')} - - - - index <= pageSize * currentPage - 1 && index >= pageSize * (currentPage - 1) - )} - > - - - {t('Total')}: {billings?.current?.length || 0} - - { - e.preventDefault(); - setcurrentPage(1); - }} - > - - - { - e.preventDefault(); - setcurrentPage(currentPage - 1); - }} - > - - - - {currentPage}/{totalPage} - - { - e.preventDefault(); - setcurrentPage(currentPage + 1); - }} - > - - - { - e.preventDefault(); - setcurrentPage(totalPage); - }} - > - - - - {pageSize} /{t('Page')} - - - - > + + + + {errorMessage} + + + } + isDisabled={!errorMessage} + isOpen={!!errorMessage} + > + {children} + ); -}; - +} function InvoicdForm({ invoiceAmount, invoiceCount, @@ -153,12 +77,18 @@ function InvoicdForm({ onSuccess: () => void; invoiceAmount: number; invoiceCount: number; - billings: ReturnType>; + billings: ReqGenInvoice['billings']; }) { const totast = useToast(); const { t, i18n } = useTranslation(); const initVal = { details: [ + { + name: t('orders.details.type.name'), + placeholder: t('orders.details.type.placeholder'), + isRequired: true, + value: 'normal' + }, { name: t('orders.details.invoiceTitle.name'), placeholder: t('orders.details.invoiceTitle.placeholder'), @@ -237,10 +167,6 @@ function InvoicdForm({ }, 1000); return () => clearInterval(interval); }, [remainTime]); - const totalTips = () => { - if (i18n.language === 'zh') return `(总计${invoiceCount}张发票)`; - else return `(Total ${invoiceCount} invoices)`; - }; const validPhone = (phone: string) => { if (!isValidPhoneNumber(phone)) { totast({ @@ -252,6 +178,16 @@ function InvoicdForm({ return false; } else return true; }; + const typeList = [ + { + label: t('orders.details.type.list.normal'), + value: 'normal' + }, + { + label: t('orders.details.type.list.special'), + value: 'special' + } + ]; const getCode = async (phone: string) => { if (!validPhone(phone)) return; setRemainTime(60); @@ -282,59 +218,25 @@ function InvoicdForm({ values, actions ) => { - if (!billings.current || billings.current.length === 0) { + if (!billings || billings.length === 0) { actions.setSubmitting(false); return; } - if (!isValidPhoneNumber(values.contract[1].value)) { - totast({ - title: t('orders.phoneValidation'), - status: 'error', - position: 'top', - duration: 2000 - }); - return; - } - if (!isValidCNTaxNumber(values.details[1].value)) { - totast({ - title: t('orders.taxNumberValidation'), - status: 'error', - position: 'top', - duration: 2000 - }); - return; - } - if (!isValidEmail(values.contract[2].value)) { - totast({ - title: t('orders.emailValidation'), - status: 'error', - position: 'top', - duration: 2000 - }); - return; - } - if (!isValidBANKAccount(values.details[3].value)) { - totast({ - title: t('orders.bankAccountValidation'), - status: 'error', - position: 'top', - duration: 2000 - }); - return; - } try { const result = await request.post( '/api/invoice/verify', { - billings: billings.current!, + token: '', + billings: billings!, detail: { - title: values.details[0].value, - tax: values.details[1].value, - bank: values.details[2].value, - bankAccount: values.details[3].value, - address: values.details[4].value, - phone: values.details[5].value, - fax: values.details[6].value + title: values.details[1].value, + tax: values.details[2].value, + bank: values.details[3].value, + bankAccount: values.details[4].value, + address: values.details[5].value, + phone: values.details[6].value, + fax: values.details[7].value, + type: values.details[0].value }, contract: { person: values.contract[0].value, @@ -352,6 +254,7 @@ function InvoicdForm({ duration: 2000 }); onSuccess(); + backcb(); } catch (err) { totast({ title: (err as { message: string }).message || t('orders.submit fail'), @@ -363,20 +266,65 @@ function InvoicdForm({ actions.setSubmitting(false); } }; - const { isOpen, onOpen, onClose } = useDisclosure(); return ( <> - - {(props) => ( + { + const errors = { + contract: [], + details: [] + }; + for (let index = 0; index < values.details.length; index++) { + const element = values.details[index]; + if (element.isRequired && !element.value) { + errors.details[index] = t('orders.require'); + return errors; + } + if (index === 2 && !isValidCNTaxNumber(element.value)) { + errors.details[2] = t('orders.taxNumberValidation'); + return errors; + } + if (index === 4 && !isValidBANKAccount(element.value)) { + errors.details[4] = t('orders.bankAccountValidation'); + return errors; + } + } + for (let index = 0; index < values.contract.length; index++) { + const element = values.contract[index]; + if (element.isRequired && !element.value) { + errors.contract[index] = t('orders.require'); + return errors; + } + if (index === 1 && !isValidPhoneNumber(element.value)) { + errors.contract[index] = t('orders.phoneValidation'); + return errors; + } + if (index === 2 && !isValidEmail(element.value)) { + errors.contract[index] = t('orders.emailValidation'); + return errors; + } + } + }} + > + {({ errors, isSubmitting, values, setFieldValue }) => ( ( + render={() => ( - + {t('SideBar.CreateInvoice')} + + + + {t('orders.Apply Inovice Tips')} + + - {t('orders.invoice')} + {t('orders.Apply Invoice')} - - - {t('orders.Invoice Details')} - {t('orders.invoiceAmount')} - ¥ {invoiceAmount} - { - e.preventDefault(); - onOpen(); - }} - > - {totalTips()} - - + + + + {t('orders.Invoice Details')} + + + {t('orders.invoiceAmount')}: + + + ¥ {formatMoney(invoiceAmount)} + - - - {t('orders.Invoice Content')} - - {t('orders.Electronic Computer Service Fee')} - - - - {props.values.details.map((item, index) => ( - - { - // @ts-ignore - ({ field }) => ( - - {item.name} - - - ) - } - - ))} + + + + + {t('orders.Invoice Content')} + + + {t('orders.Electronic Computer Service Fee')} + + + + {values.details.map((item, index) => { + const name = `details.${index}.value`; + const errorMessage = (errors.details?.[index] || '') as string; + return ( + + {({ field }: { field: FieldInputProps }) => ( + + + + {item.name} + + {index === 0 ? ( + + { + setFieldValue(name, val); + }} + w={'280px'} + h={'32px'} + fontWeight={400} + /> + + ) : ( + + )} + + + )} + + ); + })} - - - - {t('orders.Contact Information')} + + + + {t('orders.Contact Information')} + - - - {props.values.contract.map((item, index) => ( - - { - // @ts-ignore - ({ field }) => ( - - {item.name} - {index === 1 ? ( - + {values.contract.map((item, index) => { + const errorMessage = (errors.contract?.[index] || '') as string; + return ( + + { + // @ts-ignore + ({ field }) => ( + + - - - {remainTime <= 0 ? ( - { - e.preventDefault(); - getCode(item.value); + {item.name} + {index === 1 ? ( + + + - {t('Get Code')} - - ) : ( - {remainTime} s - )} - - - ) : ( - - )} - - ) - } - - ))} + {remainTime <= 0 ? ( + { + e.preventDefault(); + getCode(item.value); + }} + > + {t('Get Code')} + + ) : ( + {remainTime} s + )} + + + ) : ( + + )} + + + ) + } + + ); + })} @@ -593,15 +595,6 @@ function InvoicdForm({ /> )} - - - - > ); } diff --git a/frontend/providers/costcenter/src/pages/create_invoice/InvoicdFormDetail.tsx b/frontend/providers/costcenter/src/pages/create_invoice/InvoicdFormDetail.tsx new file mode 100644 index 000000000000..25cb18bbb45d --- /dev/null +++ b/frontend/providers/costcenter/src/pages/create_invoice/InvoicdFormDetail.tsx @@ -0,0 +1,298 @@ +import { ApiResp, InvoicePayload, InvoicesCollection, ReqGenInvoice } from '@/types'; +import { + Flex, + Img, + Heading, + Stack, + FormLabel, + Button, + Text, + Box, + useToast, + useDisclosure +} from '@chakra-ui/react'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import artical_icon from '@/assert/article.svg'; +import arrow_icon from '@/assert/left2.svg'; +import email_icon from '@/assert/mdi_email-receive-outline.svg'; +import useInvoiceStore from '@/stores/invoce'; +import { formatMoney } from '@/utils/format'; +import { InvoiceStatus } from '@/components/invoice/Status'; + +function InvoicdForm({ backcb, onSuccess }: { backcb: () => void; onSuccess: () => void }) { + const totast = useToast(); + const { t, i18n } = useTranslation(); + const { invoiceDetail, data, setData } = useInvoiceStore(); + + const props = useMemo(() => { + const convertType = (type: string) => { + switch (type) { + case 'normal': + return t('orders.details.type.list.normal'); + case 'special': + return t('orders.details.type.list.special'); + default: + return t('orders.details.type.list.normal'); + } + }; + const initVal: { + details: { name: string; value: string; key: keyof InvoicesCollection['detail'] }[]; + contract: { name: string; value: string; key: keyof InvoicesCollection['contract'] }[]; + } = { + details: [ + { + name: t('orders.details.type.name'), + key: 'type', + value: convertType('normal') + }, + { + name: t('orders.details.invoiceTitle.name'), + value: '', + key: 'title' + }, + { + name: t('orders.details.taxRegistrationNumber.name'), + value: '', + key: 'tax' + }, + { + name: t('orders.details.bankName.name'), + key: 'bank', + value: '' + }, + { + name: t('orders.details.bankAccount.name'), + value: '', + key: 'bankAccount' + }, + { + name: t('orders.details.address.name'), + value: '', + key: 'address' + }, + { + name: t('orders.details.phone.name'), + value: '', + key: 'phone' + }, + { + name: t('orders.details.fax.name'), + value: '', + key: 'fax' + } + ], + contract: [ + { + name: t('orders.contract.person.name'), + value: '', + key: 'person' + }, + { + name: t('orders.contract.phone.name'), + value: '', + key: 'phone' + }, + { + name: t('orders.contract.email.name'), + value: '', + key: 'email' + } + ] + }; + const invoiceDetail = data?.detail; + if (!invoiceDetail) { + return initVal; + } + try { + const res = JSON.parse(invoiceDetail) as InvoicesCollection; + initVal.details.forEach((d) => { + const value = res.detail[d.key]; + if (value) { + if (d.key === 'type') { + d.value = convertType(value); + } else d.value = value; + } + }); + + initVal.contract.forEach((d) => { + const value = res.contract[d.key]; + if (value) { + d.value = value; + } + }); + return initVal; + } catch (e) { + return initVal; + } + }, [data?.detail, t]); + + if (!data) return null; + return ( + + + { + setData(); + backcb(); + }} + > + + + + {t('SideBar.CreateInvoice')} + + + + + + + + {t('orders.Invoice Details')} + + + {t('orders.invoiceAmount')}: + + + ¥ {formatMoney(data.totalAmount)} + + + + + + + {t('orders.Invoice Content')} + + + {t('orders.Electronic Computer Service Fee')} + + + {props && + props.details.map((item, index) => ( + + + {item.name} + + + {item.value} + + + ))} + + + + + + + + {t('orders.Contact Information')} + + + + + {props && + props.contract.map((item, index) => ( + + + {item.name} + + + {item.value} + + + ))} + + + + + ); +} + +export default InvoicdForm; diff --git a/frontend/providers/costcenter/src/pages/create_invoice/index.tsx b/frontend/providers/costcenter/src/pages/create_invoice/index.tsx index fc90df75b3d8..946b50eaaf85 100644 --- a/frontend/providers/costcenter/src/pages/create_invoice/index.tsx +++ b/frontend/providers/costcenter/src/pages/create_invoice/index.tsx @@ -1,67 +1,48 @@ -import { InvoiceTable } from '@/components/invoice/invoiceTable'; -import { Box, Button, Flex, Heading, Img, Input, Text } from '@chakra-ui/react'; +// import { InvoiceTable } from '@/components/table/InovicePaymentTable' +import { + Box, + Button, + Flex, + Heading, + IconButton, + Img, + Input, + InputGroup, + InputRightAddon, + InputRightElement, + Tab, + TabList, + TabPanels, + Tabs, + Text +} from '@chakra-ui/react'; import { useEffect, useRef, useState } from 'react'; import receipt_icon from '@/assert/invoice-active.svg'; -import arrow_icon from '@/assert/Vector.svg'; -import arrow_left_icon from '@/assert/toleft.svg'; import magnifyingGlass_icon from '@/assert/magnifyingGlass.svg'; import { useQuery, useQueryClient } from '@tanstack/react-query'; -import request from '@/service/request'; import { RechargeBillingData, RechargeBillingItem } from '@/types/billing'; import SelectRange from '@/components/billing/selectDateRange'; -import useOverviewStore from '@/stores/overview'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { useTranslation } from 'next-i18next'; -import NotFound from '@/components/notFound'; -import listIcon from '@/assert/list.svg'; import { ApiResp, ReqGenInvoice } from '@/types'; import InvoicdForm from './InvoicdForm'; import { formatMoney } from '@/utils/format'; +import PaymentPanel from '@/components/invoice/PaymentPanel'; +import RecordPanel from '@/components/invoice/RecordPanel'; +import InvoicdFormDetail from './InvoicdFormDetail'; function Invoice() { const { t, i18n } = useTranslation(); - const startTime = useOverviewStore((state) => state.startTime); - const endTime = useOverviewStore((state) => state.endTime); - const selectBillings = useRef([]); + const [selectBillings, setSelectBillings] = useState([]); const [searchValue, setSearch] = useState(''); const [orderID, setOrderID] = useState(''); - const [totalPage, setTotalPage] = useState(1); - const [currentPage, setcurrentPage] = useState(1); - const [pageSize, setPageSize] = useState(10); const queryClient = useQueryClient(); - const [invoiceAmount, setInvoiceAmount] = useState(0); - const [processState, setProcessState] = useState(0); - const [invoiceCount, setInvoiceCount] = useState(0); - const { data, isLoading, isSuccess } = useQuery( - [ - 'billing', - 'invoice', - { - startTime, - endTime - } - ], - () => { - return request>('/api/billing/rechargeBillingList', { - data: { - startTime, - endTime - }, - method: 'POST' - }); - }, - { - select(data) { - return ((data?.data?.payment || []) as RechargeBillingItem[]) - .filter((d) => !d.InvoicedAt) - .map((d) => ({ - ...d, - Amount: formatMoney(d.Amount) - })); - } - } - ); + const [tabIdx, setTabIdx] = useState(0); + const [processState, setProcessState] = useState(0); + const invoiceAmount = selectBillings.reduce((acc, cur) => acc + cur.Amount, 0); + const invoiceCount = selectBillings.length; + const isLoading = false; return ( {processState === 0 ? ( <> - - - {t('SideBar.CreateInvoice')} - - - - - {t('Transaction Time')} - - + + + + {t('SideBar.CreateInvoice')} - - - - - setSearch(v.target.value)} - > - - + { - e.preventDefault(); - setOrderID(searchValue.trim()); - }} - > - {t('Search')} - - - - {isSuccess ? ( - <> - - - - - {t('orders.list')} - - - - {t('orders.invoiceAmount')}: - ¥ {invoiceAmount} - - { - e.preventDefault(); - setProcessState(1); - }} - isDisabled={invoiceCount === 0} - ml="19px" - color="#FFFFFF" - bg={'#24282C'} - variant={'unstyled'} - _hover={{ - opacity: '0.5' - }} - py="6px" - px="30px" - > - {t('orders.invoice')} {invoiceCount > 0 ? <>({invoiceCount})> : <>>} - - - - { - if (checked) { - setInvoiceAmount(invoiceAmount + item.Amount); - setInvoiceCount(invoiceCount + 1); - selectBillings.current.push({ ...item }); - } else { - setInvoiceAmount(invoiceAmount - item.Amount); - setInvoiceCount(invoiceCount - 1); - const idx = selectBillings.current.findIndex( - (billing) => billing.ID === item.ID - ); - selectBillings.current.splice(idx, 1); - } + placeholder={t('Order Number') as string} + value={searchValue} + onChange={(v) => setSearch(v.target.value)} + /> + + { + e.preventDefault(); + setOrderID(searchValue.trim()); }} - > - - - {t('Total')}: - {data.length} - - { - e.preventDefault(); - setcurrentPage(1); - }} - > - - - { - e.preventDefault(); - setcurrentPage(currentPage - 1); - }} - > - - - - {currentPage}/{totalPage} + variant={'unstyled'} + icon={} + aria-label={'search orderId'} + > + + + + { + setTabIdx(idx); + }} + > + + + {t('orders.list')} + + + {t('orders.invoiceRecord')} + + {tabIdx === 0 && ( + + + {t('orders.invoiceAmount')}: + ¥ {formatMoney(invoiceAmount)} { e.preventDefault(); - setcurrentPage(currentPage + 1); + setProcessState(1); }} - > - - - { - e.preventDefault(); - setcurrentPage(totalPage); + isDisabled={invoiceCount === 0} + ml="19px" + color="#FFFFFF" + bg={'#24282C'} + variant={'unstyled'} + _hover={{ + opacity: '0.5' }} + py="6px" + px="30px" > - + {t('orders.invoice')} {invoiceCount > 0 ? <>({invoiceCount})> : <>>} - {pageSize} - /{t('Page')} - - > - ) : ( - - - - )} + )} + + + + { + setProcessState(2); + }} + /> + + > ) : processState === 1 ? ( { - selectBillings.current = []; - setInvoiceAmount(0); - setInvoiceCount(0); + setSelectBillings([]); queryClient.invalidateQueries({ queryKey: ['billing'], exact: false @@ -285,6 +165,19 @@ function Invoice() { setProcessState(0); }} > + ) : processState === 2 ? ( + { + setSelectBillings([]); + queryClient.invalidateQueries({ + queryKey: ['billing'], + exact: false + }); + }} + backcb={() => { + setProcessState(0); + }} + > ) : ( <>> )} diff --git a/frontend/providers/costcenter/src/service/auth.ts b/frontend/providers/costcenter/src/service/auth.ts new file mode 100644 index 000000000000..12dd9af80501 --- /dev/null +++ b/frontend/providers/costcenter/src/service/auth.ts @@ -0,0 +1,59 @@ +import { IncomingHttpHeaders } from 'http'; +import { sign, verify } from 'jsonwebtoken'; + +const regionUID = () => global.AppConfig?.cloud.regionUID || '123456789'; +const internalJwtSecret = () => global.AppConfig?.costCenter.auth.jwt.internal || '123456789'; +const externalJwtSecret = () => global.AppConfig?.costCenter.auth.jwt.external || '123456789'; + +export type AuthenticationTokenPayload = { + userUid: string; + userId: string; +}; +export type AccessTokenPayload = { + regionUid: string; + userCrUid: string; + userCrName: string; + workspaceUid: string; + workspaceId: string; +} & AuthenticationTokenPayload; + +// export const verifyAccessToken = async (header: IncomingHttpHeaders) => +// verifyToken(header).then( +// (payload) => { +// if (payload?.regionUid === regionUID()) { +// return payload; +// } else { +// return null; +// } +// }, +// (err) => null +// ); +// export const verifyAuthenticationToken = async (header: IncomingHttpHeaders) => { +// try { +// if (!header?.authorization) { +// throw new Error('缺少凭证'); +// } +// const token = decodeURIComponent(header.authorization); +// const payload = await verifyJWT(token, grobalJwtSecret()); +// return payload; +// } catch (err) { +// return null; +// } +// }; +export const verifyJWT = (token: string, secret: string) => + new Promise((resolve) => { + if (!token) return resolve(null); + verify(token, secret, (err, payload) => { + if (err) { + // console.log(err); + resolve(null); + } else if (!payload) { + resolve(null); + } else { + resolve(payload as T); + } + }); + }); + +export const generateAppToken = (props: AccessTokenPayload) => + sign(props, internalJwtSecret(), { expiresIn: '7d' }); diff --git a/frontend/providers/costcenter/src/service/backend/db/verifyCode.ts b/frontend/providers/costcenter/src/service/backend/db/verifyCode.ts index 2d0c54bdf477..0345497131c3 100644 --- a/frontend/providers/costcenter/src/service/backend/db/verifyCode.ts +++ b/frontend/providers/costcenter/src/service/backend/db/verifyCode.ts @@ -11,7 +11,6 @@ type TVerification_Codes = { async function connectToUserCollection() { const client = await connectToDatabase(); const collection = client.db().collection('verification_codes'); - // console.log('connect to verification_codes collection') await collection.createIndex({ createdTime: 1 }, { expireAfterSeconds: 60 * 5 }); return collection; } diff --git a/frontend/providers/costcenter/src/service/sendToBot.ts b/frontend/providers/costcenter/src/service/sendToBot.ts index 9918718865b5..6f93a934d8fe 100644 --- a/frontend/providers/costcenter/src/service/sendToBot.ts +++ b/frontend/providers/costcenter/src/service/sendToBot.ts @@ -1,103 +1,263 @@ -import { Tbilling, TInvoiceContract, TInvoiceDetail } from '@/types'; +import { initAppConfig } from '@/pages/api/platform/getAppConfig'; +import { + InvoicePayload, + InvoicesCollection, + RechargeBillingItem, + ReqGenInvoice, + Tbilling, + TInvoiceContract, + TInvoiceDetail +} from '@/types'; +import { formatMoney } from '@/utils/format'; + import axios from 'axios'; +import { parseISO } from 'date-fns'; +import { NextApiResponse } from 'next'; +import { makeAPIURL } from './backend/region'; +const feishuAxios = axios.create({ + baseURL: 'https://open.feishu.cn/open-apis', + headers: { + 'Content-Type': 'application/json' + } +}); +const getStatus = (invoiceStatus: InvoicePayload['status']) => + invoiceStatus === 'COMPLETED' ? '已完成' : '申请中'; +export const updateTenantAccessToken = async () => { + try { + initAppConfig(); + const feishuConfig = global.AppConfig.costCenter.invoice.feishApp; -export const sendToBot = async ({ - detail, - contract, - k8s_user, - billings + const body = { + app_id: feishuConfig.appId, + app_secret: feishuConfig.appSecret + }; + + const res = await feishuAxios.post('/auth/v3/tenant_access_token/internal', body); + + if (res.data.msg !== 'ok' || !res.data.tenant_access_token) return false; + feishuAxios.defaults.headers.common['Authorization'] = 'Bearer ' + res.data.tenant_access_token; + + return true; + } catch (error) { + console.log(error); + console.log('auth error'); + return false; + } +}; +const generateBotTemplate = ({ + invoice, + payments }: { - billings: Tbilling[]; - detail: TInvoiceDetail; - contract: TInvoiceContract; - k8s_user: string; + invoice: Omit; + payments: RechargeBillingItem[]; }) => { - const body = JSON.stringify({ - msg_type: 'post', - content: { - post: { - zh_cn: { - title: '新的发票请求', - content: [ - [ - { - tag: 'text', - text: '以下是联系方式:' - } - ], - [ - { - tag: 'text', - text: `用户: ${k8s_user}, 电话: ${contract.phone}, ` - }, - { - tag: 'text', - text: `邮箱:${contract.email}` - } - ], - [ - { - tag: 'text', - text: '以下是发票详情:' - } - ], - [ - { - tag: 'text', - text: `发票抬头: ${detail.title}, ` - }, - { - tag: 'text', - text: `税号: ${detail.tax}, ` - }, - { - tag: 'text', - text: `开户行: ${detail.bank}, ` - }, - { - tag: 'text', - text: `银行账号: ${detail.bankAccount}, ` - }, - { - tag: 'text', - text: `地址: ${detail.address || '-'}, ` - }, - { - tag: 'text', - text: `电话: ${detail.phone || '-'}, ` - }, - { - tag: 'text', - text: `传真: ${detail.fax || '-'}, ` - }, - { - tag: 'text', - text: `总额: ¥${billings.reduce((pre, cur) => pre + cur.amount, 0)}` - } - ], - [ - { - tag: 'text', - text: '以下是所有消费记录:' - } - ], - ...billings.map((item) => [ - { - tag: 'text', - text: `订单号: ${item.order_id}, 创建时间: ${item.createdTime}, 金额: ¥${item.amount}, 可用区UID: ${item.regionUID}, 用户UID ${item.userUID}` - } - ]) - ] - } + const { contract, detail } = JSON.parse(invoice.detail) as InvoicesCollection; + const invoiceDetail = [ + { + invoiceKey: `用户`, + invoiceValue: invoice.userID + }, + { + invoiceKey: `联系电话`, + invoiceValue: contract.phone + }, + { + invoiceKey: `接收邮箱`, + invoiceValue: contract.email + }, + { + invoiceKey: `发票抬头`, + invoiceValue: detail.title + }, + { + invoiceKey: `发票类型`, + invoiceValue: detail.type === 'special' ? '专票' : '普票' + }, + { + invoiceKey: `税号`, + invoiceValue: detail.tax + }, + { + invoiceKey: `开户行`, + invoiceValue: detail.bank + }, + { + invoiceKey: `银行账号`, + invoiceValue: detail.bankAccount + }, + { + invoiceKey: `地址`, + invoiceValue: detail.address || '-' + }, + { + invoiceKey: `电话`, + invoiceValue: detail.phone || '-' + }, + { + invoiceKey: `传真`, + invoiceValue: detail.fax || '-' + }, + { + invoiceKey: `总额`, + invoiceValue: `¥${formatMoney(invoice.totalAmount)}` + } + ]; + const invoiceStatus: InvoicePayload['status'] = invoice.status; + const invoiceAmount = formatMoney(invoice.totalAmount); + const billingList = payments.map((v) => ({ + order_id: v.ID, + regionUID: v.RegionUID, + createdTime: parseISO(v.CreatedAt).getTime(), + amount: formatMoney(v.Amount) + })); + const card = { + type: 'template', + data: { + template_id: AppConfig.costCenter.invoice.feishApp.template.id, + template_version_name: AppConfig.costCenter.invoice.feishApp.template.version, + template_variable: { + invoiceId: invoice.id, + invoiceDetail, + invoiceStatus: getStatus(invoiceStatus), + billingList, + invoiceAmount, + invoiceCreatedTime: invoice.createdAt, + invoiceKv: invoiceDetail.reduce( + (pre, { invoiceKey, invoiceValue }) => pre + `- ${invoiceKey}: ${invoiceValue}\n`, + '\n' + ) } } + }; + return card; +}; +export const getInvoicePayments = async (invoiceID: string): Promise => { + try { + const token = AppConfig.costCenter.invoice.serviceToken; + if (!token) throw Error('token is null'); + const url = makeAPIURL(null, `/account/v1alpha1/invoice/get-payment`); + const res = await fetch(url, { + method: 'POST', + body: JSON.stringify({ + token, + invoiceID + }) + }); + const result = await res.json(); + + if (!res.ok) { + console.log(result); + return []; + } + + return result.data || []; + } catch (e) { + console.log(e); + return []; + } +}; +export const sendToBot = async ({ + invoiceDetail, + payments +}: { + invoiceDetail: { + detail: ReqGenInvoice['detail']; + contract: Omit; + }; + payments: RechargeBillingItem[]; +}) => { + // await updateTenantAccessToken() + const card = generateBotTemplate({ + invoice: { + detail: JSON.stringify(invoiceDetail), + id: '-', + status: 'PENDING', + userID: '', + createdAt: new Date(), + totalAmount: 0 + }, + payments }); - const url = global.AppConfig.costCenter.invoice.feiShuBotURL; - const result = await axios.post(url, body, { + const body = { + msg_type: 'interactive', + receive_id: AppConfig.costCenter.invoice.feishApp.chatId, + content: JSON.stringify(card) + }; + + const result = await feishuAxios.post('/im/v1/messages?receive_id_type=chat_id', body, { timeout: 15000, headers: { 'Content-Type': 'application/json' } }); + const data = result.data; + if (data.code === 0 && data.msg === 'success' && data?.data?.message_id) { + return data.data.message_id as string; + } else return null; +}; +export const sendToUpdateBot = async ({ + invoice, + payments, + message_id +}: { + invoice: Omit; + payments: RechargeBillingItem[]; + message_id: string; +}) => { + const card = generateBotTemplate({ invoice, payments }); + const body = { + msg_type: 'interactive', + receive_id: AppConfig.costCenter.invoice.feishApp.chatId, + content: JSON.stringify(card) + }; + + const result = await feishuAxios.patch(`/im/v1/messages/${message_id}`, body, { + timeout: 15000, + headers: { + 'Content-Type': 'application/json' + } + }); + const data = result.data; + if (data.code !== 0 || data.msg !== 'success') { + console.log(result); + throw Error('update error'); + } return result.data; }; +export const sendToWithdrawBot = async ({ message_id }: { message_id: string }) => { + const result = await feishuAxios.delete(`/im/v1/messages/${message_id}`, { + timeout: 15000, + headers: { + 'Content-Type': 'application/json' + } + }); + const data = result.data; + if (data.code !== 0 || data.msg !== 'success') { + console.log(result); + throw Error('delete error'); + } + return result.data; +}; +export const callbackToUpdateBot = async ( + res: NextApiResponse, + { + invoice, + payments + }: { + invoice: InvoicePayload; + payments: RechargeBillingItem[]; + } +) => { + const card = generateBotTemplate({ invoice, payments }); + return res.json({ + toast: { + type: 'info', + content: '状态变更成功', + i18n: { + zh_cn: '状态变更成功', + en_us: 'card action success' + } + }, + card + }); +}; diff --git a/frontend/providers/costcenter/src/stores/billing.ts b/frontend/providers/costcenter/src/stores/billing.ts index d9d595685f29..26cdc73d0570 100644 --- a/frontend/providers/costcenter/src/stores/billing.ts +++ b/frontend/providers/costcenter/src/stores/billing.ts @@ -69,7 +69,6 @@ const useBillingStore = create()( const appName = getAppName(); const newAppNameIdx = appNameList.findIndex((item) => item === appName); const appNameIdx = newAppNameIdx === -1 ? 0 : newAppNameIdx; - console.log('setAppNameList', JSON.stringify(appNameList), appNameIdx, appName); set({ appNameList, appNameIdx }); }, setAppTypeList(appTypeList: string[]) { diff --git a/frontend/providers/costcenter/src/stores/invoce.ts b/frontend/providers/costcenter/src/stores/invoce.ts new file mode 100644 index 000000000000..839b1022decf --- /dev/null +++ b/frontend/providers/costcenter/src/stores/invoce.ts @@ -0,0 +1,17 @@ +import { InvoicePayload } from '@/types'; +import { create } from 'zustand'; +type InvoiceState = { + invoiceDetail: string; + data: InvoicePayload | undefined; + setData: (data?: InvoicePayload) => void; + setInvoiceDetail: (invoiceDetail: string) => void; +}; + +const useInvoiceStore = create((set, get) => ({ + invoiceDetail: '', + data: undefined, + setInvoiceDetail: (invoiceDetail: string) => set({ invoiceDetail }), + setData: (data?: InvoicePayload) => set({ data }) +})); + +export default useInvoiceStore; diff --git a/frontend/providers/costcenter/src/styles/chakraTheme.ts b/frontend/providers/costcenter/src/styles/chakraTheme.ts index f0e13ee2688c..2a001805120b 100644 --- a/frontend/providers/costcenter/src/styles/chakraTheme.ts +++ b/frontend/providers/costcenter/src/styles/chakraTheme.ts @@ -23,6 +23,34 @@ const Button = defineStyleConfig({ } } }); +const Tabs = defineStyleConfig({ + variants: { + primary: { + tablist: { + // borderColor: '#EFF0F1', + alignItems: 'center', + border: 'unset', + gap: '12px', + fontWeight: '500' + }, + tab: { + fontWeight: '500', + px: '4px', + py: '8px', + borderBottom: '1.5px solid', + borderColor: 'transparent', + color: 'grayModern.500', + _selected: { color: 'grayModern.900', borderColor: 'grayModern.900' }, + _active: { + color: 'unset' + } + }, + tabpanels: { + mt: '12px' + } + } + } +}); const Input = defineStyleConfig({}); @@ -81,7 +109,8 @@ export const theme = extendTheme(originTheme, { components: { Select, Heading, - Card + Card, + Tabs }, breakpoints: { base: '0em', sm: '30em', md: '48em', lg: '62em', xl: '80em', '2xl': '96em' }, styles: { diff --git a/frontend/providers/costcenter/src/types/billing.ts b/frontend/providers/costcenter/src/types/billing.ts index d380f6ec23af..b720674e7e83 100644 --- a/frontend/providers/costcenter/src/types/billing.ts +++ b/frontend/providers/costcenter/src/types/billing.ts @@ -101,7 +101,9 @@ export type RechargeBillingItem = { InvoicedAt: boolean; }; export type RechargeBillingData = { - payment: RechargeBillingItem[]; + payments: RechargeBillingItem[]; + totalPage: number; + total: number; }; export type TransferBillingData = { diff --git a/frontend/providers/costcenter/src/types/config.ts b/frontend/providers/costcenter/src/types/config.ts index c8e4d2bdd2d6..84451e33bf00 100644 --- a/frontend/providers/costcenter/src/types/config.ts +++ b/frontend/providers/costcenter/src/types/config.ts @@ -12,7 +12,18 @@ export type Mongo = { export type Invoice = { enabled: boolean; - feiShuBotURL: string; + feishApp: { + appId: string; + appSecret: string; + feiShuBotURL: string; + chatId: string; + token: string; + template: { + id: string; + version: string; + }; + }; + serviceToken: string; aliSms: AliSms; mongo: Mongo; }; @@ -52,6 +63,12 @@ export type AppConfigType = { recharge: Recharge; components: Components; gpuEnabled: boolean; + auth: { + jwt: { + internal: string; + external: string; + }; + }; }; }; @@ -61,7 +78,18 @@ export var DefaultAppConfig: AppConfigType = { currencyType: 'shellCoin', invoice: { enabled: false, - feiShuBotURL: '', + feishApp: { + appId: '', + appSecret: '', + feiShuBotURL: '', + chatId: '', + token: '', + template: { + id: '', + version: '' + } + }, + serviceToken: '', aliSms: { endpoint: '', accessKeyID: '', @@ -90,7 +118,13 @@ export var DefaultAppConfig: AppConfigType = { url: 'http://account-service.account-system.svc:2333' } }, - gpuEnabled: false + gpuEnabled: false, + auth: { + jwt: { + internal: '', + external: '' + } + } }, cloud: { regionUID: '', @@ -100,4 +134,5 @@ export var DefaultAppConfig: AppConfigType = { declare global { var AppConfig: AppConfigType; + var feishuClient: any; } diff --git a/frontend/providers/costcenter/src/types/invoice.ts b/frontend/providers/costcenter/src/types/invoice.ts index 7715fcc6b71c..b51003c487b1 100644 --- a/frontend/providers/costcenter/src/types/invoice.ts +++ b/frontend/providers/costcenter/src/types/invoice.ts @@ -4,6 +4,7 @@ export type TInvoiceDetail = { title: string; tax: string; bank: string; + type?: string; bankAccount: string; address?: string; phone?: string; @@ -18,6 +19,7 @@ export type ReqGenInvoice = { detail: TInvoiceDetail; contract: TInvoiceContract & { code: string }; billings: RechargeBillingItem[]; + token: string; }; export type Tbilling = { order_id: string; @@ -35,3 +37,19 @@ export type InvoicesCollection = { k8s_user: string; createdTime: Date; }; +type InoviceStatus = 'COMPLETED' | 'PENDING' | 'REJECTED'; +export type InvoicePayload = { + id: string; + userID: string; + createdAt: Date; + updatedAt: Date; + detail: string; + remark: unknown; + totalAmount: number; + status: InoviceStatus; +}; +export type InvoiceListData = { + total: number; + totalPage: number; + invoices: InvoicePayload[]; +};