feat: branding templates table

This commit is contained in:
Ahmed Bouhuolia
2024-09-11 21:16:21 +02:00
parent ef74e250f1
commit a7df23cebc
20 changed files with 418 additions and 16 deletions

View File

@@ -149,7 +149,7 @@ export default () => {
dashboard.use('/export', Container.get(ExportController).router());
dashboard.use('/attachments', Container.get(AttachmentsController).router());
dashboard.use(
'/pdf_templates',
'/pdf-templates',
Container.get(PdfTemplatesController).router()
);

View File

@@ -1,8 +1,7 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { getTransactionTypeLabel } from '@/utils/transactions-types';
export class GetPdfTemplatesTransformer extends Transformer {
// Empty transformer with no additional methods or attributes
/**
* Exclude attributes.
* @returns {string[]}
@@ -10,4 +9,20 @@ export class GetPdfTemplatesTransformer extends Transformer {
public excludeAttributes = (): string[] => {
return ['attributes'];
};
/**
* Includeded attributes.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return ['createdAtFormatted', 'resourceFormatted'];
};
private createdAtFormatted = (template) => {
return this.formatDate(template.createdAt);
};
private resourceFormatted = (template) => {
return getTransactionTypeLabel(template.resource);
};
}

View File

@@ -2,8 +2,12 @@
import React from 'react';
import styled from 'styled-components';
export function Card({ className, children }) {
return <CardRoot className={className}>{children}</CardRoot>;
export function Card({ className, style, children }) {
return (
<CardRoot className={className} style={style}>
{children}
</CardRoot>
);
}
const CardRoot = styled.div`

View File

@@ -27,9 +27,11 @@ import { InvoiceCustomizeDrawer } from '@/containers/Sales/Invoices/InvoiceCusto
import { EstimateCustomizeDrawer } from '@/containers/Sales/Estimates/EstimateCustomize/EstimateCustomizeDrawer';
import { ReceiptCustomizeDrawer } from '@/containers/Sales/Receipts/ReceiptCustomize/ReceiptCustomizeDrawer';
import { CreditNoteCustomizeDrawer } from '@/containers/Sales/CreditNotes/CreditNoteCustomize/CreditNoteCustomizeDrawer';
import { PaymentReceivedCustomizeDrawer } from '@/containers/Sales/PaymentsReceived/PaymentReceivedCustomize/PaymentReceivedCustomizeDrawer';
import { BrandingTemplatesDrawer } from '@/containers/Sales/Invoices/BrandingTemplates/BrandingTemplatesDrawer';
import { DRAWERS } from '@/constants/drawers';
import { PaymentReceivedCustomizeDrawer } from '@/containers/Sales/PaymentsReceived/PaymentReceivedCustomize/PaymentReceivedCustomizeDrawer';
/**
* Drawers container of the dashboard.
*/
@@ -73,7 +75,10 @@ export default function DrawersContainer() {
<EstimateCustomizeDrawer name={DRAWERS.ESTIMATE_CUSTOMIZE} />
<ReceiptCustomizeDrawer name={DRAWERS.RECEIPT_CUSTOMIZE} />
<CreditNoteCustomizeDrawer name={DRAWERS.CREDIT_NOTE_CUSTOMIZE} />
<PaymentReceivedCustomizeDrawer name={DRAWERS.PAYMENT_RECEIVED_CUSTOMIZE} />
<PaymentReceivedCustomizeDrawer
name={DRAWERS.PAYMENT_RECEIVED_CUSTOMIZE}
/>
<BrandingTemplatesDrawer name={DRAWERS.BRANDING_TEMPLATES} />
</div>
);
}

View File

@@ -30,6 +30,6 @@ export enum DRAWERS {
PAYMENT_RECEIPT_CUSTOMIZE = 'PAYMENT_RECEIPT_CUSTOMIZE',
RECEIPT_CUSTOMIZE = 'RECEIPT_CUSTOMIZE',
CREDIT_NOTE_CUSTOMIZE = 'CREDIT_NOTE_CUSTOMIZE',
PAYMENT_RECEIVED_CUSTOMIZE = 'PAYMENT_RECEIVED_CUSTOMIZE'
PAYMENT_RECEIVED_CUSTOMIZE = 'PAYMENT_RECEIVED_CUSTOMIZE',
BRANDING_TEMPLATES = 'BRANDING_TEMPLATES'
}

View File

@@ -29,6 +29,7 @@ import { CashflowAlerts } from '../CashFlow/CashflowAlerts';
import { BankRulesAlerts } from '../Banking/Rules/RulesList/BankRulesAlerts';
import { SubscriptionAlerts } from '../Subscriptions/alerts/alerts';
import { BankAccountAlerts } from '@/containers/CashFlow/AccountTransactions/alerts';
import { BrandingTemplatesAlerts } from '../Sales/Invoices/BrandingTemplates/alerts/BrandingTemplatesAlerts';
export default [
...AccountsAlerts,
@@ -61,4 +62,5 @@ export default [
...BankRulesAlerts,
...SubscriptionAlerts,
...BankAccountAlerts,
...BrandingTemplatesAlerts,
];

View File

@@ -0,0 +1,15 @@
.table {
:global {
.table .tbody .tr .td{
padding-top: 14px;
padding-bottom: 14px;
}
.table .thead .th{
text-transform: uppercase;
font-size: 13px;
}
}
}

View File

@@ -0,0 +1,35 @@
// @ts-nocheck
import React from 'react';
import { Button, NavbarGroup, Intent } from '@blueprintjs/core';
import { DashboardActionsBar, Icon } from '@/components';
import { DRAWERS } from '@/constants/drawers';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import { compose } from '@/utils';
/**
* Account drawer action bar.
*/
function BrandingTemplateActionsBarRoot({ openDrawer }) {
// Handle new child button click.
const handleCreateBtnClick = () => {
openDrawer(DRAWERS.INVOICE_CUSTOMIZE);
};
return (
<DashboardActionsBar>
<NavbarGroup>
<Button
intent={Intent.PRIMARY}
icon={<Icon icon="plus" />}
onClick={handleCreateBtnClick}
minimal
>
Create Invoice Branding
</Button>
</NavbarGroup>
</DashboardActionsBar>
);
}
export const BrandingTemplateActionsBar = compose(withDrawerActions)(
BrandingTemplateActionsBarRoot,
);

View File

@@ -0,0 +1,32 @@
import React, { createContext } from 'react';
import { useGetPdfTemplates } from '@/hooks/query/pdf-templates';
interface BrandingTemplatesBootValues {
pdfTemplates: any;
isPdfTemplatesLoading: boolean;
}
const BrandingTemplatesBootContext = createContext<BrandingTemplatesBootValues>(
{} as BrandingTemplatesBootValues,
);
interface BrandingTemplatesBootProps {
children: React.ReactNode;
}
function BrandingTemplatesBoot({ ...props }: BrandingTemplatesBootProps) {
const { data: pdfTemplates, isLoading: isPdfTemplatesLoading } =
useGetPdfTemplates();
const provider = {
pdfTemplates,
isPdfTemplatesLoading,
} as BrandingTemplatesBootValues;
return <BrandingTemplatesBootContext.Provider value={provider} {...props} />;
}
const useBrandingTemplatesBoot = () =>
React.useContext<BrandingTemplatesBootValues>(BrandingTemplatesBootContext);
export { BrandingTemplatesBoot, useBrandingTemplatesBoot };

View File

@@ -0,0 +1,52 @@
// @ts-nocheck
import * as R from 'ramda';
import { Button, Classes, Intent } from '@blueprintjs/core';
import { BrandingTemplatesBoot } from './BrandingTemplatesBoot';
import {
Box,
Card,
DrawerBody,
DrawerHeaderContent,
Group,
} from '@/components';
import { DRAWERS } from '@/constants/drawers';
import { BrandingTemplatesTable } from './BrandingTemplatesTable';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import { BrandingTemplateActionsBar } from './BrandingTemplatesActionsBar';
export default function BrandingTemplateContent() {
return (
<Box>
<DrawerHeaderContent
name={DRAWERS.BRANDING_TEMPLATES}
title={'Branding Templates'}
/>
<Box className={Classes.DRAWER_BODY}>
<BrandingTemplatesBoot>
<BrandingTemplateActionsBar />
<Card style={{ padding: 0 }}>
<BrandingTemplatesTable />
</Card>
</BrandingTemplatesBoot>
</Box>
</Box>
);
}
const BrandingTemplateHeader = R.compose(withDrawerActions)(
({ openDrawer }) => {
const handleCreateBtnClick = () => {
openDrawer(DRAWERS.INVOICE_CUSTOMIZE);
};
return (
<Group>
<Button intent={Intent.PRIMARY} onClick={handleCreateBtnClick}>
Create Invoice Branding
</Button>
</Group>
);
},
);
BrandingTemplateHeader.displayName = 'BrandingTemplateHeader';

View File

@@ -0,0 +1,37 @@
// @ts-nocheck
import React from 'react';
import * as R from 'ramda';
import { Drawer, DrawerSuspense } from '@/components';
import withDrawers from '@/containers/Drawer/withDrawers';
const BrandingTemplatesContent = React.lazy(
() => import('./BrandingTemplatesContent'),
);
/**
* Invoice customize drawer.
* @returns {React.ReactNode}
*/
function BrandingTemplatesDrawerRoot({
name,
// #withDrawer
isOpen,
payload: {},
}) {
return (
<Drawer
isOpen={isOpen}
name={name}
size={'600px'}
style={{ borderLeftColor: '#cbcbcb' }}
>
<DrawerSuspense>
<BrandingTemplatesContent />
</DrawerSuspense>
</Drawer>
);
}
export const BrandingTemplatesDrawer = R.compose(withDrawers())(
BrandingTemplatesDrawerRoot,
);

View File

@@ -0,0 +1,87 @@
// @ts-nocheck
import * as R from 'ramda';
import { Classes, Tag } from '@blueprintjs/core';
import clsx from 'classnames';
import { DataTable, Group, TableSkeletonRows } from '@/components';
import { useBrandingTemplatesBoot } from './BrandingTemplatesBoot';
import { ActionsMenu } from './_components';
import withAlertActions from '@/containers/Alert/withAlertActions';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import { DRAWERS } from '@/constants/drawers';
import styles from './BrandTemplates.module.scss';
interface BrandingTemplatesTableProps {}
function BrandingTemplateTableRoot({
openAlert,
openDrawer,
}: BrandingTemplatesTableProps) {
// Table columns.
const columns = useBrandingTemplatesColumns();
const { isPdfTemplatesLoading, pdfTemplates } = useBrandingTemplatesBoot();
const handleEditTemplate = (template) => {
openDrawer(DRAWERS.INVOICE_CUSTOMIZE, {
templateId: template.id,
resource: template.resource,
});
};
const handleDeleteTemplate = (template) => {
openAlert('branding-template-delete', { templateId: template.id });
};
const handleCellClick = (cell, event) => {
const templateId = cell.row.original.id;
const resource = cell.row.original.resource;
openDrawer(DRAWERS.INVOICE_CUSTOMIZE, { templateId, resource });
};
return (
<DataTable
columns={columns}
data={pdfTemplates || []}
loading={isPdfTemplatesLoading}
progressBarLoading={isPdfTemplatesLoading}
TableLoadingRenderer={TableSkeletonRows}
ContextMenu={ActionsMenu}
noInitialFetch={true}
payload={{
onDeleteTemplate: handleDeleteTemplate,
onEditTemplate: handleEditTemplate,
}}
rowContextMenu={ActionsMenu}
onCellClick={handleCellClick}
className={styles.table}
/>
);
}
export const BrandingTemplatesTable = R.compose(
withAlertActions,
withDrawerActions,
)(BrandingTemplateTableRoot);
const useBrandingTemplatesColumns = () => {
return [
{
Header: 'Template Name',
accessor: (row) => (
<Group spacing={10}>
{row.template_name} <Tag round>Default</Tag>
</Group>
),
width: 65,
clickable: true,
},
{
Header: 'Created At',
accessor: 'created_at_formatted',
width: 35,
className: clsx(Classes.TEXT_MUTED),
clickable: true,
},
];
};

View File

@@ -0,0 +1,26 @@
// @ts-nocheck
import { safeCallback } from '@/utils';
import { Intent, Menu, MenuDivider, MenuItem } from '@blueprintjs/core';
/**
* Templates table actions menu.
*/
export function ActionsMenu({
row: { original },
payload: { onDeleteTemplate, onEditTemplate },
}) {
return (
<Menu>
<MenuItem
text={'Edit Template'}
onClick={safeCallback(onEditTemplate, original)}
/>
<MenuDivider />
<MenuItem
text={'Delete Template'}
intent={Intent.DANGER}
onClick={safeCallback(onDeleteTemplate, original)}
/>
</Menu>
);
}

View File

@@ -0,0 +1,10 @@
// @ts-nocheck
import React from 'react';
const DeleteBrandingTemplateAlert = React.lazy(
() => import('./DeleteBrandingTemplateAlert'),
);
export const BrandingTemplatesAlerts = [
{ name: 'branding-template-delete', component: DeleteBrandingTemplateAlert },
];

View File

@@ -0,0 +1,72 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { AppToaster } from '@/components';
import { Alert, Intent } from '@blueprintjs/core';
import { useDeletePdfTemplate} from '@/hooks/query/pdf-templates';
import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect';
import withAlertActions from '@/containers/Alert/withAlertActions';
import { compose } from '@/utils';
/**
* Delete branding template alert.
*/
function DeleteBrandingTemplateAlert({
// #ownProps
name,
// #withAlertStoreConnect
isOpen,
payload: { templateId },
// #withAlertActions
closeAlert,
}) {
const { mutateAsync: deleteBrandingTemplateMutate } = useDeletePdfTemplate();
const handleConfirmDelete = () => {
deleteBrandingTemplateMutate({ templateId })
.then(() => {
AppToaster.show({
message: 'The branding template has been deleted successfully.',
intent: Intent.SUCCESS,
});
closeAlert(name);
})
.catch((error) => {
AppToaster.show({
message: 'Something went wrong.',
intent: Intent.DANGER,
});
closeAlert(name);
});
};
const handleCancel = () => {
closeAlert(name);
};
return (
<Alert
cancelButtonText={intl.get('cancel')}
confirmButtonText={intl.get('delete')}
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={handleCancel}
onConfirm={handleConfirmDelete}
>
<p>
Are you sure want to delete branding template?
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(DeleteBrandingTemplateAlert);

View File

@@ -41,11 +41,11 @@ export default function InvoiceCustomizeContent() {
const reqValues = transformToEditRequest(values, templateId);
// Edit existing template
editPdfTemplate({ ...reqValues })
.then(() => handleSuccess('PDF template updated successfully!'))
.catch(() =>
handleError('An error occurred while updating the PDF template.'),
);
// editPdfTemplate({ templateId, values: reqValues })
// .then(() => handleSuccess('PDF template updated successfully!'))
// .catch(() =>
// handleError('An error occurred while updating the PDF template.'),
// );
} else {
const reqValues = transformToNewRequest(values);

View File

@@ -109,7 +109,7 @@ function InvoiceActionsBar({
// Handles the invoice customize button click.
const handleCustomizeBtnClick = () => {
openDrawer(DRAWERS.INVOICE_CUSTOMIZE);
openDrawer(DRAWERS.BRANDING_TEMPLATES);
};
return (

View File

@@ -1,3 +1,4 @@
// @ts-nocheck
import {
useMutation,
useQuery,

View File

@@ -20,7 +20,7 @@
.th {
padding: 0.68rem 0.5rem;
background: #f5f5f5;
background: #f6f7f9;
font-size: 14px;
color: #424853;
font-weight: 400;

View File

@@ -232,6 +232,15 @@ $dashboard-views-bar-height: 44px;
&.#{$ns}-minimal.#{$ns}-intent-warning{
color: #cc7e25;
}
&.#{$ns}-minimal.#{$ns}-intent-primary{
color: #223fc5;
&:hover,
&:focus{
color: #223fc5;
background: rgba(34, 63, 197, 0.08);
}
}
&.button--blue-highlight {
background-color: #ebfaff;