From eb5fdbf4ee48ae71efade22a42179f1fdb3eb086 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 22 Sep 2024 21:23:02 +0200 Subject: [PATCH] feat: Control the payment method from invoice form --- .../api/controllers/Sales/SalesInvoices.ts | 4 +- .../GetPaymentServicesSpecificInvoice.ts | 2 +- ...ymentServicesSpecificInvoiceTransformer.ts | 8 ++ .../PaymentMethods/PaymentMethodSelect.tsx | 84 +++++++++++++++++++ .../SelectPaymentMethodPopover.tsx | 52 ++++++++++++ .../InvoiceForm/InvoiceFormFooterLeft.tsx | 77 +++++++++++++---- .../InvoiceForm/InvoiceFormProvider.tsx | 4 + .../Sales/Invoices/InvoiceForm/utils.tsx | 11 +++ .../src/hooks/query/payment-services.ts | 2 +- packages/webapp/src/icons/Mastercard.tsx | 24 ++++++ packages/webapp/src/icons/Visa.tsx | 47 +++++++++++ 11 files changed, 295 insertions(+), 20 deletions(-) create mode 100644 packages/webapp/src/containers/PaymentMethods/PaymentMethodSelect.tsx create mode 100644 packages/webapp/src/containers/PaymentMethods/SelectPaymentMethodPopover.tsx create mode 100644 packages/webapp/src/icons/Mastercard.tsx create mode 100644 packages/webapp/src/icons/Visa.tsx diff --git a/packages/server/src/api/controllers/Sales/SalesInvoices.ts b/packages/server/src/api/controllers/Sales/SalesInvoices.ts index 429879837..2cc2fbfa0 100644 --- a/packages/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/packages/server/src/api/controllers/Sales/SalesInvoices.ts @@ -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(), ]; } diff --git a/packages/server/src/services/PaymentServices/GetPaymentServicesSpecificInvoice.ts b/packages/server/src/services/PaymentServices/GetPaymentServicesSpecificInvoice.ts index 6358914a4..447f95943 100644 --- a/packages/server/src/services/PaymentServices/GetPaymentServicesSpecificInvoice.ts +++ b/packages/server/src/services/PaymentServices/GetPaymentServicesSpecificInvoice.ts @@ -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( diff --git a/packages/server/src/services/PaymentServices/GetPaymentServicesSpecificInvoiceTransformer.ts b/packages/server/src/services/PaymentServices/GetPaymentServicesSpecificInvoiceTransformer.ts index 2b3ea4318..9ef5ca61f 100644 --- a/packages/server/src/services/PaymentServices/GetPaymentServicesSpecificInvoiceTransformer.ts +++ b/packages/server/src/services/PaymentServices/GetPaymentServicesSpecificInvoiceTransformer.ts @@ -8,4 +8,12 @@ export class GetPaymentServicesSpecificInvoiceTransformer extends Transformer { public excludeAttributes = (): string[] => { return ['accountId']; }; + + public includeAttributes = (): string[] => { + return ['serviceFormatted']; + }; + + public serviceFormatted(method) { + return 'Stripe'; + } } diff --git a/packages/webapp/src/containers/PaymentMethods/PaymentMethodSelect.tsx b/packages/webapp/src/containers/PaymentMethods/PaymentMethodSelect.tsx new file mode 100644 index 000000000..75a7172b9 --- /dev/null +++ b/packages/webapp/src/containers/PaymentMethods/PaymentMethodSelect.tsx @@ -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({ + value, + initialValue, + finalValue: false, + onChange, + }); + const handleClick = () => { + handleChange(!_value); + }; + + return ( + + + {label} + + ); +} + +export interface PaymentMethodSelectFieldProps + extends Partial { + 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 ( + + ); +} + +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; +`; diff --git a/packages/webapp/src/containers/PaymentMethods/SelectPaymentMethodPopover.tsx b/packages/webapp/src/containers/PaymentMethods/SelectPaymentMethodPopover.tsx new file mode 100644 index 000000000..d89277c04 --- /dev/null +++ b/packages/webapp/src/containers/PaymentMethods/SelectPaymentMethodPopover.tsx @@ -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; + children: React.ReactNode; +} +export function PaymentOptionsButtonPopver({ + paymentMethods, + children, +}: PaymentOptionsButtonPopverProps) { + return ( + + Payment Options + + + {paymentMethods?.map((service, key) => ( + + ))} + + + } + > + {children} + + ); +} + +const PaymentMethodsTitle = styled('h6')` + font-size: 12px; + font-weight: 500; + margin: 0; + color: rgb(95, 107, 124); +`; diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormFooterLeft.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormFooterLeft.tsx index 61e7c2291..2757a2819 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormFooterLeft.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormFooterLeft.tsx @@ -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 ( - - - Payment Options - - + {/* --------- Invoice message --------- */} - + + {/* --------- Payment Options --------- */} + + + Select an online payment option to get paid faster{' '} + + + + + + + Payment Options + + + + + ); } 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; + } +`; diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormProvider.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormProvider.tsx index 43ddb56aa..172dcda94 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormProvider.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormProvider.tsx @@ -150,6 +150,10 @@ function InvoiceFormProvider({ invoiceId, baseCurrency, ...props }) { editInvoiceMutate, setSubmitPayload, isNewMode, + + // Payment Services + paymentServices, + isPaymentServicesLoading, }; return ( diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx index 659b0d5f3..c741575ad 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx @@ -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, +): 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(); diff --git a/packages/webapp/src/hooks/query/payment-services.ts b/packages/webapp/src/hooks/query/payment-services.ts index 5130a2e65..8c2b3d27f 100644 --- a/packages/webapp/src/hooks/query/payment-services.ts +++ b/packages/webapp/src/hooks/query/payment-services.ts @@ -31,7 +31,7 @@ export const useGetPaymentServices = ( .then( (response) => transformToCamelCase( - response.data?.paymentServices, + response.data?.payment_services, ) as GetPaymentServicesResponse, ), { diff --git a/packages/webapp/src/icons/Mastercard.tsx b/packages/webapp/src/icons/Mastercard.tsx new file mode 100644 index 000000000..fc9df26f2 --- /dev/null +++ b/packages/webapp/src/icons/Mastercard.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +export const MastercardIcon: React.FC> = ( + props, +) => { + return ( + + + + + + + + + ); +}; diff --git a/packages/webapp/src/icons/Visa.tsx b/packages/webapp/src/icons/Visa.tsx new file mode 100644 index 000000000..cb4bdcb46 --- /dev/null +++ b/packages/webapp/src/icons/Visa.tsx @@ -0,0 +1,47 @@ +import React from 'react'; + +export const VisaIcon: React.FC> = (props) => { + return ( + + + + + + + + + + + + + + + ); +};