mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 13:50:31 +00:00
feat: Uploading company logo
This commit is contained in:
@@ -1,12 +1,17 @@
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
import { GetPdfTemplateTransformer } from './GetPdfTemplateTransformer';
|
||||||
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class GetPdfTemplate {
|
export class GetPdfTemplate {
|
||||||
@Inject()
|
@Inject()
|
||||||
private tenancy: HasTenancyService;
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private transformer: TransformerInjectable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a pdf template by its ID.
|
* Retrieves a pdf template by its ID.
|
||||||
* @param {number} tenantId - The ID of the tenant.
|
* @param {number} tenantId - The ID of the tenant.
|
||||||
@@ -24,6 +29,10 @@ export class GetPdfTemplate {
|
|||||||
.findById(templateId)
|
.findById(templateId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
return template;
|
return this.transformer.transform(
|
||||||
|
tenantId,
|
||||||
|
template,
|
||||||
|
new GetPdfTemplateTransformer()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
import { getTransactionTypeLabel } from '@/utils/transactions-types';
|
||||||
|
|
||||||
|
export class GetPdfTemplateTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Includeded attributes.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return ['createdAtFormatted', 'resourceFormatted', 'attributes'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the creation date of the PDF template.
|
||||||
|
* @param {Object} template
|
||||||
|
* @returns {string} A formatted string representing the creation date of the template.
|
||||||
|
*/
|
||||||
|
protected createdAtFormatted = (template) => {
|
||||||
|
return this.formatDate(template.createdAt);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the creation date of the PDF template.
|
||||||
|
* @param {Object} template -
|
||||||
|
* @returns {string} A formatted string representing the creation date of the template.
|
||||||
|
*/
|
||||||
|
protected resourceFormatted = (template) => {
|
||||||
|
return getTransactionTypeLabel(template.resource);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves transformed brand attributes.
|
||||||
|
* @param {} template
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
protected attributes = (template) => {
|
||||||
|
return this.item(
|
||||||
|
template.attributes,
|
||||||
|
new GetPdfTemplateAttributesTransformer()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetPdfTemplateAttributesTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Includeded attributes.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return ['companyLogoUri'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the company logo uri.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected companyLogoUri(template) {
|
||||||
|
return template.companyLogoKey
|
||||||
|
? `https://bigcapital.sfo3.digitaloceanspaces.com/${template.companyLogoKey}`
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import { FormikHelpers } from 'formik';
|
import { FormikHelpers } from 'formik';
|
||||||
import { BrandingTemplateValues } from './types';
|
import { BrandingTemplateValues } from './types';
|
||||||
import { useUploadAttachments } from '@/hooks/query/attachments';
|
import { useUploadAttachments } from '@/hooks/query/attachments';
|
||||||
|
import { excludePrivateProps } from '@/utils';
|
||||||
|
|
||||||
interface BrandingTemplateFormProps<T> extends ElementCustomizeProps<T> {
|
interface BrandingTemplateFormProps<T> extends ElementCustomizeProps<T> {
|
||||||
resource: string;
|
resource: string;
|
||||||
@@ -41,7 +42,7 @@ export function BrandingTemplateForm<T extends BrandingTemplateValues>({
|
|||||||
|
|
||||||
const initialValues = useBrandingTemplateFormInitialValues<T>(defaultValues);
|
const initialValues = useBrandingTemplateFormInitialValues<T>(defaultValues);
|
||||||
const [isUploading, setIsLoading] = useState<boolean>(false);
|
const [isUploading, setIsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
// Uploads the attachments.
|
// Uploads the attachments.
|
||||||
const { mutateAsync: uploadAttachments } = useUploadAttachments({
|
const { mutateAsync: uploadAttachments } = useUploadAttachments({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -53,38 +54,53 @@ export function BrandingTemplateForm<T extends BrandingTemplateValues>({
|
|||||||
// - Push the updated data.
|
// - Push the updated data.
|
||||||
const handleFormSubmit = async (
|
const handleFormSubmit = async (
|
||||||
values: T,
|
values: T,
|
||||||
{ setSubmitting }: FormikHelpers<T>,
|
{ setSubmitting, setFieldValue }: FormikHelpers<T>,
|
||||||
) => {
|
) => {
|
||||||
|
const _values = { ...values };
|
||||||
|
|
||||||
|
// Handle create/edit request success.
|
||||||
const handleSuccess = (message: string) => {
|
const handleSuccess = (message: string) => {
|
||||||
AppToaster.show({ intent: Intent.SUCCESS, message });
|
AppToaster.show({ intent: Intent.SUCCESS, message });
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
onSuccess && onSuccess();
|
onSuccess && onSuccess();
|
||||||
};
|
};
|
||||||
|
// Handle create/edit request error.
|
||||||
const handleError = (message: string) => {
|
const handleError = (message: string) => {
|
||||||
AppToaster.show({ intent: Intent.DANGER, message });
|
AppToaster.show({ intent: Intent.DANGER, message });
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
onError && onError();
|
onError && onError();
|
||||||
};
|
};
|
||||||
|
// Start upload the company logo file if it is presented.
|
||||||
if (values.companyLogoFile) {
|
if (values._companyLogoFile) {
|
||||||
isUploading(true);
|
setIsLoading(true);
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
const key = Date.now().toString();
|
const key = Date.now().toString();
|
||||||
|
|
||||||
formData.append('file', values.companyLogoFile);
|
formData.append('file', values._companyLogoFile);
|
||||||
formData.append('internalKey', key);
|
formData.append('internalKey', key);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await uploadAttachments(formData);
|
const uploadedAttachmentRes = await uploadAttachments(formData);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
||||||
|
// Adds the attachment key to the values after finishing upload.
|
||||||
|
_values['companyLogoKey'] = uploadedAttachmentRes?.key;
|
||||||
} catch {
|
} catch {
|
||||||
handleError('An error occurred while uploading company logo.');
|
handleError('An error occurred while uploading company logo.');
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Exclude all the private props that starts with _.
|
||||||
|
const excludedPrivateValues = excludePrivateProps(_values);
|
||||||
|
|
||||||
|
// Transform the the form values to request based on the mode (new or edit mode).
|
||||||
|
const reqValues = templateId
|
||||||
|
? transformToEditRequest(excludedPrivateValues, initialValues)
|
||||||
|
: transformToNewRequest(excludedPrivateValues, initialValues, resource);
|
||||||
|
|
||||||
|
// Template id is presented means edit mode.
|
||||||
if (templateId) {
|
if (templateId) {
|
||||||
const reqValues = transformToEditRequest(values);
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -94,7 +110,6 @@ export function BrandingTemplateForm<T extends BrandingTemplateValues>({
|
|||||||
handleError('An error occurred while updating the PDF template.');
|
handleError('An error occurred while updating the PDF template.');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const reqValues = transformToNewRequest(values, resource);
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -119,3 +134,8 @@ export function BrandingTemplateForm<T extends BrandingTemplateValues>({
|
|||||||
export const validationSchema = Yup.object().shape({
|
export const validationSchema = Yup.object().shape({
|
||||||
templateName: Yup.string().required('Template Name is required'),
|
templateName: Yup.string().required('Template Name is required'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Initial values - companyLogoKey, companyLogoUri
|
||||||
|
// Form - _companyLogoFile, companyLogoKey, companyLogoUri
|
||||||
|
// Request - companyLogoKey
|
||||||
|
|||||||
@@ -7,26 +7,35 @@ import {
|
|||||||
import { useBrandingTemplateBoot } from './BrandingTemplateBoot';
|
import { useBrandingTemplateBoot } from './BrandingTemplateBoot';
|
||||||
import { transformToForm } from '@/utils';
|
import { transformToForm } from '@/utils';
|
||||||
import { BrandingTemplateValues } from './types';
|
import { BrandingTemplateValues } from './types';
|
||||||
import { useFormikContext } from 'formik';
|
|
||||||
import { DRAWERS } from '@/constants/drawers';
|
import { DRAWERS } from '@/constants/drawers';
|
||||||
|
|
||||||
|
const commonExcludedAttrs = ['templateName', 'companyLogoUri'];
|
||||||
|
|
||||||
export const transformToEditRequest = <T extends BrandingTemplateValues>(
|
export const transformToEditRequest = <T extends BrandingTemplateValues>(
|
||||||
values: T,
|
values: T,
|
||||||
|
defaultValues: T,
|
||||||
): EditPdfTemplateValues => {
|
): EditPdfTemplateValues => {
|
||||||
return {
|
return {
|
||||||
templateName: values.templateName,
|
templateName: values.templateName,
|
||||||
attributes: omit(values, ['templateName']),
|
attributes: transformToForm(
|
||||||
|
omit(values, commonExcludedAttrs),
|
||||||
|
defaultValues,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const transformToNewRequest = <T extends BrandingTemplateValues>(
|
export const transformToNewRequest = <T extends BrandingTemplateValues>(
|
||||||
values: T,
|
values: T,
|
||||||
|
defaultValues: T,
|
||||||
resource: string,
|
resource: string,
|
||||||
): CreatePdfTemplateValues => {
|
): CreatePdfTemplateValues => {
|
||||||
return {
|
return {
|
||||||
resource,
|
resource,
|
||||||
templateName: values.templateName,
|
templateName: values.templateName,
|
||||||
attributes: omit(values, ['templateName']),
|
attributes: transformToForm(
|
||||||
|
omit(values, commonExcludedAttrs),
|
||||||
|
defaultValues,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,5 +75,5 @@ export const getButtonLabelFromResource = (resource: string) => {
|
|||||||
CreditNote: 'Create Credit Note Branding',
|
CreditNote: 'Create Credit Note Branding',
|
||||||
PaymentReceive: 'Create Payment Branding',
|
PaymentReceive: 'Create Payment Branding',
|
||||||
};
|
};
|
||||||
return R.prop(resource, pairs) || 'Create Branding Template';
|
return R.prop(resource, pairs) || 'Create Branding Template';
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,4 +2,7 @@
|
|||||||
|
|
||||||
export interface BrandingTemplateValues {
|
export interface BrandingTemplateValues {
|
||||||
templateName: string;
|
templateName: string;
|
||||||
|
|
||||||
|
companyLogoKey?: string;
|
||||||
|
companyLogoUri?: string;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Group } from '@/components';
|
import { Group } from '@/components';
|
||||||
import { ElementCustomizeProvider } from './ElementCustomizeProvider';
|
import { ElementCustomizeProvider } from './ElementCustomizeProvider';
|
||||||
import {
|
import {
|
||||||
ElementCustomizeForm,
|
ElementCustomizeForm,
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { FFormGroup } from '@/components';
|
||||||
|
import { CompanyLogoUpload } from './CompanyLogoUpload';
|
||||||
|
|
||||||
|
export function BrandingCompanyLogoUploadField() {
|
||||||
|
const { setFieldValue, values } = useFormikContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FFormGroup name={'companyLogo'} label={''} fastField>
|
||||||
|
<CompanyLogoUpload
|
||||||
|
initialPreview={values.companyLogoUri}
|
||||||
|
onChange={(file) => {
|
||||||
|
const imageUrl = file ? URL.createObjectURL(file) : '';
|
||||||
|
|
||||||
|
setFieldValue('_companyLogoFile', file);
|
||||||
|
setFieldValue('companyLogoUri', imageUrl);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
.root {
|
||||||
|
min-height: 120px;
|
||||||
|
height: 120px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid;
|
||||||
|
display: flex;
|
||||||
|
border: 1px solid #E1E1E1;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover .removeButton{
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.removeButton{
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
top: 5px;
|
||||||
|
border-radius: 24px;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentPrePreview {
|
||||||
|
color: #738091;
|
||||||
|
font-size: 13px;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzoneContent{
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewImage{
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { Button, Intent } from '@blueprintjs/core';
|
||||||
|
import { Icon, Stack } from '@/components';
|
||||||
|
import { Dropzone, DropzoneProps } from '@/components/Dropzone';
|
||||||
|
import { MIME_TYPES } from '@/components/Dropzone/mine-types';
|
||||||
|
import { useUncontrolled } from '@/hooks/useUncontrolled';
|
||||||
|
import styles from './CompanyLogoUpload.module.scss';
|
||||||
|
|
||||||
|
export interface CompanyLogoUploadProps {
|
||||||
|
/** Initial preview uri. */
|
||||||
|
initialPreview?: string;
|
||||||
|
|
||||||
|
/** The initial file object for uploading */
|
||||||
|
initialValue?: File;
|
||||||
|
|
||||||
|
/** The current file object for uploading */
|
||||||
|
value?: File;
|
||||||
|
|
||||||
|
/** Function called when the file is changed */
|
||||||
|
onChange?: (file: File) => void;
|
||||||
|
|
||||||
|
/** Props for the Dropzone component */
|
||||||
|
dropzoneProps?: DropzoneProps;
|
||||||
|
|
||||||
|
/** Icon element for the upload button */
|
||||||
|
uploadIcon?: JSX.Element;
|
||||||
|
|
||||||
|
/** Title displayed in the component */
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
/** Custom CSS class names for styling */
|
||||||
|
classNames?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CompanyLogoUpload({
|
||||||
|
initialPreview,
|
||||||
|
initialValue,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
dropzoneProps,
|
||||||
|
uploadIcon = <Icon icon="download" iconSize={26} />,
|
||||||
|
title = 'Drag images here or click to select files',
|
||||||
|
classNames,
|
||||||
|
}: CompanyLogoUploadProps) {
|
||||||
|
const [localValue, handleChange] = useUncontrolled<File | null>({
|
||||||
|
value,
|
||||||
|
initialValue,
|
||||||
|
finalValue: null,
|
||||||
|
onChange,
|
||||||
|
});
|
||||||
|
const [initialLocalPreview, setInitialLocalPreview] = useState<string | null>(
|
||||||
|
initialPreview || null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const openRef = useRef<() => void>(null);
|
||||||
|
|
||||||
|
const handleRemove = () => {
|
||||||
|
handleChange(null);
|
||||||
|
setInitialLocalPreview(null);
|
||||||
|
};
|
||||||
|
const imagePreviewUrl = localValue
|
||||||
|
? URL.createObjectURL(localValue)
|
||||||
|
: initialLocalPreview || '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropzone
|
||||||
|
onDrop={(files) => handleChange(files[0])}
|
||||||
|
onReject={(files) => console.log('rejected files', files)}
|
||||||
|
maxSize={5 * 1024 ** 2}
|
||||||
|
accept={[MIME_TYPES.png, MIME_TYPES.jpeg]}
|
||||||
|
classNames={{ root: styles?.root, content: styles.dropzoneContent }}
|
||||||
|
activateOnClick={false}
|
||||||
|
openRef={openRef}
|
||||||
|
{...dropzoneProps}
|
||||||
|
>
|
||||||
|
{imagePreviewUrl ? (
|
||||||
|
<span>
|
||||||
|
<img src={imagePreviewUrl} alt="" className={styles.previewImage} />
|
||||||
|
<Button
|
||||||
|
minimal
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
onClick={handleRemove}
|
||||||
|
icon={<Icon icon={'smallCross'} iconSize={16} />}
|
||||||
|
className={styles?.removeButton}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<Stack spacing={10} align="center" className={styles.contentPrePreview}>
|
||||||
|
{title && <span className={styles.title}>{title}</span>}
|
||||||
|
<Button
|
||||||
|
intent="none"
|
||||||
|
onClick={() => openRef.current?.()}
|
||||||
|
style={{ pointerEvents: 'all' }}
|
||||||
|
minimal
|
||||||
|
outlined
|
||||||
|
small
|
||||||
|
>
|
||||||
|
{'Upload File'}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Dropzone>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { FColorInput } from '@/components/Forms/FColorInput';
|
import { FColorInput } from '@/components/Forms/FColorInput';
|
||||||
import { Overlay } from '../../Invoices/InvoiceCustomize/Overlay';
|
import { Overlay } from '../../Invoices/InvoiceCustomize/Overlay';
|
||||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||||
|
import { BrandingCompanyLogoUploadField } from '@/containers/ElementCustomize/components/BrandingCompanyLogoUploadField';
|
||||||
|
|
||||||
export function CreditNoteCustomizeGeneralField() {
|
export function CreditNoteCustomizeGeneralField() {
|
||||||
const isTemplateNameFilled = useIsTemplateNamedFilled();
|
const isTemplateNameFilled = useIsTemplateNamedFilled();
|
||||||
@@ -64,15 +65,23 @@ export function CreditNoteCustomizeGeneralField() {
|
|||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
<FFormGroup name={'showCompanyLogo'} label={'Logo'} fastField>
|
<Stack spacing={10}>
|
||||||
<FSwitch
|
<FFormGroup
|
||||||
name={'showCompanyLogo'}
|
name={'showCompanyLogo'}
|
||||||
label={'Display company logo in the paper'}
|
label={'Logo'}
|
||||||
style={{ fontSize: 14 }}
|
|
||||||
large
|
|
||||||
fastField
|
fastField
|
||||||
/>
|
style={{ marginBottom: 0 }}
|
||||||
</FFormGroup>
|
>
|
||||||
|
<FSwitch
|
||||||
|
name={'showCompanyLogo'}
|
||||||
|
label={'Display company logo in the paper'}
|
||||||
|
style={{ fontSize: 14 }}
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
<BrandingCompanyLogoUploadField />
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { Classes, Text } from '@blueprintjs/core';
|
import { Classes } from '@blueprintjs/core';
|
||||||
import {
|
import {
|
||||||
FFormGroup,
|
FFormGroup,
|
||||||
FInputGroup,
|
FInputGroup,
|
||||||
FSwitch,
|
FSwitch,
|
||||||
FieldRequiredHint,
|
FieldRequiredHint,
|
||||||
Group,
|
|
||||||
Stack,
|
Stack,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { FColorInput } from '@/components/Forms/FColorInput';
|
import { FColorInput } from '@/components/Forms/FColorInput';
|
||||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||||
import { Overlay } from '../../Invoices/InvoiceCustomize/Overlay';
|
import { Overlay } from '../../Invoices/InvoiceCustomize/Overlay';
|
||||||
|
import { BrandingCompanyLogoUploadField } from '@/containers/ElementCustomize/components/BrandingCompanyLogoUploadField';
|
||||||
|
|
||||||
export function EstimateCustomizeGeneralField() {
|
export function EstimateCustomizeGeneralField() {
|
||||||
const isTemplateNameFilled = useIsTemplateNamedFilled();
|
const isTemplateNameFilled = useIsTemplateNamedFilled();
|
||||||
@@ -65,15 +65,24 @@ export function EstimateCustomizeGeneralField() {
|
|||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
<FFormGroup name={'showCompanyLogo'} label={'Logo'} fastField>
|
<Stack spacing={10}>
|
||||||
<FSwitch
|
<FFormGroup
|
||||||
name={'showCompanyLogo'}
|
name={'showCompanyLogo'}
|
||||||
label={'Display company logo in the paper'}
|
label={'Logo'}
|
||||||
style={{ fontSize: 14 }}
|
|
||||||
large
|
|
||||||
fastField
|
fastField
|
||||||
/>
|
style={{ marginBottom: 0 }}
|
||||||
</FFormGroup>
|
>
|
||||||
|
<FSwitch
|
||||||
|
name={'showCompanyLogo'}
|
||||||
|
label={'Display company logo in the paper'}
|
||||||
|
style={{ fontSize: 14 }}
|
||||||
|
large
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
<BrandingCompanyLogoUploadField />
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import { useRef, useState } from 'react';
|
|
||||||
import { Button, Intent } from '@blueprintjs/core';
|
|
||||||
import clsx from 'classnames';
|
|
||||||
import { Box, Icon, Stack } from '@/components';
|
|
||||||
import { Dropzone, DropzoneProps } from '@/components/Dropzone';
|
|
||||||
import { MIME_TYPES } from '@/components/Dropzone/mine-types';
|
|
||||||
import { useUncontrolled } from '@/hooks/useUncontrolled';
|
|
||||||
import styles from './CompanyLogoUpload.module.scss';
|
|
||||||
|
|
||||||
export interface CompanyLogoUploadProps {
|
|
||||||
initialValue?: File;
|
|
||||||
value?: File;
|
|
||||||
onChange?: (file: File) => void;
|
|
||||||
dropzoneProps?: DropzoneProps;
|
|
||||||
uploadIcon?: JSX.Element;
|
|
||||||
title?: string;
|
|
||||||
classNames?: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CompanyLogoUpload({
|
|
||||||
initialValue,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
dropzoneProps,
|
|
||||||
uploadIcon = <Icon icon="download" iconSize={26} />,
|
|
||||||
title = 'Drag images here or click to select files',
|
|
||||||
classNames,
|
|
||||||
}: CompanyLogoUploadProps) {
|
|
||||||
const [localValue, handleChange] = useUncontrolled({
|
|
||||||
value,
|
|
||||||
initialValue,
|
|
||||||
finalValue: null,
|
|
||||||
onChange,
|
|
||||||
});
|
|
||||||
const openRef = useRef<() => void>(null);
|
|
||||||
|
|
||||||
const handleRemove = () => {
|
|
||||||
handleChange(null);
|
|
||||||
};
|
|
||||||
const imagePreviewUrl = localValue ? URL.createObjectURL(localValue) : '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dropzone
|
|
||||||
onDrop={(files) => handleChange(files[0])}
|
|
||||||
onReject={(files) => console.log('rejected files', files)}
|
|
||||||
maxSize={5 * 1024 ** 2}
|
|
||||||
accept={[MIME_TYPES.png, MIME_TYPES.jpeg]}
|
|
||||||
classNames={{ root: styles?.root, content: styles.dropzoneContent }}
|
|
||||||
activateOnClick={false}
|
|
||||||
openRef={openRef}
|
|
||||||
{...dropzoneProps}
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
spacing={12}
|
|
||||||
align="center"
|
|
||||||
className={clsx(styles.content, classNames?.content)}
|
|
||||||
>
|
|
||||||
{localValue ? (
|
|
||||||
<Stack spacing={10} justify="center" align="center">
|
|
||||||
<img src={imagePreviewUrl} alt="" className={styles.previewImage} />
|
|
||||||
<Button
|
|
||||||
minimal
|
|
||||||
intent={Intent.DANGER}
|
|
||||||
onClick={handleRemove}
|
|
||||||
icon={<Icon icon={'smallCross'} iconSize={16} />}
|
|
||||||
className={styles?.removeButton}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
) : (
|
|
||||||
<Stack spacing={10} align="center">
|
|
||||||
{title && <span className={styles.title}>{title}</span>}
|
|
||||||
<Button
|
|
||||||
intent="none"
|
|
||||||
onClick={() => openRef.current?.()}
|
|
||||||
style={{ pointerEvents: 'all' }}
|
|
||||||
minimal
|
|
||||||
outlined
|
|
||||||
small
|
|
||||||
>
|
|
||||||
{'Upload File'}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Dropzone>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -12,8 +12,7 @@ import { FColorInput } from '@/components/Forms/FColorInput';
|
|||||||
import { CreditCardIcon } from '@/icons/CreditCardIcon';
|
import { CreditCardIcon } from '@/icons/CreditCardIcon';
|
||||||
import { Overlay } from './Overlay';
|
import { Overlay } from './Overlay';
|
||||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||||
import { CompanyLogoUpload } from './CompanyLogoUpload';
|
import { BrandingCompanyLogoUploadField } from '@/containers/ElementCustomize/components/BrandingCompanyLogoUploadField';
|
||||||
import { useFormikContext } from 'formik';
|
|
||||||
|
|
||||||
export function InvoiceCustomizeGeneralField() {
|
export function InvoiceCustomizeGeneralField() {
|
||||||
const isTemplateNameFilled = useIsTemplateNamedFilled();
|
const isTemplateNameFilled = useIsTemplateNamedFilled();
|
||||||
@@ -68,17 +67,23 @@ export function InvoiceCustomizeGeneralField() {
|
|||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
<FFormGroup name={'showCompanyLogo'} label={'Logo'} fastField>
|
<Stack spacing={10}>
|
||||||
<FSwitch
|
<FFormGroup
|
||||||
name={'showCompanyLogo'}
|
name={'showCompanyLogo'}
|
||||||
label={'Display company logo in the paper'}
|
label={'Logo'}
|
||||||
style={{ fontSize: 14 }}
|
|
||||||
large
|
|
||||||
fastField
|
fastField
|
||||||
/>
|
style={{ marginBottom: 0 }}
|
||||||
</FFormGroup>
|
>
|
||||||
|
<FSwitch
|
||||||
|
name={'showCompanyLogo'}
|
||||||
|
label={'Display company logo in the paper'}
|
||||||
|
style={{ fontSize: 14 }}
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
<CompanyLogoUploadField />
|
<BrandingCompanyLogoUploadField />
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<InvoiceCustomizePaymentManage />
|
<InvoiceCustomizePaymentManage />
|
||||||
@@ -103,24 +108,9 @@ function InvoiceCustomizePaymentManage() {
|
|||||||
<Text>Accept payment methods</Text>
|
<Text>Accept payment methods</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<a style={{ fontSize: 13 }} href={'#'}>Manage</a>
|
<a style={{ fontSize: 13 }} href={'#'}>
|
||||||
|
Manage
|
||||||
|
</a>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CompanyLogoUploadField() {
|
|
||||||
const { setFieldValue } = useFormikContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FFormGroup name={'companyLogo'} label={''} fastField>
|
|
||||||
<CompanyLogoUpload
|
|
||||||
onChange={(file) => {
|
|
||||||
const imageUrl = file ? URL.createObjectURL(file) : '';
|
|
||||||
|
|
||||||
setFieldValue('companyLogoFile', file);
|
|
||||||
setFieldValue('companyLogo', imageUrl);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FFormGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export interface InvoicePaperTemplateProps {
|
|||||||
secondaryColor?: string;
|
secondaryColor?: string;
|
||||||
|
|
||||||
showCompanyLogo?: boolean;
|
showCompanyLogo?: boolean;
|
||||||
companyLogo?: string;
|
companyLogoUri?: string;
|
||||||
|
|
||||||
showInvoiceNumber?: boolean;
|
showInvoiceNumber?: boolean;
|
||||||
invoiceNumber?: string;
|
invoiceNumber?: string;
|
||||||
@@ -95,7 +95,7 @@ export function InvoicePaperTemplate({
|
|||||||
companyName = 'Bigcapital Technology, Inc.',
|
companyName = 'Bigcapital Technology, Inc.',
|
||||||
|
|
||||||
showCompanyLogo = true,
|
showCompanyLogo = true,
|
||||||
companyLogo,
|
companyLogoUri,
|
||||||
|
|
||||||
dueDate = 'September 3, 2024',
|
dueDate = 'September 3, 2024',
|
||||||
dueDateLabel = 'Date due',
|
dueDateLabel = 'Date due',
|
||||||
@@ -185,7 +185,7 @@ export function InvoicePaperTemplate({
|
|||||||
primaryColor={primaryColor}
|
primaryColor={primaryColor}
|
||||||
secondaryColor={secondaryColor}
|
secondaryColor={secondaryColor}
|
||||||
showCompanyLogo={showCompanyLogo}
|
showCompanyLogo={showCompanyLogo}
|
||||||
companyLogo={companyLogo}
|
companyLogo={companyLogoUri}
|
||||||
bigtitle={'Invoice'}
|
bigtitle={'Invoice'}
|
||||||
>
|
>
|
||||||
<Stack spacing={24}>
|
<Stack spacing={24}>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ export const initialValues = {
|
|||||||
|
|
||||||
// Company logo.
|
// Company logo.
|
||||||
showCompanyLogo: true,
|
showCompanyLogo: true,
|
||||||
companyLogo:
|
companyLogoKey: '',
|
||||||
'https://cdn-development.mercury.com/demo-assets/avatars/mercury-demo-dark.png',
|
companyLogoUri: '',
|
||||||
|
|
||||||
// Top details.
|
// Top details.
|
||||||
showInvoiceNumber: true,
|
showInvoiceNumber: true,
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ export interface InvoiceCustomizeValues extends BrandingTemplateValues {
|
|||||||
|
|
||||||
// Company Logo
|
// Company Logo
|
||||||
showCompanyLogo?: boolean;
|
showCompanyLogo?: boolean;
|
||||||
companyLogo?: string;
|
companyLogoKey?: string;
|
||||||
|
companyLogoUri?: string;
|
||||||
|
|
||||||
// Top details.
|
// Top details.
|
||||||
showInvoiceNumber?: boolean;
|
showInvoiceNumber?: boolean;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { FColorInput } from '@/components/Forms/FColorInput';
|
import { FColorInput } from '@/components/Forms/FColorInput';
|
||||||
import { Overlay } from '../../Invoices/InvoiceCustomize/Overlay';
|
import { Overlay } from '../../Invoices/InvoiceCustomize/Overlay';
|
||||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||||
|
import { BrandingCompanyLogoUploadField } from '@/containers/ElementCustomize/components/BrandingCompanyLogoUploadField';
|
||||||
|
|
||||||
export function PaymentReceivedCustomizeGeneralField() {
|
export function PaymentReceivedCustomizeGeneralField() {
|
||||||
const isTemplateNameFilled = useIsTemplateNamedFilled();
|
const isTemplateNameFilled = useIsTemplateNamedFilled();
|
||||||
@@ -64,15 +65,24 @@ export function PaymentReceivedCustomizeGeneralField() {
|
|||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
<FFormGroup name={'showCompanyLogo'} label={'Logo'} fastField>
|
<Stack spacing={10}>
|
||||||
<FSwitch
|
<FFormGroup
|
||||||
name={'showCompanyLogo'}
|
name={'showCompanyLogo'}
|
||||||
label={'Display company logo in the paper'}
|
label={'Logo'}
|
||||||
style={{ fontSize: 14 }}
|
|
||||||
large
|
|
||||||
fastField
|
fastField
|
||||||
/>
|
style={{ marginBottom: 0 }}
|
||||||
</FFormGroup>
|
>
|
||||||
|
<FSwitch
|
||||||
|
name={'showCompanyLogo'}
|
||||||
|
label={'Display company logo in the paper'}
|
||||||
|
style={{ fontSize: 14 }}
|
||||||
|
large
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
<BrandingCompanyLogoUploadField />
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { FColorInput } from '@/components/Forms/FColorInput';
|
import { FColorInput } from '@/components/Forms/FColorInput';
|
||||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||||
import { Overlay } from '../../Invoices/InvoiceCustomize/Overlay';
|
import { Overlay } from '../../Invoices/InvoiceCustomize/Overlay';
|
||||||
|
import { BrandingCompanyLogoUploadField } from '@/containers/ElementCustomize/components/BrandingCompanyLogoUploadField';
|
||||||
|
|
||||||
export function ReceiptCustomizeGeneralField() {
|
export function ReceiptCustomizeGeneralField() {
|
||||||
const isTemplateNameFilled = useIsTemplateNamedFilled();
|
const isTemplateNameFilled = useIsTemplateNamedFilled();
|
||||||
@@ -64,15 +65,23 @@ export function ReceiptCustomizeGeneralField() {
|
|||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
<FFormGroup name={'showCompanyLogo'} label={'Logo'} fastField>
|
<Stack spacing={10}>
|
||||||
<FSwitch
|
<FFormGroup
|
||||||
name={'showCompanyLogo'}
|
name={'showCompanyLogo'}
|
||||||
label={'Display company logo in the paper'}
|
label={'Logo'}
|
||||||
style={{ fontSize: 14 }}
|
|
||||||
large
|
|
||||||
fastField
|
fastField
|
||||||
/>
|
style={{ marginBottom: 0 }}
|
||||||
</FFormGroup>
|
>
|
||||||
|
<FSwitch
|
||||||
|
name={'showCompanyLogo'}
|
||||||
|
label={'Display company logo in the paper'}
|
||||||
|
style={{ fontSize: 14 }}
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
<BrandingCompanyLogoUploadField />
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { useMutation } from 'react-query';
|
import { useMutation } from 'react-query';
|
||||||
import useApiRequest from '../useRequest';
|
import useApiRequest from '../useRequest';
|
||||||
|
import { transformToCamelCase } from '@/utils';
|
||||||
|
|
||||||
|
interface UploadAttachmentResponse {
|
||||||
|
createdAt: string;
|
||||||
|
id: number;
|
||||||
|
key: string;
|
||||||
|
mimeType: string;
|
||||||
|
originName: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads the given attachments.
|
* Uploads the given attachments.
|
||||||
@@ -8,8 +18,11 @@ import useApiRequest from '../useRequest';
|
|||||||
export function useUploadAttachments(props) {
|
export function useUploadAttachments(props) {
|
||||||
const apiRequest = useApiRequest();
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
return useMutation(
|
return useMutation<UploadAttachmentResponse>(
|
||||||
(values) => apiRequest.post('attachments', values),
|
(values) =>
|
||||||
|
apiRequest
|
||||||
|
.post('attachments', values)
|
||||||
|
.then((res) => transformToCamelCase(res.data?.data)),
|
||||||
props,
|
props,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import jsCookie from 'js-cookie';
|
|||||||
import { deepMapKeys } from './map-key-deep';
|
import { deepMapKeys } from './map-key-deep';
|
||||||
export * from './deep';
|
export * from './deep';
|
||||||
|
|
||||||
|
|
||||||
export const getCookie = (name, defaultValue) =>
|
export const getCookie = (name, defaultValue) =>
|
||||||
_.defaultTo(jsCookie.get(name), defaultValue);
|
_.defaultTo(jsCookie.get(name), defaultValue);
|
||||||
|
|
||||||
@@ -352,6 +351,14 @@ export const transformToForm = (obj, emptyInitialValues) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function excludePrivateProps(
|
||||||
|
obj: Record<string, any>,
|
||||||
|
): Record<string, any> {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(obj).filter(([key, value]) => !key.startsWith('_')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function inputIntent({ error, touched }) {
|
export function inputIntent({ error, touched }) {
|
||||||
return error && touched ? Intent.DANGER : '';
|
return error && touched ? Intent.DANGER : '';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user