mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 07:10:33 +00:00
feat: invoice pdf preview dialog.
This commit is contained in:
@@ -68,6 +68,21 @@ const CLASSES = {
|
|||||||
|
|
||||||
FINANCIAL_REPORT_INSIDER: 'dashboard__insider--financial-report',
|
FINANCIAL_REPORT_INSIDER: 'dashboard__insider--financial-report',
|
||||||
|
|
||||||
|
|
||||||
|
UNIVERSAL_SEARCH: 'universal-search',
|
||||||
|
UNIVERSAL_SEARCH_OMNIBAR: 'universal-search__omnibar',
|
||||||
|
UNIVERSAL_SEARCH_OVERLAY: 'universal-search-overlay',
|
||||||
|
UNIVERSAL_SEARCH_INPUT: 'universal-search__input',
|
||||||
|
UNIVERSAL_SEARCH_INPUT_RIGHT_ELEMENTS: 'universal-search-input-right-elements',
|
||||||
|
UNIVERSAL_SEARCH_FOOTER: 'universal-search__footer',
|
||||||
|
|
||||||
|
UNIVERSAL_SEARCH_ACTIONS: 'universal-search__actions',
|
||||||
|
UNIVERSAL_SEARCH_ACTION_SELECT: 'universal-search__action universal-search__action--select',
|
||||||
|
UNIVERSAL_SEARCH_ACTION_CLOSE: 'universal-search__action universal-search__action--close',
|
||||||
|
UNIVERSAL_SEARCH_ACTION_ARROWS: 'universal-search__action universal-search__action--arrows',
|
||||||
|
|
||||||
|
DIALOG_PDF_PREVIEW: 'dialog--pdf-preview-dialog',
|
||||||
|
|
||||||
...Classes,
|
...Classes,
|
||||||
CARD: 'card',
|
CARD: 'card',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import DashboardBreadcrumbs from 'components/Dashboard/DashboardBreadcrumbs';
|
|||||||
import DashboardBackLink from 'components/Dashboard/DashboardBackLink';
|
import DashboardBackLink from 'components/Dashboard/DashboardBackLink';
|
||||||
import { Icon, Hint, If } from 'components';
|
import { Icon, Hint, If } from 'components';
|
||||||
|
|
||||||
import withSearch from 'containers/GeneralSearch/withSearch';
|
import withUniversalSearch from 'containers/UniversalSearch/withUniversalSearch';
|
||||||
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||||
import withDashboard from 'containers/Dashboard/withDashboard';
|
import withDashboard from 'containers/Dashboard/withDashboard';
|
||||||
import withSettings from 'containers/Settings/withSettings';
|
import withSettings from 'containers/Settings/withSettings';
|
||||||
@@ -155,7 +155,7 @@ function DashboardTopbar({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default compose(
|
export default compose(
|
||||||
withSearch,
|
withUniversalSearch,
|
||||||
withDashboard(({ pageTitle, pageHint, editViewId, sidebarExpended }) => ({
|
withDashboard(({ pageTitle, pageHint, editViewId, sidebarExpended }) => ({
|
||||||
pageTitle,
|
pageTitle,
|
||||||
editViewId,
|
editViewId,
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import classNames from 'classnames';
|
|||||||
export default function DialogContent(props) {
|
export default function DialogContent(props) {
|
||||||
const { isLoading, children } = props;
|
const { isLoading, children } = props;
|
||||||
|
|
||||||
const loadingContent = (
|
const loadingContent = <Spinner size={30} />;
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(Classes.DIALOG_BODY, {
|
className={classNames(Classes.DIALOG_BODY, {
|
||||||
'is-loading': isLoading,
|
'is-loading': isLoading,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Spinner size={30} />
|
{isLoading ? loadingContent : children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
return <div>{isLoading ? loadingContent : children}</div>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import ContactDuplicateDialog from 'containers/Dialogs/ContactDuplicateDialog';
|
|||||||
import QuickPaymentReceiveFormDialog from 'containers/Dialogs/QuickPaymentReceiveFormDialog';
|
import QuickPaymentReceiveFormDialog from 'containers/Dialogs/QuickPaymentReceiveFormDialog';
|
||||||
import QuickPaymentMadeFormDialog from 'containers/Dialogs/QuickPaymentMadeFormDialog';
|
import QuickPaymentMadeFormDialog from 'containers/Dialogs/QuickPaymentMadeFormDialog';
|
||||||
import AllocateLandedCostDialog from 'containers/Dialogs/AllocateLandedCostDialog';
|
import AllocateLandedCostDialog from 'containers/Dialogs/AllocateLandedCostDialog';
|
||||||
|
import InvoicePdfPreviewDialog from 'containers/Dialogs/InvoicePdfPreviewDialog';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialogs container.
|
* Dialogs container.
|
||||||
@@ -34,6 +35,7 @@ export default function DialogsContainer() {
|
|||||||
<QuickPaymentReceiveFormDialog dialogName={'quick-payment-receive'} />
|
<QuickPaymentReceiveFormDialog dialogName={'quick-payment-receive'} />
|
||||||
<QuickPaymentMadeFormDialog dialogName={'quick-payment-made'} />
|
<QuickPaymentMadeFormDialog dialogName={'quick-payment-made'} />
|
||||||
<AllocateLandedCostDialog dialogName={'allocate-landed-cost'} />
|
<AllocateLandedCostDialog dialogName={'allocate-landed-cost'} />
|
||||||
|
<InvoicePdfPreviewDialog dialog={'invoice-pdf-preview'} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
13
client/src/components/PdfPreview/index.js
Normal file
13
client/src/components/PdfPreview/index.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Spinner } from '@blueprintjs/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Previews the pdf document of the given object url.
|
||||||
|
*/
|
||||||
|
export function PdfDocumentPreview({ url, height, width, isLoading }) {
|
||||||
|
return isLoading ? (
|
||||||
|
<Spinner size={30} />
|
||||||
|
) : (
|
||||||
|
<embed src={url} height={height} width={width} />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -67,6 +67,8 @@ export * from './AdvancedFilter/AdvancedFilterDropdown';
|
|||||||
export * from './AdvancedFilter/AdvancedFilterPopover';
|
export * from './AdvancedFilter/AdvancedFilterPopover';
|
||||||
export * from './Dashboard/DashboardFilterButton';
|
export * from './Dashboard/DashboardFilterButton';
|
||||||
export * from './Dashboard/DashboardRowsHeightButton';
|
export * from './Dashboard/DashboardRowsHeightButton';
|
||||||
|
export * from './UniversalSearch/UniversalSearch';
|
||||||
|
export * from './PdfPreview';
|
||||||
|
|
||||||
const Hint = FieldHint;
|
const Hint = FieldHint;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { AnchorButton } from '@blueprintjs/core';
|
||||||
|
|
||||||
|
import { DialogContent, PdfDocumentPreview, T } from 'components';
|
||||||
|
import { usePdfInvoice } from 'hooks/query';
|
||||||
|
|
||||||
|
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||||
|
import { compose } from 'utils';
|
||||||
|
|
||||||
|
function InvoicePdfPreviewDialogContent({
|
||||||
|
// #withDialog
|
||||||
|
closeDialog,
|
||||||
|
}) {
|
||||||
|
const { isLoading, pdfUrl } = usePdfInvoice(1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogContent>
|
||||||
|
<div class="dialog__header-actions">
|
||||||
|
<AnchorButton
|
||||||
|
href={pdfUrl}
|
||||||
|
download={'invoice.pdf'}
|
||||||
|
minimal={true}
|
||||||
|
outlined={true}
|
||||||
|
>
|
||||||
|
<T id={'pdf_preview.preview.button'} />
|
||||||
|
</AnchorButton>
|
||||||
|
|
||||||
|
<AnchorButton
|
||||||
|
href={pdfUrl}
|
||||||
|
target={'__blank'}
|
||||||
|
minimal={true}
|
||||||
|
outlined={true}
|
||||||
|
>
|
||||||
|
<T id={'pdf_preview.download.button'} />
|
||||||
|
</AnchorButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PdfDocumentPreview
|
||||||
|
height={760}
|
||||||
|
width={1000}
|
||||||
|
isLoading={isLoading}
|
||||||
|
url={pdfUrl}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(withDialogActions)(InvoicePdfPreviewDialogContent);
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import React, { lazy } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { T, Dialog, DialogSuspense } from 'components';
|
||||||
|
|
||||||
|
import withDialogRedux from 'components/DialogReduxConnect';
|
||||||
|
|
||||||
|
import { CLASSES } from 'common/classes';
|
||||||
|
import { compose } from 'utils';
|
||||||
|
|
||||||
|
// Lazy loading the content.
|
||||||
|
const PdfPreviewDialogContent = lazy(() =>
|
||||||
|
import('./InvoicePdfPreviewDialogContent'),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice PDF preview dialog.
|
||||||
|
*/
|
||||||
|
function InvoicePdfPreviewDialog({ dialogName, payload, isOpen }) {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
name={dialogName}
|
||||||
|
title={<T id={'invoice_preview.dialog.title'} />}
|
||||||
|
className={classNames(CLASSES.DIALOG_PDF_PREVIEW)}
|
||||||
|
autoFocus={true}
|
||||||
|
canEscapeKeyClose={true}
|
||||||
|
isOpen={isOpen}
|
||||||
|
style={{ width: '1000px' }}
|
||||||
|
>
|
||||||
|
<DialogSuspense>
|
||||||
|
<PdfPreviewDialogContent
|
||||||
|
dialogName={dialogName}
|
||||||
|
subscriptionForm={payload}
|
||||||
|
/>
|
||||||
|
</DialogSuspense>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(withDialogRedux())(InvoicePdfPreviewDialog);
|
||||||
@@ -63,4 +63,5 @@ export function useCellAutoFocus(ref, autoFocus, columnId, rowIndex) {
|
|||||||
return ref;
|
return ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export * from './useRequestPdf';
|
||||||
export { useAsync, useAutofocus };
|
export { useAsync, useAutofocus };
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useQueryClient, useMutation } from 'react-query';
|
|||||||
import { useRequestQuery } from '../useQueryRequest';
|
import { useRequestQuery } from '../useQueryRequest';
|
||||||
import { transformPagination } from 'utils';
|
import { transformPagination } from 'utils';
|
||||||
import useApiRequest from '../useRequest';
|
import useApiRequest from '../useRequest';
|
||||||
|
import { useRequestPdf } from '../useRequestPdf';
|
||||||
import t from './types';
|
import t from './types';
|
||||||
|
|
||||||
// Common invalidate queries.
|
// Common invalidate queries.
|
||||||
@@ -145,10 +146,10 @@ export function useDeliverInvoice(props) {
|
|||||||
* Retrieve the sale invoice details.
|
* Retrieve the sale invoice details.
|
||||||
* @param {number} invoiceId - Invoice id.
|
* @param {number} invoiceId - Invoice id.
|
||||||
*/
|
*/
|
||||||
export function useInvoice(invoiceId, props) {
|
export function useInvoice(invoiceId, props, requestProps) {
|
||||||
return useRequestQuery(
|
return useRequestQuery(
|
||||||
[t.SALE_INVOICE, invoiceId],
|
[t.SALE_INVOICE, invoiceId],
|
||||||
{ method: 'get', url: `sales/invoices/${invoiceId}` },
|
{ method: 'get', url: `sales/invoices/${invoiceId}`, ...requestProps },
|
||||||
{
|
{
|
||||||
select: (res) => res.data.sale_invoice,
|
select: (res) => res.data.sale_invoice,
|
||||||
defaultData: {},
|
defaultData: {},
|
||||||
@@ -157,6 +158,13 @@ export function useInvoice(invoiceId, props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the invoice pdf document data.
|
||||||
|
*/
|
||||||
|
export function usePdfInvoice(invoiceId) {
|
||||||
|
return useRequestPdf(`/sales/invoices/${invoiceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve due invoices of the given customer id.
|
* Retrieve due invoices of the given customer id.
|
||||||
* @param {number} customerId - Customer id.
|
* @param {number} customerId - Customer id.
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default function useApiRequest() {
|
|||||||
return instance;
|
return instance;
|
||||||
}, [token, organizationId, setGlobalErrors, setLogout]);
|
}, [token, organizationId, setGlobalErrors, setLogout]);
|
||||||
|
|
||||||
return {
|
return React.useMemo(() => ({
|
||||||
http,
|
http,
|
||||||
|
|
||||||
get(resource, params) {
|
get(resource, params) {
|
||||||
@@ -84,5 +84,5 @@ export default function useApiRequest() {
|
|||||||
delete(resource, params) {
|
delete(resource, params) {
|
||||||
return http.delete(`/api/${resource}`, params);
|
return http.delete(`/api/${resource}`, params);
|
||||||
},
|
},
|
||||||
};
|
}), [http]);
|
||||||
}
|
}
|
||||||
|
|||||||
38
client/src/hooks/useRequestPdf.js
Normal file
38
client/src/hooks/useRequestPdf.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import useApiRequest from './useRequest';
|
||||||
|
|
||||||
|
export const useRequestPdf = (url) => {
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
|
const [isLoaded, setIsLoaded] = React.useState(false);
|
||||||
|
const [pdfUrl, setPdfUrl] = React.useState('');
|
||||||
|
const [response, setResponse] = React.useState(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setIsLoading(true);
|
||||||
|
apiRequest
|
||||||
|
.get(url, {
|
||||||
|
headers: { accept: 'application/pdf' },
|
||||||
|
responseType: 'blob',
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
// Create a Blob from the PDF Stream.
|
||||||
|
const file = new Blob([response.data], { type: 'application/pdf' });
|
||||||
|
|
||||||
|
// Build a URL from the file
|
||||||
|
const fileURL = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
setPdfUrl(fileURL);
|
||||||
|
setIsLoading(false);
|
||||||
|
setIsLoaded(true);
|
||||||
|
setResponse(response);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading,
|
||||||
|
isLoaded,
|
||||||
|
pdfUrl,
|
||||||
|
response,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1204,6 +1204,21 @@
|
|||||||
"the_contact_has_been_activated_successfully": "تم تفعيل جهة اتصال بنجاح.",
|
"the_contact_has_been_activated_successfully": "تم تفعيل جهة اتصال بنجاح.",
|
||||||
"the_contact_has_been_inactivated_successfully": "تم إلغاء تنشيط جهة اتصال بنجاح.",
|
"the_contact_has_been_inactivated_successfully": "تم إلغاء تنشيط جهة اتصال بنجاح.",
|
||||||
"are_sure_to_inactive_this_contact": "هل أنت متأكد أنك تريد إلغاء تنشيط جهة اتصال؟ ستكون قادرًا على تنشيطه لاحقًا",
|
"are_sure_to_inactive_this_contact": "هل أنت متأكد أنك تريد إلغاء تنشيط جهة اتصال؟ ستكون قادرًا على تنشيطه لاحقًا",
|
||||||
"are_sure_to_activate_this_contact": "هل أنت متأكد أنك تريد تفعيل جهة اتصال؟ ستتمكن من تعطيله لاحقًا"
|
"are_sure_to_activate_this_contact": "هل أنت متأكد أنك تريد تفعيل جهة اتصال؟ ستتمكن من تعطيله لاحقًا",
|
||||||
|
"publish_adjustment": "Publish adjustment",
|
||||||
|
"inactivate_customer": "Inactivate customer",
|
||||||
|
"activate_customer": "Activate customer",
|
||||||
|
"filter.all_filters_must_match": "Atleast one filter must match",
|
||||||
|
"filter.atleast_one_filter_must_match": "Atleast one filter must match",
|
||||||
|
"filter.when": "When",
|
||||||
|
"universal_search.placeholder": "Search...",
|
||||||
|
|
||||||
|
"universal_search.enter_text": "To select",
|
||||||
|
"universal_search.close_text": "To close",
|
||||||
|
"universal_seach.navigate_text": "To navigate",
|
||||||
|
"pdf_preview.dialog.title": "PDF Preview",
|
||||||
|
"pdf_preview.download.button": "Download",
|
||||||
|
"pdf_preview.preview.button": "Preview",
|
||||||
|
"invoice_preview.dialog.title": "Invoice PDF Preview"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1197,5 +1197,13 @@
|
|||||||
"activate_customer": "Activate customer",
|
"activate_customer": "Activate customer",
|
||||||
"filter.all_filters_must_match": "Atleast one filter must match",
|
"filter.all_filters_must_match": "Atleast one filter must match",
|
||||||
"filter.atleast_one_filter_must_match": "Atleast one filter must match",
|
"filter.atleast_one_filter_must_match": "Atleast one filter must match",
|
||||||
"filter.when": "When"
|
"filter.when": "When",
|
||||||
|
"universal_search.placeholder": "Search...",
|
||||||
|
"universal_search.enter_text": "To select",
|
||||||
|
"universal_search.close_text": "To close",
|
||||||
|
"universal_seach.navigate_text": "To navigate",
|
||||||
|
"pdf_preview.dialog.title": "PDF Preview",
|
||||||
|
"pdf_preview.download.button": "Download",
|
||||||
|
"pdf_preview.preview.button": "Preview",
|
||||||
|
"invoice_preview.dialog.title": "Invoice PDF Preview"
|
||||||
}
|
}
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
@import 'components/Overlay';
|
@import 'components/Overlay';
|
||||||
@import 'components/Menu';
|
@import 'components/Menu';
|
||||||
@import 'components/SidebarOverlay';
|
@import 'components/SidebarOverlay';
|
||||||
|
@import 'components/UniversalSearch';
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
@import 'pages/view-form';
|
@import 'pages/view-form';
|
||||||
@@ -201,12 +202,52 @@ html[lang^="ar"] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bp3-popover2{
|
.bp3-popover2 {
|
||||||
box-shadow: 0 0 0;
|
box-shadow: 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bp3-tooltip2 .bp3-popover2-arrow:before {
|
||||||
.bp3-tooltip2 .bp3-popover2-arrow:before{
|
|
||||||
box-shadow: 0 0 0;
|
box-shadow: 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialog--pdf-preview-dialog {
|
||||||
|
width: 1000px;
|
||||||
|
max-height: 800px;
|
||||||
|
margin: 10px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
align-self: flex-start;
|
||||||
|
padding-bottom: 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.dialog__header-actions{
|
||||||
|
position: absolute;
|
||||||
|
right: 50px;
|
||||||
|
top: 0;
|
||||||
|
z-index: 9999999;
|
||||||
|
margin: 6px;
|
||||||
|
|
||||||
|
.bp3-button + .bp3-button{
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp3-button{
|
||||||
|
border-color: rgba(0, 0, 0, 0.25);
|
||||||
|
color: rgb(25, 32, 37);
|
||||||
|
min-height: 30px;
|
||||||
|
padding-left: 14px;
|
||||||
|
padding-right: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp3-dialog {
|
||||||
|
|
||||||
|
&-body {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
> .bp3-spinner {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user