* feat: invoice (#2293)

* feat: default voice header

* add i18n

* refactor: 优化代码

* feat: 用户开票

* refactor: 代码优化&&样式联调 (#2384)

* Feat: invoice upload (#2424)

* refactor: 验收问题&&样式调整

* feat: 文件上传

* 小调整

* perf: invoice ui

---------

Co-authored-by: papapatrick <109422393+Patrickill@users.noreply.github.com>
This commit is contained in:
Archer
2024-08-19 17:44:48 +08:00
committed by GitHub
parent 884c2d9553
commit 5fab3734fa
37 changed files with 1093 additions and 31 deletions

View File

@@ -2,7 +2,7 @@ import React, { useEffect, useMemo } from 'react';
import { Controller } from 'react-hook-form';
import RenderPluginInput from './renderPluginInput';
import { Button, Flex } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { PluginRunContext } from '../context';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';

View File

@@ -5,7 +5,7 @@ import type { PermissionValueType } from '@fastgpt/global/support/permission/typ
import { ReadPermissionVal, WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
export enum defaultPermissionEnum {
private = 'private',

View File

@@ -83,7 +83,7 @@ const Account = () => {
) : (
<>
<MyInfo />
{!!standardPlan && <PlanUsage />}
{standardPlan && <PlanUsage />}
<Other />
</>
)}

View File

@@ -0,0 +1,300 @@
import {
getInvoiceBillsList,
invoiceBillDataType,
submitInvoice
} from '@/web/support/wallet/bill/invoice/api';
import {
Box,
Button,
Checkbox,
Flex,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
useDisclosure
} from '@chakra-ui/react';
import { billTypeMap } from '@fastgpt/global/support/wallet/bill/constants';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/usage/tools';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import dayjs from 'dayjs';
import { useTranslation } from 'next-i18next';
import { useCallback, useState } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Divider from '@/pages/app/detail/components/WorkflowComponents/Flow/components/Divider';
import { TeamInvoiceHeaderType } from '@fastgpt/global/support/user/team/type';
import { InvoiceHeaderSingleForm } from './InvoiceHeaderForm';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { getTeamInvoiceHeader } from '@/web/support/user/team/api';
import { useToast } from '@fastgpt/web/hooks/useToast';
type chosenBillDataType = {
_id: string;
price: number;
};
const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { toast } = useToast();
const [chosenBillDataList, setChosenBillDataList] = useState<chosenBillDataType[]>([]);
const [totalPrice, setTotalPrice] = useState(0);
const [formData, setFormData] = useState<TeamInvoiceHeaderType>({
teamName: '',
unifiedCreditCode: '',
companyAddress: '',
companyPhone: '',
bankName: '',
bankAccount: '',
needSpecialInvoice: undefined,
emailAddress: ''
});
const {
isOpen: isOpenSettleModal,
onOpen: onOpenSettleModal,
onClose: onCloseSettleModal
} = useDisclosure();
const handleChange = useCallback((e: any) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
}, []);
const handleRatiosChange = useCallback((v: string) => {
setFormData((prev) => ({ ...prev, needSpecialInvoice: v === 'true' }));
}, []);
const isHeaderValid = useCallback((v: TeamInvoiceHeaderType) => {
const emailRegex = /\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/;
for (const [key, value] of Object.entries(v)) {
if (typeof value === 'string' && value.trim() === '') {
return false;
}
}
return emailRegex.test(v.emailAddress);
}, []);
const {
loading: isLoading,
data: billsList,
run: getInvoiceBills
} = useRequest2(() => getInvoiceBillsList(), {
manual: false
});
const { run: handleSubmitInvoice, loading: isSubmitting } = useRequest2(
() =>
submitInvoice({
amount: totalPrice,
billIdList: chosenBillDataList.map((item) => item._id),
...formData
}),
{
manual: true,
successToast: t('common:common.submit_success'),
errorToast: t('common:common.Submit failed'),
onSuccess: () => onClose()
}
);
const { loading: isLoadingHeader } = useRequest2(() => getTeamInvoiceHeader(), {
manual: false,
onSuccess: (res) => setFormData(res)
});
const handleSubmit = useCallback(async () => {
if (!isHeaderValid(formData)) {
toast({
title: t('common:support.wallet.invoice_data.in_valid'),
status: 'info'
});
return;
}
handleSubmitInvoice();
}, [formData, handleSubmitInvoice, isHeaderValid, t, toast]);
const handleBack = useCallback(() => {
setChosenBillDataList([]);
getInvoiceBills();
onCloseSettleModal();
}, [getInvoiceBills, onCloseSettleModal]);
const handleSingleCheck = useCallback(
(item: invoiceBillDataType) => {
if (chosenBillDataList.find((bill) => bill._id === item._id)) {
setChosenBillDataList(chosenBillDataList.filter((bill) => bill._id !== item._id));
} else {
setChosenBillDataList([...chosenBillDataList, { _id: item._id, price: item.price }]);
}
},
[chosenBillDataList]
);
return (
<MyModal
isOpen={true}
isCentered
iconSrc="/imgs/modal/invoice.svg"
minHeight={'42.25rem'}
w={'43rem'}
onClose={onClose}
isLoading={isLoading}
title={t('common:support.wallet.apply_invoice')}
>
{!isOpenSettleModal ? (
<Box px={['1.6rem', '3.25rem']} py={['1rem', '2rem']}>
<Box fontWeight={500} fontSize={'1rem'} pb={'0.75rem'}>
{t('common:support.wallet.billable_invoice')}
</Box>
<Box h={'27.9rem'} overflow={'auto'}>
<TableContainer>
<Table>
<Thead>
<Tr>
<Th>
<Checkbox
isChecked={
chosenBillDataList.length === billsList?.length && billsList?.length !== 0
}
onChange={(e) => {
!e.target.checked
? setChosenBillDataList([])
: setChosenBillDataList(
billsList?.map((item) => ({
_id: item._id,
price: item.price
})) || []
);
}}
/>
</Th>
<Th>{t('common:user.type')}</Th>
<Th>{t('common:user.Time')}</Th>
<Th>{t('common:support.wallet.Amount')}</Th>
</Tr>
</Thead>
<Tbody fontSize={'0.875rem'}>
{billsList?.map((item) => (
<Tr
cursor={'pointer'}
key={item._id}
onClick={(e: any) => {
if (e.target?.name && e.target.name === 'check') return;
handleSingleCheck(item);
}}
_hover={{
bg: 'blue.50'
}}
>
<Td>
<Checkbox
name="check"
isChecked={chosenBillDataList.some((i) => i._id === item._id)}
/>
</Td>
<Td>{t(billTypeMap[item.type]?.label as any)}</Td>
<Td>
{item.createTime
? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss')
: '-'}
</Td>
<Td>{t('common:pay.yuan', { amount: formatStorePrice2Read(item.price) })}</Td>
</Tr>
))}
</Tbody>
</Table>
{!isLoading && billsList && billsList.length === 0 && (
<Flex
mt={'20vh'}
flexDirection={'column'}
alignItems={'center'}
justifyContent={'center'}
>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
{t('common:support.wallet.noBill')}
</Box>
</Flex>
)}
</TableContainer>
</Box>
<Flex pt={'2.5rem'} justify={'flex-end'}>
<Button
variant={'primary'}
px="0"
isDisabled={!chosenBillDataList.length}
onClick={() => {
let total = chosenBillDataList.reduce((acc, cur) => acc + Number(cur.price), 0);
if (!total) return;
setTotalPrice(total);
onOpenSettleModal();
}}
>
<Flex alignItems={'center'}>
<Box px={'1.25rem'} py={'0.5rem'}>
{t('common:common.Confirm')}
</Box>
</Flex>
</Button>
</Flex>
</Box>
) : (
<Box px={['1.6rem', '3.25rem']} py={['1rem', '2rem']}>
<Box w={'100%'} fontSize={'0.875rem'}>
<Flex w={'100%'} justifyContent={'space-between'}>
<Box>{t('common:support.wallet.invoice_amount')}</Box>
<Box>{t('common:pay.yuan', { amount: formatStorePrice2Read(totalPrice) })}</Box>
</Flex>
<Box w={'100%'} py={4}>
<Divider showBorderBottom={false} />
</Box>
</Box>
<MyBox isLoading={isLoadingHeader}>
<Flex justify={'center'}>
<InvoiceHeaderSingleForm
formData={formData}
handleChange={handleChange}
handleRatiosChange={handleRatiosChange}
/>
</Flex>
</MyBox>
<Flex
align={'center'}
w={'19.8rem'}
h={'1.75rem'}
mt={4}
px={'0.75rem'}
py={'0.38rem'}
bg={'blue.50'}
borderRadius={'sm'}
color={'blue.600'}
>
<MyIcon name="infoRounded" w={'14px'} h={'14px'} />
<Box ml={2} fontSize={'0.6875rem'}>
{t('common:support.wallet.invoice_info')}
</Box>
</Flex>
<Flex justify={'flex-end'} w={'100%'} pt={[3, 7]}>
<Button variant={'outline'} mr={'0.75rem'} px="0" onClick={handleBack}>
<Flex alignItems={'center'}>
<Box px={'1.25rem'} py={'0.5rem'}>
{t('common:back')}
</Box>
</Flex>
</Button>
<Button isLoading={isSubmitting} px="0" onClick={handleSubmit}>
<Flex alignItems={'center'}>
<Box px={'1.25rem'} py={'0.5rem'}>
{t('common:common.Confirm')}
</Box>
</Flex>
</Button>
</Flex>
</Box>
)}
</MyModal>
);
};
export default ApplyInvoiceModal;

View File

@@ -0,0 +1,58 @@
import { Box, Button, Flex } from '@chakra-ui/react';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import dynamic from 'next/dynamic';
import { useState } from 'react';
import { useTranslation } from 'next-i18next';
import ApplyInvoiceModal from './ApplyInvoiceModal';
const TabEnum = {
bill: 'bill',
invoice: 'invoice',
invoiceHeader: 'voiceHeader'
};
const BillTable = dynamic(() => import('./BillTable'));
const InvoiceHeaderForm = dynamic(() => import('./InvoiceHeaderForm'));
const InvoiceTable = dynamic(() => import('./InvoiceTable'));
const BillAndInvoice = () => {
const [currentTab, setCurrentTab] = useState(TabEnum.bill);
const [isOpenInvoiceModal, setIsOpenInvoiceModal] = useState(false);
const { t } = useTranslation();
return (
<>
<Box p={['1rem', '2rem']}>
<Flex justifyContent={'space-between'} alignItems={'center'} pb={'0.75rem'}>
<FillRowTabs
list={[
{ label: t('common:support.wallet.bill_tag.bill'), value: TabEnum.bill },
{ label: t('common:support.wallet.bill_tag.invoice'), value: TabEnum.invoice },
{
label: t('common:support.wallet.bill_tag.default_header'),
value: TabEnum.invoiceHeader
}
]}
value={currentTab}
onChange={setCurrentTab}
></FillRowTabs>
{currentTab !== TabEnum.invoiceHeader && (
<Button variant={'primary'} px="0" onClick={() => setIsOpenInvoiceModal(true)}>
<Flex alignItems={'center'} px={'20px'}>
<Box px={'1.25rem'} py={'0.5rem'}>
{t('common:support.wallet.invoicing')}
</Box>
</Flex>
</Button>
)}
</Flex>
<Box h={'100%'}>
{currentTab === TabEnum.bill && <BillTable />}
{currentTab === TabEnum.invoice && <InvoiceTable />}
{currentTab === TabEnum.invoiceHeader && <InvoiceHeaderForm />}
</Box>
{isOpenInvoiceModal && <ApplyInvoiceModal onClose={() => setIsOpenInvoiceModal(false)} />}
</Box>
</>
);
};
export default BillAndInvoice;

View File

@@ -102,8 +102,6 @@ const BillTable = () => {
position={'relative'}
h={'100%'}
overflow={'overlay'}
py={[0, 5]}
px={[3, 8]}
>
<TableContainer>
<Table>
@@ -188,7 +186,7 @@ function BillDetailModal({ bill, onClose }: { bill: BillSchemaType; onClose: ()
isOpen={true}
onClose={onClose}
iconSrc="/imgs/modal/bill.svg"
title={t('common:support.wallet.usage.Usage Detail')}
title={t('common:support.wallet.bill_detail')}
maxW={['90vw', '700px']}
>
<ModalBody>
@@ -218,6 +216,10 @@ function BillDetailModal({ bill, onClose }: { bill: BillSchemaType; onClose: ()
<FormLabel flex={'0 0 120px'}>{t('common:support.wallet.bill.Type')}:</FormLabel>
<Box>{t(billTypeMap[bill.type]?.label as any)}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 120px'}>{t('common:support.wallet.has_invoice')}:</FormLabel>
<Box>{bill.hasInvoice ? t('common:yes') : t('common:no')}</Box>
</Flex>
{!!bill.metadata?.subMode && (
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 120px'}>

View File

@@ -0,0 +1,209 @@
import Divider from '@/pages/app/detail/components/WorkflowComponents/Flow/components/Divider';
import { getTeamInvoiceHeader, updateTeamInvoiceHeader } from '@/web/support/user/team/api';
import { Box, Button, Flex, Input, Radio, RadioGroup, Stack } from '@chakra-ui/react';
import { TeamInvoiceHeaderType } from '@fastgpt/global/support/user/team/type';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useCallback, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
const InputItem = ({
label,
value,
onChange,
name
}: {
label: string;
value: string;
onChange: (e: any) => void;
name: string;
}) => {
return (
<>
<Flex justify={'space-between'} flexDir={['column', 'row']}>
<Box fontSize={'14px'} lineHeight={'2rem'}>
{label}
</Box>
<Input
bg={'myGray.50'}
border={'1px solid'}
borderColor={'myGray.200'}
w={'21.25rem'}
focusBorderColor="myGray.200"
placeholder={label}
value={value}
onChange={onChange}
name={name}
/>
</Flex>
</>
);
};
export const InvoiceHeaderSingleForm = ({
formData,
handleChange,
handleRatiosChange
}: {
formData: TeamInvoiceHeaderType;
handleChange: (e: any) => void;
handleRatiosChange: (v: string) => void;
}) => {
const { t } = useTranslation();
return (
<>
<Flex
w={['auto', '36rem']}
flexDir={'column'}
gap={'1rem'}
fontWeight={'500'}
color={'myGray.900'}
fontSize={'14px'}
>
<InputItem
label={t('common:support.wallet.invoice_data.organization_name')}
value={formData.teamName}
onChange={handleChange}
name="teamName"
/>
<InputItem
label={t('common:support.wallet.invoice_data.unit_code')}
value={formData.unifiedCreditCode}
onChange={handleChange}
name="unifiedCreditCode"
/>
<InputItem
label={t('common:support.wallet.invoice_data.company_address')}
value={formData.companyAddress}
onChange={handleChange}
name="companyAddress"
/>
<InputItem
label={t('common:support.wallet.invoice_data.company_phone')}
value={formData.companyPhone}
onChange={handleChange}
name="companyPhone"
/>
<InputItem
label={t('common:support.wallet.invoice_data.bank')}
value={formData.bankName}
onChange={handleChange}
name="bankName"
/>
<InputItem
label={t('common:support.wallet.invoice_data.bank_account')}
value={formData.bankAccount}
onChange={handleChange}
name="bankAccount"
/>
<Flex justify={'space-between'} flexDir={['column', 'row']}>
<Box fontSize={'14px'} lineHeight={'2rem'}>
{t('common:support.wallet.invoice_data.need_special_invoice')}
</Box>
<RadioGroup
value={
formData.needSpecialInvoice === undefined
? ''
: formData.needSpecialInvoice.toString()
}
onChange={handleRatiosChange}
w={'21.25rem'}
>
<Stack direction="row" h={'2rem'}>
<Radio value="true" pr={'1rem'}>
<Box fontSize={'14px'}>{t('common:yes')}</Box>
</Radio>
<Radio value="false">
<Box fontSize={'14px'}>{t('common:no')}</Box>
</Radio>
</Stack>
</RadioGroup>
</Flex>
<Box w={'100%'}>
<Divider showBorderBottom={false} />
</Box>
<InputItem
label={t('common:support.wallet.invoice_data.email')}
value={formData.emailAddress}
onChange={handleChange}
name="emailAddress"
/>
</Flex>
</>
);
};
const InvoiceHeaderForm = () => {
const [formData, setFormData] = useState<TeamInvoiceHeaderType>({
teamName: '',
unifiedCreditCode: '',
companyAddress: '',
companyPhone: '',
bankName: '',
bankAccount: '',
needSpecialInvoice: undefined,
emailAddress: ''
});
const { loading: isLoading } = useRequest2(() => getTeamInvoiceHeader(), {
manual: false,
onSuccess: (data) => {
setFormData(data);
}
});
const { t } = useTranslation();
const { toast } = useToast();
const handleChange = useCallback((e: any) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
}, []);
const handleRatiosChange = useCallback((v: string) => {
setFormData((prev) => ({ ...prev, needSpecialInvoice: v === 'true' }));
}, []);
const isHeaderValid = useCallback((v: TeamInvoiceHeaderType) => {
const emailRegex = /\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/;
return emailRegex.test(v.emailAddress);
}, []);
const { loading: isSubmitting, run: handleSubmit } = useRequest2(
() => updateTeamInvoiceHeader(formData),
{
manual: true,
successToast: t('common:common.Save Success'),
errorToast: t('common:common.Save Failed')
}
);
const onSubmit = useCallback(() => {
if (!isHeaderValid(formData)) {
toast({
title: t('common:support.wallet.invoice_data.in_valid'),
status: 'info'
});
return;
}
handleSubmit();
}, [handleSubmit, formData, isHeaderValid, toast, t]);
return (
<>
<MyBox isLoading={isLoading} pt={['1rem', '3.5rem']}>
<Flex w={'100%'} overflow={'auto'} justify={'center'} flexDir={'column'} align={'center'}>
<InvoiceHeaderSingleForm
formData={formData}
handleChange={handleChange}
handleRatiosChange={handleRatiosChange}
/>
<Flex w={'100%'} justify={'center'} mt={'3rem'}>
<Button variant={'primary'} px="0" onClick={onSubmit} isLoading={isSubmitting}>
<Flex alignItems={'center'} px={'20px'}>
<Box px={'1.25rem'} py={'0.5rem'}>
{t('common:common.Save')}
</Box>
</Flex>
</Button>
</Flex>
</Flex>
</MyBox>
</>
);
};
export default InvoiceHeaderForm;

View File

@@ -0,0 +1,207 @@
import { getInvoiceRecords } from '@/web/support/wallet/bill/invoice/api';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useTranslation } from 'next-i18next';
import { useEffect, useState } from 'react';
import {
Box,
Button,
Flex,
FormLabel,
ModalBody,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr
} from '@chakra-ui/react';
import { usePagination } from '@fastgpt/web/hooks/usePagination';
import { InvoiceSchemaType } from '@fastgpt/global/support/wallet/bill/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import dayjs from 'dayjs';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/usage/tools';
import MyModal from '@fastgpt/web/components/common/MyModal';
const InvoiceTable = () => {
const { t } = useTranslation();
const [invoiceDetailData, setInvoiceDetailData] = useState<InvoiceSchemaType | ''>('');
const {
data: invoices,
isLoading,
Pagination,
getData,
total
} = usePagination<InvoiceSchemaType>({
api: getInvoiceRecords,
pageSize: 20,
defaultRequest: false
});
useEffect(() => {
getData(1);
}, [getData]);
return (
<MyBox isLoading={isLoading} position={'relative'} h={'100%'} overflow={'overlay'}>
<TableContainer minH={'50vh'}>
<Table>
<Thead h="3rem">
<Tr>
<Th w={'20%'}>#</Th>
<Th w={'20%'}>{t('common:user.Time')}</Th>
<Th w={'20%'}>{t('common:support.wallet.Amount')}</Th>
<Th w={'20%'}>{t('common:support.wallet.bill.Status')}</Th>
<Th w={'20%'}></Th>
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{invoices.map((item, i) => (
<Tr key={item._id}>
<Td>{i + 1}</Td>
<Td>
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
</Td>
<Td>{t('common:pay.yuan', { amount: formatStorePrice2Read(item.amount) })}</Td>
<Td>
<Flex
px={'0.75rem'}
py={'0.38rem'}
w={'4.25rem'}
h={'1.75rem'}
bg={item.status === 1 ? 'blue.50' : 'green.50'}
rounded={'md'}
justify={'center'}
align={'center'}
color={item.status === 1 ? 'blue.600' : 'green.600'}
>
<MyIcon name="point" w={'6px'} h={'6px'} />
<Box ml={'0.25rem'}>
{item.status === 1
? t('common:common.submitted')
: t('common:common.have_done')}
</Box>
</Flex>
</Td>
<Td>
<Button
onClick={() => setInvoiceDetailData(item)}
h={'2rem'}
w={'4.5rem'}
variant={'whiteBase'}
size={'sm'}
py={'0.5rem'}
px={'0.75rem'}
_hover={{
color: 'blue.600'
}}
>
<Flex>
<MyIcon name="paragraph" w={'16px'} h={'16px'} />
<Box ml={'0.38rem'}>{t('common:common.Detail')}</Box>
</Flex>
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
{total >= 20 && (
<Flex mt={3} justifyContent={'flex-end'}>
<Pagination />
</Flex>
)}
{!isLoading && invoices.length === 0 && (
<Flex
mt={'20vh'}
flexDirection={'column'}
alignItems={'center'}
justifyContent={'center'}
>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
{t('common:support.wallet.no_invoice')}
</Box>
</Flex>
)}
</TableContainer>
{!!invoiceDetailData && (
<InvoiceDetailModal invoice={invoiceDetailData} onClose={() => setInvoiceDetailData('')} />
)}
</MyBox>
);
};
export default InvoiceTable;
function InvoiceDetailModal({
invoice,
onClose
}: {
invoice: InvoiceSchemaType;
onClose: () => void;
}) {
const { t } = useTranslation();
return (
<MyModal
maxW={['90vw', '700px']}
isOpen={true}
onClose={onClose}
title={
<Flex align={'center'}>
<MyIcon name="paragraph" w={'20px'} h={'20px'} color={'blue.600'} />
<Box ml={'0.62rem'}>{t('common:support.wallet.invoice_detail')}</Box>
</Flex>
}
>
<ModalBody px={'3.25rem'} py={'2rem'}>
<Flex w={'100%'} h={'100%'} flexDir={'column'} gap={'1rem'}>
<LabelItem
label={t('common:support.wallet.invoice_amount')}
value={t('common:pay.yuan', { amount: formatStorePrice2Read(invoice.amount) })}
/>
<LabelItem
label={t('common:support.wallet.invoice_data.organization_name')}
value={invoice.teamName}
/>
<LabelItem
label={t('common:support.wallet.invoice_data.unit_code')}
value={invoice.unifiedCreditCode}
/>
<LabelItem
label={t('common:support.wallet.invoice_data.company_address')}
value={invoice.companyAddress}
/>
<LabelItem
label={t('common:support.wallet.invoice_data.company_phone')}
value={invoice.companyPhone}
/>
<LabelItem
label={t('common:support.wallet.invoice_data.bank')}
value={invoice.bankName}
/>
<LabelItem
label={t('common:support.wallet.invoice_data.bank_account')}
value={invoice.bankAccount}
/>
<LabelItem
label={t('common:support.wallet.invoice_data.need_special_invoice')}
value={invoice.needSpecialInvoice ? t('common:yes') : t('common:no')}
/>
<LabelItem
label={t('common:support.wallet.invoice_data.email')}
value={invoice.emailAddress}
/>
</Flex>
</ModalBody>
</MyModal>
);
}
function LabelItem({ label, value }: { label: string; value: string }) {
return (
<Flex alignItems={'center'} justify={'space-between'}>
<FormLabel flex={'0 0 120px'}>{label}</FormLabel>
<Box>{value}</Box>
</Flex>
);
}

View File

@@ -16,7 +16,7 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem';
const Promotion = dynamic(() => import('./components/Promotion'));
const UsageTable = dynamic(() => import('./components/UsageTable'));
const BillTable = dynamic(() => import('./components/BillTable'));
const BillAndInvoice = dynamic(() => import('./components/bill/BillAndInvoice'));
const InformTable = dynamic(() => import('./components/InformTable'));
const ApiKeyTable = dynamic(() => import('./components/ApiKeyTable'));
const Individuation = dynamic(() => import('./components/Individuation'));
@@ -53,7 +53,8 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
}
]
: []),
...(feConfigs?.show_pay && userInfo?.team?.permission.hasWritePer
// ...(feConfigs?.show_pay && userInfo?.team?.permission.hasWritePer
...(feConfigs?.show_pay || userInfo?.team?.permission.hasWritePer
? [
{
icon: 'support/bill/payRecordLight',
@@ -176,7 +177,7 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
{currentTab === TabEnum.info && <UserInfo />}
{currentTab === TabEnum.promotion && <Promotion />}
{currentTab === TabEnum.usage && <UsageTable />}
{currentTab === TabEnum.bill && <BillTable />}
{currentTab === TabEnum.bill && <BillAndInvoice />}
{currentTab === TabEnum.individuation && <Individuation />}
{currentTab === TabEnum.inform && <InformTable />}
{currentTab === TabEnum.apikey && <ApiKeyTable />}

View File

@@ -29,7 +29,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
maxSize: (global.feConfigs?.uploadFileMaxSize || 500) * 1024 * 1024
});
const { file, bucketName, metadata } = await upload.doUpload(req, res);
filePaths.push(file.path);
const { teamId, tmbId, outLinkUid } = await authChatCert({ req, authToken: true });
await authUploadLimit(outLinkUid || tmbId);

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { Box, Flex, Input } from '@chakra-ui/react';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import dayjs from 'dayjs';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
import { UseFormRegister, UseFormSetValue } from 'react-hook-form';
import { OutLinkEditType } from '@fastgpt/global/support/outLink/type';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';

View File

@@ -2,7 +2,7 @@ import { useCopyData } from '@/web/common/hooks/useCopyData';
import { Box, Image, Flex, ModalBody } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
export type ShowShareLinkModalProps = {
shareLink: string;

View File

@@ -5,7 +5,7 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
import { postCreateDatasetCollectionTag } from '@/web/core/dataset/api';
import { useContextSelector } from 'use-context-selector';
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
import { useCallback, useEffect, useState } from 'react';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { CollectionPageContext } from './Context';

View File

@@ -5,7 +5,7 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
import { postCreateDatasetCollectionTag, putDatasetCollectionById } from '@/web/core/dataset/api';
import { useContextSelector } from 'use-context-selector';
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useDeepCompareEffect } from 'ahooks';

View File

@@ -34,7 +34,7 @@ import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useFolderDrag } from '@/components/common/folder/useFolderDrag';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useI18n } from '@/web/context/I18n';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
function List() {
const { setLoading } = useSystemStore();

View File

@@ -20,7 +20,7 @@ import dynamic from 'next/dynamic';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { DatasetItemType, DatasetListItemType } from '@fastgpt/global/core/dataset/type';
import { EditResourceInfoFormType } from '@/components/common/Modal/EditResourceModal';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
const MoveModal = dynamic(() => import('@/components/common/folder/MoveModal'));

View File

@@ -14,6 +14,7 @@ import {
TeamMemberSchema
} from '@fastgpt/global/support/user/team/type.d';
import { FeTeamPlanStatusType, TeamSubSchema } from '@fastgpt/global/support/wallet/sub/type';
import { TeamInvoiceHeaderType } from '@fastgpt/global/support/user/team/type';
/* --------------- team ---------------- */
export const getTeamList = (status: `${TeamMemberSchema['status']}`) =>
@@ -61,3 +62,9 @@ export const getTeamPlanStatus = () =>
GET<FeTeamPlanStatusType>(`/support/user/team/plan/getTeamPlanStatus`, { maxQuantity: 1 });
export const getTeamPlans = () =>
GET<TeamSubSchema[]>(`/proApi/support/user/team/plan/getTeamPlans`);
export const getTeamInvoiceHeader = () =>
GET<TeamInvoiceHeaderType>(`/proApi/support/user/team/invoiceAccount/getTeamInvoiceHeader`);
export const updateTeamInvoiceHeader = (data: TeamInvoiceHeaderType) =>
POST(`/proApi/support/user/team/invoiceAccount/update`, data);

View File

@@ -0,0 +1,20 @@
import { RequestPaging } from '@/types';
import { GET, POST } from '@/web/common/api/request';
import { BillTypeEnum } from '@fastgpt/global/support/wallet/bill/constants';
import { InvoiceType } from '@fastgpt/global/support/wallet/bill/type';
import { InvoiceSchemaType } from '../../../../../../../../packages/global/support/wallet/bill/type';
export type invoiceBillDataType = {
type: BillTypeEnum;
price: number;
createTime: Date;
_id: string;
};
export const getInvoiceBillsList = () =>
GET<invoiceBillDataType[]>(`/proApi/support/wallet/bill/invoice/unInvoiceList`);
export const submitInvoice = (data: InvoiceType) =>
POST(`/proApi/support/wallet/bill/invoice/submit`, data);
export const getInvoiceRecords = (data: RequestPaging) =>
POST<InvoiceSchemaType[]>(`/proApi/support/wallet/bill/invoice/records`, data);