diff --git a/src/components/settings/invoices/DefaultCustomSectionDialog.tsx b/src/components/settings/invoices/DefaultCustomSectionDialog.tsx new file mode 100644 index 000000000..53b32428f --- /dev/null +++ b/src/components/settings/invoices/DefaultCustomSectionDialog.tsx @@ -0,0 +1,81 @@ +import { forwardRef, useImperativeHandle, useRef, useState } from 'react' + +import { Button, Dialog, DialogRef } from '~/components/designSystem' +import { useInternationalization } from '~/hooks/core/useInternationalization' + +type ActionType = 'setDefault' | 'removeDefault' + +type TDefaultCustomSectionDialogProps = { + type: ActionType + onConfirm: () => void + onCancel: () => void +} + +export interface DefaultCustomSectionDialogRef { + openDialog: (props: TDefaultCustomSectionDialogProps) => unknown + closeDialog: () => unknown +} + +export const DefaultCustomSectionDialog = forwardRef( + (_props, ref) => { + const { translate } = useInternationalization() + const dialogRef = useRef(null) + const [localData, setLocalData] = useState() + + useImperativeHandle(ref, () => ({ + openDialog: (props) => { + setLocalData(props) + dialogRef.current?.openDialog() + }, + closeDialog: () => { + setLocalData(undefined) + dialogRef.current?.closeDialog() + }, + })) + + return ( + ( + <> + + + + + )} + data-test="set-invoice-default-section-dialog" + /> + ) + }, +) + +DefaultCustomSectionDialog.displayName = 'DefaultCustomSectionDialog' diff --git a/src/components/settings/invoices/PreviewCustomSectionDrawer.tsx b/src/components/settings/invoices/PreviewCustomSectionDrawer.tsx new file mode 100644 index 000000000..f419d60a7 --- /dev/null +++ b/src/components/settings/invoices/PreviewCustomSectionDrawer.tsx @@ -0,0 +1,108 @@ +import { gql } from '@apollo/client' +import { forwardRef, useImperativeHandle, useRef, useState } from 'react' + +import { Button, Drawer, DrawerRef, Typography } from '~/components/designSystem' +import { + CreateInvoiceCustomSectionInput, + useGetOrganizationCustomFooterForInvoiceLazyQuery, +} from '~/generated/graphql' +import { useInternationalization } from '~/hooks/core/useInternationalization' +import Logo from '~/public/images/logo/lago-logo-grey.svg' + +gql` + query GetOrganizationCustomFooterForInvoice { + organization { + billingConfiguration { + invoiceFooter + } + } + } +` + +type TPreviewCustomSectionDrawerProps = Pick< + CreateInvoiceCustomSectionInput, + 'displayName' | 'details' +> + +export interface PreviewCustomSectionDrawerRef { + openDrawer: (params: TPreviewCustomSectionDrawerProps) => void + closeDrawer: () => void +} + +export const PreviewCustomSectionDrawer = forwardRef( + (_props, ref) => { + const { translate } = useInternationalization() + const drawerRef = useRef(null) + const [localData, setLocalData] = useState< + TPreviewCustomSectionDrawerProps & { + invoiceFooter?: string + } + >() + + const [getOrganizationCustomFooter] = useGetOrganizationCustomFooterForInvoiceLazyQuery() + + useImperativeHandle(ref, () => ({ + openDrawer: async (args) => { + const { data } = await getOrganizationCustomFooter() + + setLocalData({ + ...args, + invoiceFooter: data?.organization?.billingConfiguration?.invoiceFooter ?? undefined, + }) + + drawerRef.current?.openDrawer() + }, + closeDrawer: () => drawerRef.current?.closeDrawer(), + })) + + const hasLocalData = localData?.displayName || localData?.details + + return ( + ( +
+ +
+ )} + title={ +
+ + {translate('text_17326350108761jc0z8eusa8')} + +
+ } + > +
+
+ {hasLocalData && ( +
+ {localData?.displayName && ( + + {localData.displayName} + + )} + {localData?.details && ( + {localData.details} + )} +
+ )} + + + {localData?.invoiceFooter} + +
+ + {translate('text_6419c64eace749372fc72b03')} + + +
+
+
+
+ ) + }, +) + +PreviewCustomSectionDrawer.displayName = 'PreviewCustomSectionDrawer' diff --git a/src/core/router/SettingRoutes.tsx b/src/core/router/SettingRoutes.tsx index 90b7aa0c6..abeb5574f 100644 --- a/src/core/router/SettingRoutes.tsx +++ b/src/core/router/SettingRoutes.tsx @@ -18,6 +18,14 @@ const InvoiceSettings = lazyLoad( () => import(/* webpackChunkName: 'invoice-settings' */ '~/pages/settings/Invoices/InvoiceSettings'), ) + +const CreateInvoiceCustomSection = lazyLoad( + () => + import( + /* webpackChunkName: 'invoice-custom-section' */ '~/pages/settings/Invoices/CreateCustomSection' + ), +) + const TaxesSettings = lazyLoad( () => import(/* webpackChunkName: 'tax-settings' */ '~/pages/settings/TaxesSettings'), ) @@ -164,8 +172,14 @@ export const EMAILS_SCENARIO_CONFIG_ROUTE = `${SETTINGS_ROUTE}/emails/config/:ty export const XERO_INTEGRATION_ROUTE = `${INTEGRATIONS_ROUTE}/xero` export const XERO_INTEGRATION_DETAILS_ROUTE = `${INTEGRATIONS_ROUTE}/xero/:integrationId/:tab` export const DUNNINGS_SETTINGS_ROUTE = `${SETTINGS_ROUTE}/dunnings` + +/** + * Creation routes + */ export const CREATE_DUNNING_ROUTE = `${SETTINGS_ROUTE}/dunnings/create` export const UPDATE_DUNNING_ROUTE = `${SETTINGS_ROUTE}/dunnings/:campaignId/edit` +export const CREATE_INVOICE_CUSTOM_SECTION = `${INVOICE_SETTINGS_ROUTE}/custom-section/create` +export const EDIT_INVOICE_CUSTOM_SECTION = `${INVOICE_SETTINGS_ROUTE}/custom-section/:sectionId/edit` export const settingRoutes: CustomRouteObject[] = [ { @@ -348,4 +362,11 @@ export const settingRoutes: CustomRouteObject[] = [ element: , permissions: ['dunningCampaignsCreate', 'dunningCampaignsView', 'dunningCampaignsUpdate'], }, + { + path: [CREATE_INVOICE_CUSTOM_SECTION, EDIT_INVOICE_CUSTOM_SECTION], + private: true, + element: , + // TODO: Add permissions + permissions: ['organizationInvoicesUpdate'], + }, ] diff --git a/src/generated/graphql.tsx b/src/generated/graphql.tsx index 0c8f8eb04..7e870a63e 100644 --- a/src/generated/graphql.tsx +++ b/src/generated/graphql.tsx @@ -7890,6 +7890,11 @@ export type UpdateOrganizationInvoiceTemplateMutationVariables = Exact<{ export type UpdateOrganizationInvoiceTemplateMutation = { __typename?: 'Mutation', updateOrganization?: { __typename?: 'CurrentOrganization', id: string, billingConfiguration?: { __typename?: 'OrganizationBillingConfiguration', id: string, invoiceFooter?: string | null } | null } | null }; +export type GetOrganizationCustomFooterForInvoiceQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetOrganizationCustomFooterForInvoiceQuery = { __typename?: 'Query', organization?: { __typename?: 'CurrentOrganization', billingConfiguration?: { __typename?: 'OrganizationBillingConfiguration', invoiceFooter?: string | null } | null } | null }; + export type CreateInviteMutationVariables = Exact<{ input: CreateInviteInput; }>; @@ -8196,6 +8201,29 @@ export type UpdateDunningCampaignMutationVariables = Exact<{ export type UpdateDunningCampaignMutation = { __typename?: 'Mutation', updateDunningCampaign?: { __typename?: 'DunningCampaign', id: string, name: string, code: string, description?: string | null, daysBetweenAttempts: number, maxAttempts: number, appliedToOrganization: boolean, thresholds: Array<{ __typename?: 'DunningCampaignThreshold', amountCents: any, currency: CurrencyEnum }> } | null }; +export type InvoiceCustomSectionFormFragment = { __typename?: 'InvoiceCustomSection', name: string, code: string, description?: string | null, details?: string | null, displayName?: string | null, selected: boolean }; + +export type GetSingleInvoiceCustomSectionQueryVariables = Exact<{ + id: Scalars['ID']['input']; +}>; + + +export type GetSingleInvoiceCustomSectionQuery = { __typename?: 'Query', invoiceCustomSection: { __typename?: 'InvoiceCustomSection', id: string, name: string, code: string, description?: string | null, details?: string | null, displayName?: string | null, selected: boolean } }; + +export type CreateInvoiceCustomSectionMutationVariables = Exact<{ + input: CreateInvoiceCustomSectionInput; +}>; + + +export type CreateInvoiceCustomSectionMutation = { __typename?: 'Mutation', createInvoiceCustomSection?: { __typename?: 'InvoiceCustomSection', id: string, name: string, code: string, description?: string | null, details?: string | null, displayName?: string | null, selected: boolean } | null }; + +export type UpdateInvoiceCustomSectionMutationVariables = Exact<{ + input: UpdateInvoiceCustomSectionInput; +}>; + + +export type UpdateInvoiceCustomSectionMutation = { __typename?: 'Mutation', updateInvoiceCustomSection?: { __typename?: 'InvoiceCustomSection', id: string, name: string, code: string, description?: string | null, details?: string | null, displayName?: string | null, selected: boolean } | null }; + export type TaxFormFragment = { __typename?: 'Tax', id: string, code: string, description?: string | null, name: string, rate: number, customersCount: number }; export type TaxFormQueryShapeFragment = { __typename?: 'Tax', autoGenerated: boolean, id: string, code: string, description?: string | null, name: string, rate: number, customersCount: number }; @@ -10481,6 +10509,16 @@ export const DunningCampaignFormFragmentDoc = gql` appliedToOrganization } `; +export const InvoiceCustomSectionFormFragmentDoc = gql` + fragment InvoiceCustomSectionForm on InvoiceCustomSection { + name + code + description + details + displayName + selected +} + `; export const TaxFormFragmentDoc = gql` fragment TaxForm on Tax { id @@ -18831,6 +18869,47 @@ export function useUpdateOrganizationInvoiceTemplateMutation(baseOptions?: Apoll export type UpdateOrganizationInvoiceTemplateMutationHookResult = ReturnType; export type UpdateOrganizationInvoiceTemplateMutationResult = Apollo.MutationResult; export type UpdateOrganizationInvoiceTemplateMutationOptions = Apollo.BaseMutationOptions; +export const GetOrganizationCustomFooterForInvoiceDocument = gql` + query GetOrganizationCustomFooterForInvoice { + organization { + billingConfiguration { + invoiceFooter + } + } +} + `; + +/** + * __useGetOrganizationCustomFooterForInvoiceQuery__ + * + * To run a query within a React component, call `useGetOrganizationCustomFooterForInvoiceQuery` and pass it any options that fit your needs. + * When your component renders, `useGetOrganizationCustomFooterForInvoiceQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetOrganizationCustomFooterForInvoiceQuery({ + * variables: { + * }, + * }); + */ +export function useGetOrganizationCustomFooterForInvoiceQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetOrganizationCustomFooterForInvoiceDocument, options); + } +export function useGetOrganizationCustomFooterForInvoiceLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetOrganizationCustomFooterForInvoiceDocument, options); + } +export function useGetOrganizationCustomFooterForInvoiceSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions) { + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(GetOrganizationCustomFooterForInvoiceDocument, options); + } +export type GetOrganizationCustomFooterForInvoiceQueryHookResult = ReturnType; +export type GetOrganizationCustomFooterForInvoiceLazyQueryHookResult = ReturnType; +export type GetOrganizationCustomFooterForInvoiceSuspenseQueryHookResult = ReturnType; +export type GetOrganizationCustomFooterForInvoiceQueryResult = Apollo.QueryResult; export const CreateInviteDocument = gql` mutation createInvite($input: CreateInviteInput!) { createInvite(input: $input) { @@ -20226,6 +20305,115 @@ export function useUpdateDunningCampaignMutation(baseOptions?: Apollo.MutationHo export type UpdateDunningCampaignMutationHookResult = ReturnType; export type UpdateDunningCampaignMutationResult = Apollo.MutationResult; export type UpdateDunningCampaignMutationOptions = Apollo.BaseMutationOptions; +export const GetSingleInvoiceCustomSectionDocument = gql` + query getSingleInvoiceCustomSection($id: ID!) { + invoiceCustomSection(id: $id) { + id + ...InvoiceCustomSectionForm + } +} + ${InvoiceCustomSectionFormFragmentDoc}`; + +/** + * __useGetSingleInvoiceCustomSectionQuery__ + * + * To run a query within a React component, call `useGetSingleInvoiceCustomSectionQuery` and pass it any options that fit your needs. + * When your component renders, `useGetSingleInvoiceCustomSectionQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetSingleInvoiceCustomSectionQuery({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useGetSingleInvoiceCustomSectionQuery(baseOptions: Apollo.QueryHookOptions & ({ variables: GetSingleInvoiceCustomSectionQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetSingleInvoiceCustomSectionDocument, options); + } +export function useGetSingleInvoiceCustomSectionLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetSingleInvoiceCustomSectionDocument, options); + } +export function useGetSingleInvoiceCustomSectionSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions) { + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(GetSingleInvoiceCustomSectionDocument, options); + } +export type GetSingleInvoiceCustomSectionQueryHookResult = ReturnType; +export type GetSingleInvoiceCustomSectionLazyQueryHookResult = ReturnType; +export type GetSingleInvoiceCustomSectionSuspenseQueryHookResult = ReturnType; +export type GetSingleInvoiceCustomSectionQueryResult = Apollo.QueryResult; +export const CreateInvoiceCustomSectionDocument = gql` + mutation createInvoiceCustomSection($input: CreateInvoiceCustomSectionInput!) { + createInvoiceCustomSection(input: $input) { + id + ...InvoiceCustomSectionForm + } +} + ${InvoiceCustomSectionFormFragmentDoc}`; +export type CreateInvoiceCustomSectionMutationFn = Apollo.MutationFunction; + +/** + * __useCreateInvoiceCustomSectionMutation__ + * + * To run a mutation, you first call `useCreateInvoiceCustomSectionMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateInvoiceCustomSectionMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [createInvoiceCustomSectionMutation, { data, loading, error }] = useCreateInvoiceCustomSectionMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useCreateInvoiceCustomSectionMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateInvoiceCustomSectionDocument, options); + } +export type CreateInvoiceCustomSectionMutationHookResult = ReturnType; +export type CreateInvoiceCustomSectionMutationResult = Apollo.MutationResult; +export type CreateInvoiceCustomSectionMutationOptions = Apollo.BaseMutationOptions; +export const UpdateInvoiceCustomSectionDocument = gql` + mutation updateInvoiceCustomSection($input: UpdateInvoiceCustomSectionInput!) { + updateInvoiceCustomSection(input: $input) { + id + ...InvoiceCustomSectionForm + } +} + ${InvoiceCustomSectionFormFragmentDoc}`; +export type UpdateInvoiceCustomSectionMutationFn = Apollo.MutationFunction; + +/** + * __useUpdateInvoiceCustomSectionMutation__ + * + * To run a mutation, you first call `useUpdateInvoiceCustomSectionMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUpdateInvoiceCustomSectionMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [updateInvoiceCustomSectionMutation, { data, loading, error }] = useUpdateInvoiceCustomSectionMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useUpdateInvoiceCustomSectionMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UpdateInvoiceCustomSectionDocument, options); + } +export type UpdateInvoiceCustomSectionMutationHookResult = ReturnType; +export type UpdateInvoiceCustomSectionMutationResult = Apollo.MutationResult; +export type UpdateInvoiceCustomSectionMutationOptions = Apollo.BaseMutationOptions; export const GetSingleTaxDocument = gql` query getSingleTax($id: ID!) { tax(id: $id) { diff --git a/src/hooks/useCreateEditInvoiceCustomSection.ts b/src/hooks/useCreateEditInvoiceCustomSection.ts new file mode 100644 index 000000000..1aa410f24 --- /dev/null +++ b/src/hooks/useCreateEditInvoiceCustomSection.ts @@ -0,0 +1,138 @@ +import { gql } from '@apollo/client' +import { useEffect, useMemo } from 'react' +import { useNavigate, useParams } from 'react-router-dom' + +import { addToast, hasDefinedGQLError } from '~/core/apolloClient' +import { FORM_ERRORS_ENUM } from '~/core/constants/form' +import { ERROR_404_ROUTE, INVOICE_SETTINGS_ROUTE } from '~/core/router' +import { + CreateInvoiceCustomSectionInput, + InvoiceCustomSectionFormFragment, + LagoApiError, + useCreateInvoiceCustomSectionMutation, + useGetSingleInvoiceCustomSectionQuery, + useUpdateInvoiceCustomSectionMutation, +} from '~/generated/graphql' + +gql` + fragment InvoiceCustomSectionForm on InvoiceCustomSection { + name + code + description + details + displayName + selected + } + + query getSingleInvoiceCustomSection($id: ID!) { + invoiceCustomSection(id: $id) { + id + ...InvoiceCustomSectionForm + } + } + + mutation createInvoiceCustomSection($input: CreateInvoiceCustomSectionInput!) { + createInvoiceCustomSection(input: $input) { + id + ...InvoiceCustomSectionForm + } + } + + mutation updateInvoiceCustomSection($input: UpdateInvoiceCustomSectionInput!) { + updateInvoiceCustomSection(input: $input) { + id + ...InvoiceCustomSectionForm + } + } +` + +interface UseCreateEditInvoiceCustomSectionReturn { + loading: boolean + errorCode?: string + isEdition: boolean + invoiceCustomSection?: InvoiceCustomSectionFormFragment + onSave: (value: CreateInvoiceCustomSectionInput) => Promise + onClose: () => void +} + +export const useCreateEditInvoiceCustomSection = (): UseCreateEditInvoiceCustomSectionReturn => { + const navigate = useNavigate() + const { sectionId } = useParams() + + const { data, loading, error } = useGetSingleInvoiceCustomSectionQuery({ + variables: { + id: sectionId as string, + }, + skip: !sectionId, + }) + + const [create, { error: createError }] = useCreateInvoiceCustomSectionMutation({ + context: { silentErrorCodes: [LagoApiError.UnprocessableEntity] }, + onCompleted({ createInvoiceCustomSection }) { + if (!!createInvoiceCustomSection) { + addToast({ + severity: 'success', + translateKey: 'text_17338418252493b2rz0ks49m', + }) + } + navigate(INVOICE_SETTINGS_ROUTE) + }, + }) + + const [update, { error: updateError }] = useUpdateInvoiceCustomSectionMutation({ + context: { silentErrorCodes: [LagoApiError.UnprocessableEntity] }, + onCompleted({ updateInvoiceCustomSection }) { + if (!!updateInvoiceCustomSection) { + addToast({ + severity: 'success', + translateKey: 'text_1733841825249i5g7vr4gnzo', + }) + } + navigate(INVOICE_SETTINGS_ROUTE) + }, + }) + + useEffect(() => { + if (hasDefinedGQLError('NotFound', error, 'invoiceCustomSection')) { + navigate(ERROR_404_ROUTE) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [error]) + + const errorCode = useMemo(() => { + if (hasDefinedGQLError('ValueAlreadyExist', createError || updateError)) { + return FORM_ERRORS_ENUM.existingCode + } + + return undefined + }, [createError, updateError]) + + return useMemo( + () => ({ + loading, + errorCode, + isEdition: !!sectionId, + invoiceCustomSection: data?.invoiceCustomSection || undefined, + onClose: () => navigate(INVOICE_SETTINGS_ROUTE), + onSave: async (values) => { + !!sectionId + ? await update({ + variables: { + input: { + ...values, + id: sectionId, + }, + }, + }) + : await create({ + variables: { + input: { + ...values, + }, + }, + }) + }, + }), + [sectionId, data?.invoiceCustomSection, errorCode, loading, navigate, create, update], + ) +} diff --git a/src/layouts/Settings.tsx b/src/layouts/Settings.tsx index de24a06c6..cd708fed3 100644 --- a/src/layouts/Settings.tsx +++ b/src/layouts/Settings.tsx @@ -7,8 +7,10 @@ import { Button, Typography, VerticalMenu } from '~/components/designSystem' import { AUTHENTICATION_ROUTE, CREATE_DUNNING_ROUTE, + CREATE_INVOICE_CUSTOM_SECTION, CREATE_TAX_ROUTE, DUNNINGS_SETTINGS_ROUTE, + EDIT_INVOICE_CUSTOM_SECTION, EMAILS_SETTINGS_ROUTE, HOME_ROUTE, INTEGRATIONS_ROUTE, @@ -46,7 +48,14 @@ const Settings = () => { } return acc }, - [CREATE_TAX_ROUTE, UPDATE_TAX_ROUTE, CREATE_DUNNING_ROUTE, UPDATE_DUNNING_ROUTE], + [ + CREATE_TAX_ROUTE, + UPDATE_TAX_ROUTE, + CREATE_DUNNING_ROUTE, + UPDATE_DUNNING_ROUTE, + CREATE_INVOICE_CUSTOM_SECTION, + EDIT_INVOICE_CUSTOM_SECTION, + ], ) return ( diff --git a/src/pages/settings/Invoices/CreateCustomSection.tsx b/src/pages/settings/Invoices/CreateCustomSection.tsx new file mode 100644 index 000000000..98be2a970 --- /dev/null +++ b/src/pages/settings/Invoices/CreateCustomSection.tsx @@ -0,0 +1,260 @@ +import { useFormik } from 'formik' +import { useRef, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { boolean, object, string } from 'yup' + +import { Button, Tooltip, Typography } from '~/components/designSystem' +import { SwitchField, TextInputField } from '~/components/form' +import { CenteredPage } from '~/components/layouts/Pages' +import { + DefaultCustomSectionDialog, + DefaultCustomSectionDialogRef, +} from '~/components/settings/invoices/DefaultCustomSectionDialog' +import { + PreviewCustomSectionDrawer, + PreviewCustomSectionDrawerRef, +} from '~/components/settings/invoices/PreviewCustomSectionDrawer' +import { WarningDialog, WarningDialogRef } from '~/components/WarningDialog' +import { INVOICE_SETTINGS_ROUTE } from '~/core/router' +import { CreateInvoiceCustomSectionInput } from '~/generated/graphql' +import { useInternationalization } from '~/hooks/core/useInternationalization' +import { useCreateEditInvoiceCustomSection } from '~/hooks/useCreateEditInvoiceCustomSection' +import { FormLoadingSkeleton } from '~/styles/mainObjectsForm' + +const CreateInvoiceCustomSection = () => { + const { translate } = useInternationalization() + const navigate = useNavigate() + + const warningDirtyAttributesDialogRef = useRef(null) + const defaultCustomSectionDialogRef = useRef(null) + const previewCustomSectionDrawerRef = useRef(null) + + const { loading, isEdition, invoiceCustomSection, onSave } = useCreateEditInvoiceCustomSection() + + const formikProps = useFormik({ + initialValues: { + name: invoiceCustomSection?.name || '', + code: invoiceCustomSection?.code || '', + description: invoiceCustomSection?.description || '', + displayName: invoiceCustomSection?.displayName || '', + details: invoiceCustomSection?.details || '', + selected: invoiceCustomSection?.selected || false, + }, + validationSchema: object().shape({ + name: string().required(''), + code: string().required(''), + description: string(), + displayName: string().when('details', { + is: (details: string) => !details, + then: (schema) => schema.required(''), + otherwise: (schema) => schema.notRequired(), + }), + selected: boolean().required(''), + }), + enableReinitialize: true, + validateOnMount: true, + onSubmit: async (values) => { + onSave(values) + }, + }) + + const [shouldDisplayDescription, setShouldDisplayDescription] = useState( + !!formikProps.initialValues.description, + ) + + const onSubmit = () => { + if (!!formikProps.values.selected) { + defaultCustomSectionDialogRef.current?.openDialog({ + type: 'setDefault', + onConfirm: formikProps.submitForm, + onCancel: () => formikProps.setFieldValue('selected', false), + }) + } else { + formikProps.submitForm() + } + } + + return ( + <> + + + + {isEdition + ? translate('text_1733841825248s6mxx67rsw7') + : translate('text_1732553358445p5bxpiijc65')} + + + )} + + +
+
+ + {translate('text_1732553358445ia697d93gbj')} + + + {translate('text_1732553358445diim0lbo5nl')} + +
+ + + +
+ +
+ +
+ + + )} + + + + + + +
+ + navigate(INVOICE_SETTINGS_ROUTE)} + /> + + + + ) +} + +export default CreateInvoiceCustomSection diff --git a/translations/base.json b/translations/base.json index 1c09c4354..b2d473518 100644 --- a/translations/base.json +++ b/translations/base.json @@ -639,9 +639,9 @@ "text_62e0ee200a543924c8f67759": "Looks like there are no available credits, please top up this wallet. If you already did, it can take some time to be updated.", "text_62e0ee200a543924c8f6775d": "Top up credits", "text_62e161ceb87c201025388aa2": "Edit wallet", - "text_62e161ceb87c201025388ada": "Top up credits", "text_62e161ceb87c201025388ade": "Terminate wallet", "text_62e257c032ae895bbfead62e": "Wallet successfully terminated", + "text_62e161ceb87c201025388ada": "Top up credits", "text_62e2a2f2a79d60429eff3035": "Terminated", "text_62e79671d23ae6ff149de924": "Top up credits to this customer", "text_62e79671d23ae6ff149de928": "A new invoice will be generated for each top up. The credits granted will be added to the available balance once the invoice is paid.", @@ -2734,5 +2734,26 @@ "text_1734774653389kvylgxjiltu": "Is between", "text_1734774653389pt3rhh3lspa": "Is equal to", "text_1734792781750cot2uyp6f1x": "Is up to", - "text_17347927817503hromltntvm": "Is at least" -} + "text_17347927817503hromltntvm": "Is at least", + "text_1732553358445p5bxpiijc65": "Add an invoice custom section", + "text_1732553358445168zt8fopyf": "Invoice custom section", + "text_1732553358445p7rg0i0dzws": "Display custom information like bank details on invoice documents.", + "text_1732553358445sjgzrnstueo": "Custom section settings", + "text_17325533584451rema9e6rs5": "Define a custom section name to easily identify it.", + "text_1732553358445ia697d93gbj": "Custom section content", + "text_1732553358445diim0lbo5nl": "Define the content to be displayed on invoices. This content will appear at the end of the invoices, just before the invoice footer.", + "text_1732553358445fhl5zibpn2l": "Content", + "text_1732553358446t0zh79g9ruk": "Add custom section content", + "text_1732553889947h4iijpzflwj": "Set as default for all invoices", + "text_1732553889948thkn4jonnyy": "For more flexibility, override it at the customer level.", + "text_17325538899488ftsvph8ko5": "Add custom section", + "text_1732634307286ij3e2kb6c4v": "Set these invoice custom section details as default", + "text_17326343072869mt7lostpsa": "This change will affect all customers who currently use the organization's invoice custom section details. This will be added and appear on their future invoices. Are you sure you want to proceed?", + "text_1732634307286y0yb9lyb70f": "Remove this invoice custom section details as default", + "text_1732634307286joro3vhe88v": "This change will remove these invoice custom section as the default option for the organization. All customers currently using it will be affected. These details will no longer appear on their future invoices. Are you sure you want to proceed?", + "text_1733841825248s6mxx67rsw7": "Edit invoice custom section", + "text_17338418252493b2rz0ks49m": "Invoice custom section successfully created", + "text_1733841825249i5g7vr4gnzo": "Invoice custom section successfully edited", + "text_17326350108761jc0z8eusa8": "Invoice preview", + "text_173255335844629sa49oljif": "Preview invoice" +} \ No newline at end of file