mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-13 11:20:31 +00:00
feat: Control the payment method from invoice form
This commit is contained in:
@@ -260,8 +260,8 @@ export default class SaleInvoicesController extends BaseController {
|
||||
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
// Payment methods.
|
||||
check('payment_methods').optional({ nullable: true }).isArray({ min: 1 }),
|
||||
check('payment_methods.*.payment_integration_id').exists(),
|
||||
check('payment_methods').optional({ nullable: true }).isArray(),
|
||||
check('payment_methods.*.payment_integration_id').exists().toInt(),
|
||||
check('payment_methods.*.enable').exists().isBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export class GetPaymentServicesSpecificInvoice {
|
||||
const { PaymentIntegration } = this.tenancy.models(tenantId);
|
||||
|
||||
const paymentGateways = await PaymentIntegration.query()
|
||||
.where('enable', true)
|
||||
.where('active', true)
|
||||
.orderBy('name', 'ASC');
|
||||
|
||||
return this.transform.transform(
|
||||
|
||||
@@ -8,4 +8,12 @@ export class GetPaymentServicesSpecificInvoiceTransformer extends Transformer {
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['accountId'];
|
||||
};
|
||||
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['serviceFormatted'];
|
||||
};
|
||||
|
||||
public serviceFormatted(method) {
|
||||
return 'Stripe';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { FCheckbox, Group } from '@/components';
|
||||
import { useUncontrolled } from '@/hooks/useUncontrolled';
|
||||
import { Checkbox, Text } from '@blueprintjs/core';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { get } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface PaymentMethodSelectProps {
|
||||
label: string;
|
||||
value?: boolean;
|
||||
initialValue?: boolean;
|
||||
onChange?: (value: boolean) => void;
|
||||
}
|
||||
export function PaymentMethodSelect({
|
||||
value,
|
||||
initialValue,
|
||||
onChange,
|
||||
label,
|
||||
}: PaymentMethodSelectProps) {
|
||||
const [_value, handleChange] = useUncontrolled<boolean>({
|
||||
value,
|
||||
initialValue,
|
||||
finalValue: false,
|
||||
onChange,
|
||||
});
|
||||
const handleClick = () => {
|
||||
handleChange(!_value);
|
||||
};
|
||||
|
||||
return (
|
||||
<PaymentMethodSelectRoot onClick={handleClick}>
|
||||
<PaymentMethodCheckbox
|
||||
label={''}
|
||||
checked={_value}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
<PaymentMethodText>{label}</PaymentMethodText>
|
||||
</PaymentMethodSelectRoot>
|
||||
);
|
||||
}
|
||||
|
||||
export interface PaymentMethodSelectFieldProps
|
||||
extends Partial<PaymentMethodSelectProps> {
|
||||
label: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function PaymentMethodSelectField({
|
||||
name,
|
||||
...props
|
||||
}: PaymentMethodSelectFieldProps) {
|
||||
const { values, setFieldValue } = useFormikContext();
|
||||
const value = useMemo(() => get(values, name), [values, name]);
|
||||
|
||||
const handleChange = (newValue: boolean) => {
|
||||
setFieldValue(name, newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<PaymentMethodSelect value={value} onChange={handleChange} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
const PaymentMethodSelectRoot = styled(Group)`
|
||||
border: 1px solid #d3d8de;
|
||||
border-radius: 3px;
|
||||
padding: 8px;
|
||||
gap: 0;
|
||||
min-width: 200px;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const PaymentMethodCheckbox = styled(Checkbox)`
|
||||
margin: 0;
|
||||
|
||||
&.bp4-control .bp4-control-indicator {
|
||||
box-shadow: 0 0 0 1px #c5cbd3;
|
||||
}
|
||||
`;
|
||||
|
||||
const PaymentMethodText = styled(Text)`
|
||||
color: #404854;
|
||||
`;
|
||||
@@ -0,0 +1,52 @@
|
||||
import styled from 'styled-components';
|
||||
import React from 'react';
|
||||
import {
|
||||
Classes,
|
||||
Popover,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import { Stack } from '@/components';
|
||||
import { PaymentMethodSelectField } from './PaymentMethodSelect';
|
||||
|
||||
interface PaymentOptionsButtonPopverProps {
|
||||
paymentMethods: Array<any>;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
export function PaymentOptionsButtonPopver({
|
||||
paymentMethods,
|
||||
children,
|
||||
}: PaymentOptionsButtonPopverProps) {
|
||||
return (
|
||||
<Popover
|
||||
interactionKind={PopoverInteractionKind.HOVER}
|
||||
position={Position.TOP_RIGHT}
|
||||
popoverClassName={Classes.POPOVER_CONTENT_SIZING}
|
||||
minimal={true}
|
||||
content={
|
||||
<Stack spacing={8}>
|
||||
<PaymentMethodsTitle>Payment Options</PaymentMethodsTitle>
|
||||
|
||||
<Stack spacing={8}>
|
||||
{paymentMethods?.map((service, key) => (
|
||||
<PaymentMethodSelectField
|
||||
name={`payment_methods.${service.id}.enable`}
|
||||
label={'Card (Stripe)'}
|
||||
key={key}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
const PaymentMethodsTitle = styled('h6')`
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
color: rgb(95, 107, 124);
|
||||
`;
|
||||
@@ -2,23 +2,25 @@
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import styled from 'styled-components';
|
||||
import { FFormGroup, FEditableText, FormattedMessage as T } from '@/components';
|
||||
import { useDialogActions } from '@/hooks/state';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import { Button, Intent } from '@blueprintjs/core';
|
||||
import {
|
||||
FFormGroup,
|
||||
FEditableText,
|
||||
FormattedMessage as T,
|
||||
Box,
|
||||
Group,
|
||||
Stack,
|
||||
} from '@/components';
|
||||
import { VisaIcon } from '@/icons/Visa';
|
||||
import { MastercardIcon } from '@/icons/Mastercard';
|
||||
import { useInvoiceFormContext } from './InvoiceFormProvider';
|
||||
import { PaymentOptionsButtonPopver } from '@/containers/PaymentMethods/SelectPaymentMethodPopover';
|
||||
|
||||
export function InvoiceFormFooterLeft() {
|
||||
const { openDialog } = useDialogActions();
|
||||
|
||||
const handleSelectPaymentMethodsClick = () => {
|
||||
openDialog(DialogsName.SelectPaymentMethod, {});
|
||||
}
|
||||
const { paymentServices } = useInvoiceFormContext();
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<FFormGroup label={'Payment Options'} name={'payment_method_id'}>
|
||||
<a href={'#'} onClick={handleSelectPaymentMethodsClick}>Payment Options</a>
|
||||
</FFormGroup>
|
||||
|
||||
<Stack spacing={20}>
|
||||
{/* --------- Invoice message --------- */}
|
||||
<InvoiceMsgFormGroup
|
||||
name={'invoice_message'}
|
||||
@@ -46,14 +48,31 @@ export function InvoiceFormFooterLeft() {
|
||||
fastField
|
||||
/>
|
||||
</TermsConditsFormGroup>
|
||||
</React.Fragment>
|
||||
|
||||
{/* --------- Payment Options --------- */}
|
||||
<PaymentOptionsFormGroup
|
||||
label={'Payment Options'}
|
||||
name={'payment_method_id'}
|
||||
>
|
||||
<PaymentOptionsText>
|
||||
Select an online payment option to get paid faster{' '}
|
||||
<Group spacing={6} style={{ marginLeft: 8 }}>
|
||||
<VisaIcon />
|
||||
<MastercardIcon />
|
||||
</Group>
|
||||
<PaymentOptionsButtonPopver paymentMethods={paymentServices}>
|
||||
<PaymentOptionsButton intent={Intent.PRIMARY} small minimal>
|
||||
Payment Options
|
||||
</PaymentOptionsButton>
|
||||
</PaymentOptionsButtonPopver>
|
||||
</PaymentOptionsText>
|
||||
</PaymentOptionsFormGroup>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const InvoiceMsgFormGroup = styled(FFormGroup)`
|
||||
&.bp4-form-group {
|
||||
margin-bottom: 40px;
|
||||
|
||||
.bp4-label {
|
||||
font-size: 12px;
|
||||
margin-bottom: 12px;
|
||||
@@ -75,3 +94,29 @@ const TermsConditsFormGroup = styled(FFormGroup)`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const PaymentOptionsFormGroup = styled(FFormGroup)`
|
||||
&.bp4-form-group {
|
||||
.bp4-label {
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const PaymentOptionsText = styled(Box)`
|
||||
font-size: 13px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #5f6b7c;
|
||||
`;
|
||||
|
||||
const PaymentOptionsButton = styled(Button)`
|
||||
font-size: 13px;
|
||||
margin-left: 4px;
|
||||
|
||||
&.bp4-minimal.bp4-intent-primary {
|
||||
color: #0052cc;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -150,6 +150,10 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) {
|
||||
editInvoiceMutate,
|
||||
setSubmitPayload,
|
||||
isNewMode,
|
||||
|
||||
// Payment Services
|
||||
paymentServices,
|
||||
isPaymentServicesLoading,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -69,6 +69,7 @@ export const defaultInvoice = {
|
||||
pdf_template_id: '',
|
||||
entries: [...repeatValue(defaultInvoiceEntry, MIN_LINES_NUMBER)],
|
||||
attachments: [],
|
||||
payment_methods: {},
|
||||
};
|
||||
|
||||
// Invoice entry request schema.
|
||||
@@ -223,9 +224,19 @@ export function transformValueToRequest(values) {
|
||||
entries: transformEntriesToRequest(values.entries),
|
||||
delivered: false,
|
||||
attachments: transformAttachmentsToRequest(values),
|
||||
payment_methods: transformPaymentMethodsToRequest(values?.payment_methods),
|
||||
};
|
||||
}
|
||||
|
||||
const transformPaymentMethodsToRequest = (
|
||||
paymentMethods: Record<string, { enable: boolean }>,
|
||||
): Array<{ payment_integration_id: string; enable: boolean }> => {
|
||||
return Object.entries(paymentMethods).map(([paymentMethodId, method]) => ({
|
||||
payment_integration_id: paymentMethodId,
|
||||
enable: method.enable,
|
||||
}));
|
||||
};
|
||||
|
||||
export const useSetPrimaryWarehouseToForm = () => {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const { warehouses, isWarehousesSuccess } = useInvoiceFormContext();
|
||||
|
||||
@@ -31,7 +31,7 @@ export const useGetPaymentServices = (
|
||||
.then(
|
||||
(response) =>
|
||||
transformToCamelCase(
|
||||
response.data?.paymentServices,
|
||||
response.data?.payment_services,
|
||||
) as GetPaymentServicesResponse,
|
||||
),
|
||||
{
|
||||
|
||||
24
packages/webapp/src/icons/Mastercard.tsx
Normal file
24
packages/webapp/src/icons/Mastercard.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
export const MastercardIcon: React.FC<React.SVGProps<SVGSVGElement>> = (
|
||||
props,
|
||||
) => {
|
||||
return (
|
||||
<svg
|
||||
height="16"
|
||||
viewBox="0 0 24 16"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<rect fill="#252525" height={16} rx={2} width={24} />
|
||||
<circle cx={9} cy={8} fill="#eb001b" r={5} />
|
||||
<circle cx={15} cy={8} fill="#f79e1b" r={5} />
|
||||
<path
|
||||
d="m12 3.99963381c1.2144467.91220633 2 2.36454836 2 4.00036619s-.7855533 3.0881599-2 4.0003662c-1.2144467-.9122063-2-2.36454837-2-4.0003662s.7855533-3.08815986 2-4.00036619z"
|
||||
fill="#ff5f00"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
47
packages/webapp/src/icons/Visa.tsx
Normal file
47
packages/webapp/src/icons/Visa.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
|
||||
export const VisaIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
return (
|
||||
<svg
|
||||
width="24px"
|
||||
height="16px"
|
||||
viewBox="0 0 24 16"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
{...props}
|
||||
>
|
||||
<g id="319" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
|
||||
<g
|
||||
id="New-Icons"
|
||||
transform="translate(-80.000000, -280.000000)"
|
||||
fillRule="nonzero"
|
||||
>
|
||||
<g id="Card-Brands" transform="translate(40.000000, 200.000000)">
|
||||
<g id="Color" transform="translate(0.000000, 80.000000)">
|
||||
<g id="Visa" transform="translate(40.000000, 0.000000)">
|
||||
<rect
|
||||
id="Container"
|
||||
strokeOpacity="0.2"
|
||||
stroke="#000000"
|
||||
strokeWidth="0.5"
|
||||
fill="#FFFFFF"
|
||||
x="0.25"
|
||||
y="0.25"
|
||||
width="23.5"
|
||||
height="15.5"
|
||||
rx="2"
|
||||
></rect>
|
||||
<path
|
||||
d="M2.78773262,5.91443732 C2.26459089,5.62750595 1.6675389,5.39673777 1,5.23659312 L1.0280005,5.1118821 L3.76497922,5.1118821 C4.13596254,5.12488556 4.43699113,5.23650585 4.53494636,5.63071135 L5.12976697,8.46659052 L5.31198338,9.32072617 L6.97796639,5.1118821 L8.77678896,5.1118821 L6.10288111,11.2775284 L4.30396552,11.2775284 L2.78773262,5.91443732 L2.78773262,5.91443732 Z M10.0999752,11.2840738 L8.39882877,11.2840738 L9.46284763,5.1118821 L11.163901,5.1118821 L10.0999752,11.2840738 Z M16.2667821,5.26277458 L16.0354292,6.59558538 L15.881566,6.53004446 C15.5737466,6.40524617 15.1674138,6.28053516 14.6143808,6.29371316 C13.942741,6.29371316 13.6415263,6.56277129 13.6345494,6.82545859 C13.6345494,7.11441463 13.998928,7.3048411 14.5939153,7.58725177 C15.5740257,8.02718756 16.0286384,8.56556562 16.0218476,9.26818871 C16.0080799,10.5486366 14.8460128,11.376058 13.0610509,11.376058 C12.2978746,11.3694253 11.5627918,11.2180965 11.163808,11.0475679 L11.4018587,9.66204513 L11.6258627,9.76066195 C12.1788958,9.99070971 12.5428092,10.0889775 13.221984,10.0889775 C13.7117601,10.0889775 14.2368857,9.89837643 14.2435835,9.48488392 C14.2435835,9.21565125 14.0198586,9.01850486 13.3617074,8.7164581 C12.717789,8.42086943 11.8568435,7.92848346 11.8707973,7.04197926 C11.8780532,5.84042483 13.0610509,5 14.7409877,5 C15.3990458,5 15.9312413,5.13788902 16.2667821,5.26277458 Z M18.5277524,9.0974856 L19.941731,9.0974856 C19.8717762,8.78889347 19.549631,7.31147374 19.549631,7.31147374 L19.4307452,6.77964104 C19.3467437,7.00942698 19.1998574,7.38373457 19.2069273,7.37055657 C19.2069273,7.37055657 18.6678479,8.74290137 18.5277524,9.0974856 Z M20.6276036,5.1118821 L22,11.2839865 L20.4249023,11.2839865 C20.4249023,11.2839865 20.2707601,10.5748181 20.221922,10.3581228 L18.0377903,10.3581228 C17.9746264,10.5221933 17.6807607,11.2839865 17.6807607,11.2839865 L15.8957988,11.2839865 L18.4226343,5.62399144 C18.5977072,5.22341512 18.9059917,5.1118821 19.3117663,5.1118821 L20.6276036,5.1118821 L20.6276036,5.1118821 Z"
|
||||
id="Shape"
|
||||
fill="#1434CB"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user