Compare commits

...

39 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
031ccc4a0b fix: Edit the payment received transactions with attachments 2024-06-10 13:41:10 +02:00
Ahmed Bouhuolia
858f347fd4 Merge pull request #493 from bigcapitalhq/BIG-198
fix: Something wrong in uploading uncategorized bank transactions
2024-06-09 21:30:32 +02:00
Ahmed Bouhuolia
4d73b59cf3 fix: Something wrong in uploading uncategorized bank transactions 2024-06-09 21:30:07 +02:00
Ahmed Bouhuolia
bc67f0cca8 fix: increment/decrement the uncategorized transactios on accounts 2024-06-09 21:05:43 +02:00
Ahmed Bouhuolia
ef2d1ff141 feat: Add COGS type to cash transactions categorization 2024-06-09 21:05:19 +02:00
Ahmed Bouhuolia
dc4cdb2a8f fix: Assign branch in categorize bank transaction 2024-06-09 20:05:15 +02:00
Ahmed Bouhuolia
8862810706 Merge pull request #489 from bigcapitalhq/fix-plaid-syncing
fix: Plaid data available syncing
2024-06-07 01:31:34 +02:00
Ahmed Bouhuolia
3dadbeac4d fix: all sql queries should be under one transaction 2024-06-07 01:30:08 +02:00
Ahmed Bouhuolia
494d2c1fe0 fix: TS typing 2024-06-07 01:11:19 +02:00
Ahmed Bouhuolia
d27562bd43 fix: Plaid data available syncing 2024-06-07 01:07:17 +02:00
Ahmed Bouhuolia
fc9995c4da chore: dump CHANGELOG.md 2024-06-06 12:32:31 +02:00
Ahmed Bouhuolia
7dc769004d fix: billing variant id 2024-06-06 11:19:19 +02:00
Ahmed Bouhuolia
909a70e2c5 feat: correct the migration files 2024-06-04 17:42:29 +02:00
Ahmed Bouhuolia
84dd0fa86b Merge remote-tracking branch 'refs/remotes/origin/develop' into develop 2024-06-04 16:22:07 +02:00
Ahmed Bouhuolia
a4719fe15b fix: add Plaid env variables to docker-compose.prod file 2024-06-04 16:21:49 +02:00
Ahmed Bouhuolia
fd915b503f fix: Run migrations only for initialized tenants (#484) 2024-06-04 16:13:18 +02:00
Ahmed Bouhuolia
bbba54c08e fix: validate the s3 configures exist (#482) 2024-06-04 15:11:21 +02:00
Ahmed Bouhuolia
f241e2bede fix: Plaid syncs deposit imports as withdrawals (#481) 2024-06-03 21:56:29 +02:00
Ahmed Bouhuolia
175bc243f3 fix: Organize Plaid env variables for development and sandbox envs (#480) 2024-06-03 20:50:02 +02:00
Ahmed Bouhuolia
7c06c8bb8a fix: Lemon Squeezy redirect to base url (#479)
fix: Lemon Squeezy redirect to base url
2024-06-03 19:54:40 +02:00
Ahmed Bouhuolia
8fd930caac Merge pull request #478 from bigcapitalhq/virtual-docker-internal-network
feat: Internal docker virtual network
2024-06-02 21:25:55 +02:00
Ahmed Bouhuolia
e175307da4 feat: internal docker virtual network 2024-06-02 21:25:15 +02:00
Ahmed Bouhuolia
b1bf932e88 fix: add S3 env variables to docker-compose prod 2024-06-02 17:46:10 +02:00
Ahmed Bouhuolia
aa897212ab Merge remote-tracking branch 'refs/remotes/origin/develop' into develop 2024-06-02 17:35:49 +02:00
Ahmed Bouhuolia
890903e08b chore: change the variant id. 2024-06-02 17:34:52 +02:00
Ahmed Bouhuolia
16b2a33cf6 Merge pull request #476 from bigcapitalhq/abouolia-patch-1
Build and deploy develop Docker container
2024-06-02 16:57:36 +02:00
Ahmed Bouhuolia
382d4ab028 Build and deploy develop Docker container 2024-06-02 16:57:07 +02:00
Ahmed Bouhuolia
85f26e1079 Merge pull request #460 from bigcapitalhq/print-resources
feat: Export resource tables to pdf
2024-06-02 13:24:43 +02:00
Ahmed Bouhuolia
1b237323f6 feat: document export functions 2024-06-02 13:24:21 +02:00
Ahmed Bouhuolia
f15fecde54 feat: resource tables printing 2024-06-02 13:15:56 +02:00
Ahmed Bouhuolia
79be4266bb feat: style resource printable columns 2024-05-31 18:12:09 +02:00
Ahmed Bouhuolia
08ad117331 feat: migrate the printing from Attachment to Document model 2024-05-31 15:32:51 +02:00
Ahmed Bouhuolia
958f78e7a4 feat: improve UI experience of resource priting 2024-05-31 15:30:49 +02:00
Ahmed Bouhuolia
ba77351e44 Merge branch 'develop' into print-resources 2024-05-30 19:50:05 +02:00
Ahmed Bouhuolia
09a15966f0 Merge pull request #459 from bigcapitalhq/skip-sending-confirm-email-if-disabled
fix: skip send confirmation email if disabled
2024-05-30 18:38:17 +02:00
Ahmed Bouhuolia
7ff36e8c4f feat: add s3 env variables to .env.example 2024-05-30 17:58:58 +02:00
Ahmed Bouhuolia
dc5bdf0b66 feat: print action when click on print button 2024-05-23 19:39:23 +02:00
Ahmed Bouhuolia
fe41f7976d feat: export resource tables to pdf 2024-05-23 14:23:49 +02:00
Ahmed Bouhuolia
2c7da86a00 fix: skip send confirmation email if disabled 2024-05-23 10:09:48 +02:00
98 changed files with 1405 additions and 539 deletions

View File

@@ -75,31 +75,17 @@ PLAID_ENV=sandbox
# Your Plaid keys, which can be found in the Plaid Dashboard. # Your Plaid keys, which can be found in the Plaid Dashboard.
# https://dashboard.plaid.com/account/keys # https://dashboard.plaid.com/account/keys
PLAID_CLIENT_ID= PLAID_CLIENT_ID=
PLAID_SECRET_DEVELOPMENT= PLAID_SECRET=
PLAID_SECRET_SANDBOX=
PLAID_LINK_WEBHOOK= PLAID_LINK_WEBHOOK=
# (Optional) Redirect URI settings section
# Only required for OAuth redirect URI testing (not common on desktop):
# Sandbox Mode:
# Set the PLAID_SANDBOX_REDIRECT_URI below to 'http://localhost:3001/oauth-link'.
# The OAuth redirect flow requires an endpoint on the developer's website
# that the bank website should redirect to. You will also need to configure
# this redirect URI for your client ID through the Plaid developer dashboard
# at https://dashboard.plaid.com/team/api.
# Development mode:
# When running in development mode, you must use an https:// url.
# You will need to configure this https:// redirect URI in the Plaid developer dashboard.
# Instructions to create a self-signed certificate for localhost can be found at
# https://github.com/plaid/pattern/blob/master/README.md#testing-oauth.
# If your system is not set up to run localhost with https://, you will be unable to test
# the OAuth in development and should leave the PLAID_DEVELOPMENT_REDIRECT_URI blank.
PLAID_SANDBOX_REDIRECT_URI=
PLAID_DEVELOPMENT_REDIRECT_URI=
# https://docs.lemonsqueezy.com/guides/developer-guide/getting-started#create-an-api-key # https://docs.lemonsqueezy.com/guides/developer-guide/getting-started#create-an-api-key
LEMONSQUEEZY_API_KEY= LEMONSQUEEZY_API_KEY=
LEMONSQUEEZY_STORE_ID= LEMONSQUEEZY_STORE_ID=
LEMONSQUEEZY_WEBHOOK_SECRET= LEMONSQUEEZY_WEBHOOK_SECRET=
# S3 documents and attachments
S3_REGION=
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_ENDPOINT=
S3_BUCKET=

View File

@@ -0,0 +1,127 @@
# This workflow will build a docker container, publish it to Github Registry.
name: Build and Deploy Develop Docker Container
on:
push:
branches:
- develop
env:
WEBAPP_IMAGE_NAME: bigcapitalhq/webapp
SERVER_IMAGE_NAME: bigcapitalhq/server
jobs:
build-publish-webapp:
strategy:
fail-fast: false
name: Build and deploy webapp container
runs-on: ubuntu-latest
environment: production
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Login to Container registry.
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.WEBAPP_IMAGE_NAME }}
# Builds and push the Docker image.
- name: Build and push Docker image
uses: docker/build-push-action@v5
id: build
with:
context: ./
file: ./packages/webapp/Dockerfile
platforms: linux/amd64
push: true
labels: ${{ steps.meta.outputs.labels }}
tags: bigcapitalhq/webapp:develop
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-webapp
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
# Send notification to Slack channel.
- name: Slack Notification built and published webapp container successfully.
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
build-publish-server:
name: Build and deploy server container
runs-on: ubuntu-latest
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Login to Container registry.
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# Builds and push the Docker image.
- name: Build and push Docker image
uses: docker/build-push-action@v5
id: build
with:
context: ./
file: ./packages/server/Dockerfile
platforms: linux/amd64
push: true
tags: bigcapitalhq/server:develop
labels: ${{ steps.meta.outputs.labels }}
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-server
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
# Send notification to Slack channel.
- name: Slack Notification built and published server container successfully.
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

View File

@@ -2,6 +2,41 @@
All notable changes to Bigcapital server-side will be in this file. All notable changes to Bigcapital server-side will be in this file.
## [0.17.0] - 04-06-2024
### New
* feat: Upload and attach documents by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/461
* feat: Export resource tables to pdf by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/460
* feat: Build and deploy develop Docker container by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/476
* feat: Internal docker virtual network by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/478
### Fixes
* fix: Skip send confirmation email if disabled by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/459
* fix: Lemon Squeezy redirect to base url by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/479
* fix: Organize Plaid env variables for development and sandbox envs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/480
* fix: Plaid syncs deposit imports as withdrawals by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/481
* fix: Validate the s3 configures exist by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/482
* fix: Run migrations only for initialized tenants by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/484
## [0.16.16] -
* feat: handle http exceptions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/456
* feat: add the missing Newrelic env vars to docker-compose.prod file by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/457
* fix: add the signup email confirmation env var by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/458
## [0.16.14] -
* fix: Typo in setup wizard by @ccantrell72 in https://github.com/bigcapitalhq/bigcapital/pull/440
* fix: Showing the real mail address on email confirmation view by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/445
* fix: Auto-increment setting parsing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/453
## [0.16.12] -
* feat: Create a manifest list for `webapp` Docker image and push it to DockerHub. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/436
* feat: Combine arm64 and amd64 in one Github action runner by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/437
## [0.16.11] - 06-05-2024 ## [0.16.11] - 06-05-2024
### improvements ### improvements

View File

@@ -22,11 +22,15 @@ services:
- server - server
- webapp - webapp
restart: on-failure restart: on-failure
networks:
- bigcapital_network
webapp: webapp:
container_name: bigcapital-webapp container_name: bigcapital-webapp
image: bigcapitalhq/webapp:latest image: bigcapitalhq/webapp:latest
restart: on-failure restart: on-failure
networks:
- bigcapital_network
server: server:
container_name: bigcapital-server container_name: bigcapital-server
@@ -89,14 +93,17 @@ services:
- GOTENBERG_URL=${GOTENBERG_URL} - GOTENBERG_URL=${GOTENBERG_URL}
- GOTENBERG_DOCS_URL=${GOTENBERG_DOCS_URL} - GOTENBERG_DOCS_URL=${GOTENBERG_DOCS_URL}
# Exchange Rate
- EXCHANGE_RATE_SERVICE=${EXCHANGE_RATE_SERVICE}
- OPEN_EXCHANGE_RATE_APP_ID-${OPEN_EXCHANGE_RATE_APP_ID}
# Bank Sync # Bank Sync
- BANKING_CONNECT=${BANKING_CONNECT} - BANKING_CONNECT=${BANKING_CONNECT}
# Plaid # Plaid
- PLAID_ENV=${PLAID_ENV} - PLAID_ENV=${PLAID_ENV}
- PLAID_CLIENT_ID=${PLAID_CLIENT_ID} - PLAID_CLIENT_ID=${PLAID_CLIENT_ID}
- PLAID_SECRET_DEVELOPMENT=${PLAID_SECRET_DEVELOPMENT} - PLAID_SECRET=${PLAID_SECRET}
- PLAID_SECRET_SANDBOX=${b8cf42b441e110451e2f69ad7e1e9f}
- PLAID_LINK_WEBHOOK=${PLAID_LINK_WEBHOOK} - PLAID_LINK_WEBHOOK=${PLAID_LINK_WEBHOOK}
# Lemon Squeez # Lemon Squeez
@@ -114,6 +121,15 @@ services:
- NEW_RELIC_LICENSE_KEY=${NEW_RELIC_LICENSE_KEY} - NEW_RELIC_LICENSE_KEY=${NEW_RELIC_LICENSE_KEY}
- NEW_RELIC_APP_NAME=${NEW_RELIC_APP_NAME} - NEW_RELIC_APP_NAME=${NEW_RELIC_APP_NAME}
# S3
- S3_REGION=${S3_REGION}
- S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID}
- S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY}
- S3_ENDPOINT=${S3_ENDPOINT}
- S3_BUCKET=${S3_BUCKET}
networks:
- bigcapital_network
database_migration: database_migration:
container_name: bigcapital-database-migration container_name: bigcapital-database-migration
build: build:
@@ -130,6 +146,8 @@ services:
- TENANT_DB_NAME_PERFIX=${TENANT_DB_NAME_PERFIX} - TENANT_DB_NAME_PERFIX=${TENANT_DB_NAME_PERFIX}
depends_on: depends_on:
- mysql - mysql
networks:
- bigcapital_network
mysql: mysql:
container_name: bigcapital-mysql container_name: bigcapital-mysql
@@ -145,6 +163,8 @@ services:
- mysql:/var/lib/mysql - mysql:/var/lib/mysql
expose: expose:
- '3306' - '3306'
networks:
- bigcapital_network
mongo: mongo:
container_name: bigcapital-mongo container_name: bigcapital-mongo
@@ -154,6 +174,8 @@ services:
- '27017' - '27017'
volumes: volumes:
- mongo:/var/lib/mongodb - mongo:/var/lib/mongodb
networks:
- bigcapital_network
redis: redis:
container_name: bigcapital-redis container_name: bigcapital-redis
@@ -164,11 +186,15 @@ services:
- '6379' - '6379'
volumes: volumes:
- redis:/data - redis:/data
networks:
- bigcapital_network
gotenberg: gotenberg:
image: gotenberg/gotenberg:7 image: gotenberg/gotenberg:7
expose: expose:
- '9000' - '9000'
networks:
- bigcapital_network
# Volumes # Volumes
volumes: volumes:
@@ -183,3 +209,8 @@ volumes:
redis: redis:
name: bigcapital_prod_redis name: bigcapital_prod_redis
driver: local driver: local
# Networks
networks:
bigcapital_network:
driver: bridge

View File

@@ -0,0 +1,38 @@
@import "../base.scss";
body {
font-family: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
font-size: 12px;
line-height: 1.4;
margin: 0;
}
.sheet__title{
margin-bottom: 18px;
}
.sheet__title h2{
line-height: 1;
margin-top: 0;
margin-bottom: 10px;
font-size: 16px;
}
.sheet__table {
font-size: inherit;
line-height: inherit;
width: 100%;
}
.sheet__table {
table-layout: auto;
border-collapse: collapse;
width: 100%;
}
.sheet__table thead tr th {
border-top: 1px solid #000;
border-bottom: 1px solid #000;
background: #fff;
padding: 8px;
line-height: 1.2;
}
.sheet__table tbody tr td {
padding: 4px 8px;
border-bottom: 1px solid #CCC;
}

View File

@@ -0,0 +1,24 @@
block head
style
include ../../css/modules/export-resource-table.css
style.
!{customCSS}
block content
.sheet
.sheet__title
h2.sheetTitle= sheetTitle
p.sheetDesc= sheetDescription
table.sheet__table
thead
tr
each column in table.columns
th(style=column.style class='column--' + column.key)= column.name
tbody
each row in table.rows
tr(class=row.classNames)
each cell in row.cells
td(class='cell--' + cell.key)
span!= cell.value

View File

@@ -70,6 +70,10 @@ module.exports = {
src: `${RESOURCES_PATH}/scss/modules/financial-sheet.scss`, src: `${RESOURCES_PATH}/scss/modules/financial-sheet.scss`,
dest: `${RESOURCES_PATH}/css/modules`, dest: `${RESOURCES_PATH}/css/modules`,
}, },
{
src: `${RESOURCES_PATH}/scss/modules/export-resource-table.scss`,
dest: `${RESOURCES_PATH}/css/modules`,
},
], ],
// RTL builds. // RTL builds.
rtl: [ rtl: [

View File

@@ -4,12 +4,16 @@ import { Router, Response, NextFunction, Request } from 'express';
import { body, param } from 'express-validator'; import { body, param } from 'express-validator';
import BaseController from '@/api/controllers/BaseController'; import BaseController from '@/api/controllers/BaseController';
import { AttachmentsApplication } from '@/services/Attachments/AttachmentsApplication'; import { AttachmentsApplication } from '@/services/Attachments/AttachmentsApplication';
import { AttachmentUploadPipeline } from '@/services/Attachments/S3UploadPipeline';
@Service() @Service()
export class AttachmentsController extends BaseController { export class AttachmentsController extends BaseController {
@Inject() @Inject()
private attachmentsApplication: AttachmentsApplication; private attachmentsApplication: AttachmentsApplication;
@Inject()
private uploadPipelineService: AttachmentUploadPipeline;
/** /**
* Router constructor. * Router constructor.
*/ */
@@ -18,7 +22,8 @@ export class AttachmentsController extends BaseController {
router.post( router.post(
'/', '/',
this.attachmentsApplication.uploadPipeline.single('file'), this.uploadPipelineService.validateS3Configured,
this.uploadPipelineService.uploadPipeline().single('file'),
this.validateUploadedFileExistance, this.validateUploadedFileExistance,
this.uploadAttachment.bind(this) this.uploadAttachment.bind(this)
); );

View File

@@ -5,6 +5,7 @@ import BaseController from '@/api/controllers/BaseController';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { ExportApplication } from '@/services/Export/ExportApplication'; import { ExportApplication } from '@/services/Export/ExportApplication';
import { ACCEPT_TYPE } from '@/interfaces/Http'; import { ACCEPT_TYPE } from '@/interfaces/Http';
import { convertAcceptFormatToFormat } from './_utils';
@Service() @Service()
export class ExportController extends BaseController { export class ExportController extends BaseController {
@@ -25,7 +26,6 @@ export class ExportController extends BaseController {
], ],
this.validationResult, this.validationResult,
this.export.bind(this), this.export.bind(this),
this.catchServiceErrors
); );
return router; return router;
} }
@@ -48,10 +48,12 @@ export class ExportController extends BaseController {
ACCEPT_TYPE.APPLICATION_CSV, ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_PDF, ACCEPT_TYPE.APPLICATION_PDF,
]); ]);
const applicationFormat = convertAcceptFormatToFormat(acceptType);
const data = await this.exportResourceApp.export( const data = await this.exportResourceApp.export(
tenantId, tenantId,
query.resource, query.resource,
acceptType === ACCEPT_TYPE.APPLICATION_XLSX ? 'xlsx' : 'csv' applicationFormat
); );
// Retrieves the csv format. // Retrieves the csv format.
if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) { if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
@@ -70,31 +72,16 @@ export class ExportController extends BaseController {
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
); );
return res.send(data); return res.send(data);
//
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
res.set({
'Content-Type': 'application/pdf',
'Content-Length': data.length,
});
res.send(data);
} }
} catch (error) { } catch (error) {
next(error); next(error);
} }
} }
/**
* Transforms service errors to response.
* @param {Error}
* @param {Request} req
* @param {Response} res
* @param {ServiceError} error
*/
private catchServiceErrors(
error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
return res.status(400).send({
errors: [{ type: error.errorType }],
});
}
next(error);
}
} }

View File

@@ -0,0 +1,13 @@
import { ACCEPT_TYPE } from '@/interfaces/Http';
import { ExportFormat } from '@/services/Export/common';
export const convertAcceptFormatToFormat = (accept: string): ExportFormat => {
switch (accept) {
case ACCEPT_TYPE.APPLICATION_CSV:
return ExportFormat.Csv;
case ACCEPT_TYPE.APPLICATION_PDF:
return ExportFormat.Pdf;
case ACCEPT_TYPE.APPLICATION_XLSX:
return ExportFormat.Xlsx;
}
};

View File

@@ -1,7 +1,6 @@
import { Router } from 'express'; import { NextFunction, Router, Request, Response } from 'express';
import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication';
import { Request, Response } from 'express';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication';
import BaseController from '../BaseController'; import BaseController from '../BaseController';
import { LemonSqueezyWebhooks } from '@/services/Subscription/LemonSqueezyWebhooks'; import { LemonSqueezyWebhooks } from '@/services/Subscription/LemonSqueezyWebhooks';
import { PlaidWebhookTenantBootMiddleware } from '@/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware'; import { PlaidWebhookTenantBootMiddleware } from '@/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware';
@@ -34,7 +33,7 @@ export class Webhooks extends BaseController {
* @param {Response} res * @param {Response} res
* @returns {Response} * @returns {Response}
*/ */
public async lemonWebhooks(req: Request, res: Response, next: any) { public async lemonWebhooks(req: Request, res: Response, next: NextFunction) {
const data = req.body; const data = req.body;
const signature = req.headers['x-signature'] ?? ''; const signature = req.headers['x-signature'] ?? '';
const rawBody = req.rawBody; const rawBody = req.rawBody;
@@ -57,20 +56,25 @@ export class Webhooks extends BaseController {
* @param {Response} res * @param {Response} res
* @returns {Response} * @returns {Response}
*/ */
public async plaidWebhooks(req: Request, res: Response) { public async plaidWebhooks(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const {
webhook_type: webhookType,
webhook_code: webhookCode,
item_id: plaidItemId,
} = req.body;
await this.plaidApp.webhooks( try {
tenantId, const {
plaidItemId, webhook_type: webhookType,
webhookType, webhook_code: webhookCode,
webhookCode item_id: plaidItemId,
); } = req.body;
return res.status(200).send({ code: 200, message: 'ok' });
await this.plaidApp.webhooks(
tenantId,
plaidItemId,
webhookType,
webhookCode
);
return res.status(200).send({ code: 200, message: 'ok' });
} catch (error) {
next(error);
}
} }
} }

View File

@@ -71,6 +71,10 @@ function getAllSystemTenants(knex) {
return knex('tenants'); return knex('tenants');
} }
function getAllInitializedSystemTenants(knex) {
return knex('tenants').whereNotNull('initializedAt');
}
// module.exports = { // module.exports = {
// log, // log,
// success, // success,
@@ -183,7 +187,7 @@ commander
.action(async (cmd) => { .action(async (cmd) => {
try { try {
const sysKnex = await initSystemKnex(); const sysKnex = await initSystemKnex();
const tenants = await getAllSystemTenants(sysKnex); const tenants = await getAllInitializedSystemTenants(sysKnex);
const tenantsOrgsIds = tenants.map((tenant) => tenant.organizationId); const tenantsOrgsIds = tenants.map((tenant) => tenant.organizationId);
if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) { if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) {
@@ -220,7 +224,6 @@ commander
const oper = migrateTenant(cmd.tenant_id); const oper = migrateTenant(cmd.tenant_id);
migrateOpers.push(oper); migrateOpers.push(oper);
} }
Promise.all(migrateOpers).then(() => { Promise.all(migrateOpers).then(() => {
success('All tenants are migrated.'); success('All tenants are migrated.');
}); });
@@ -280,4 +283,3 @@ commander
exit(error); exit(error);
} }
}); });

View File

@@ -204,10 +204,7 @@ module.exports = {
plaid: { plaid: {
env: process.env.PLAID_ENV || 'sandbox', env: process.env.PLAID_ENV || 'sandbox',
clientId: process.env.PLAID_CLIENT_ID, clientId: process.env.PLAID_CLIENT_ID,
secretDevelopment: process.env.PLAID_SECRET_DEVELOPMENT, secret: process.env.PLAID_SECRET,
secretSandbox: process.env.PLAID_SECRET_SANDBOX,
redirectSandBox: process.env.PLAID_SANDBOX_REDIRECT_URI,
redirectDevelopment: process.env.PLAID_DEVELOPMENT_REDIRECT_URI,
linkWebhook: process.env.PLAID_LINK_WEBHOOK, linkWebhook: process.env.PLAID_LINK_WEBHOOK,
}, },
@@ -218,6 +215,7 @@ module.exports = {
key: process.env.LEMONSQUEEZY_API_KEY, key: process.env.LEMONSQUEEZY_API_KEY,
storeId: process.env.LEMONSQUEEZY_STORE_ID, storeId: process.env.LEMONSQUEEZY_STORE_ID,
webhookSecret: process.env.LEMONSQUEEZY_WEBHOOK_SECRET, webhookSecret: process.env.LEMONSQUEEZY_WEBHOOK_SECRET,
redirectTo: `${process.env.BASE_URL}/setup`,
}, },
/** /**
@@ -233,10 +231,10 @@ module.exports = {
* S3 for documents. * S3 for documents.
*/ */
s3: { s3: {
region: process.env.AWS_REGION, region: process.env.S3_REGION,
accessKeyId: process.env.AWS_ACCESS_KEY_ID, accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
endpoint: process.env.AWS_ENDPOINT, endpoint: process.env.S3_ENDPOINT,
bucket: process.env.AWS_BUCKET, bucket: process.env.S3_BUCKET || 'bigcapital-documents',
}, },
}; };

View File

@@ -0,0 +1,14 @@
exports.up = function (knex) {
return knex.schema.createTable('storage', (table) => {
table.increments('id').primary();
table.string('key').notNullable();
table.string('path').notNullable();
table.string('extension').notNullable();
table.integer('expire_in');
table.timestamps();
});
};
exports.down = function (knex) {
return knex.schema.dropTableIfExists('storage');
};

View File

@@ -0,0 +1,5 @@
exports.up = function (knex) {
return knex.schema.dropTableIfExists('storage');
};
exports.down = function (knex) {};

View File

@@ -3,7 +3,7 @@ exports.up = function (knex) {
table.increments('id').primary(); table.increments('id').primary();
table.string('key').notNullable(); table.string('key').notNullable();
table.string('mime_type').notNullable(); table.string('mime_type').notNullable();
table.integer('size').unsigned().notNullable(); table.integer('size').unsigned();
table.string('origin_name'); table.string('origin_name');
table.timestamps(); table.timestamps();
}); });

View File

@@ -164,3 +164,7 @@ export enum TaxRateAction {
DELETE = 'Delete', DELETE = 'Delete',
VIEW = 'View', VIEW = 'View',
} }
export interface CreateAccountParams {
ignoreUniqueName: boolean;
}

View File

@@ -122,6 +122,10 @@ export type IModelMetaCollectionField = IModelMetaCollectionFieldCommon &
export type IModelMetaRelationField = IModelMetaRelationFieldCommon & export type IModelMetaRelationField = IModelMetaRelationFieldCommon &
IModelMetaRelationEnumerationField; IModelMetaRelationEnumerationField;
interface IModelPrintMeta{
pageTitle: string;
}
export interface IModelMeta { export interface IModelMeta {
defaultFilterField: string; defaultFilterField: string;
defaultSort: IModelMetaDefaultSort; defaultSort: IModelMetaDefaultSort;
@@ -134,6 +138,8 @@ export interface IModelMeta {
importAggregateOn?: string; importAggregateOn?: string;
importAggregateBy?: string; importAggregateBy?: string;
print?: IModelPrintMeta;
fields: { [key: string]: IModelMetaField }; fields: { [key: string]: IModelMetaField };
columns: { [key: string]: IModelMetaColumn }; columns: { [key: string]: IModelMetaColumn };
} }

View File

@@ -11,6 +11,9 @@ export interface ISystemUser extends Model {
password: string; password: string;
email: string; email: string;
verifyToken: string;
verified: boolean;
roleId: number; roleId: number;
tenantId: number; tenantId: number;

View File

@@ -70,10 +70,7 @@ export class PlaidClientWrapper {
baseOptions: { baseOptions: {
headers: { headers: {
'PLAID-CLIENT-ID': config.plaid.clientId, 'PLAID-CLIENT-ID': config.plaid.clientId,
'PLAID-SECRET': 'PLAID-SECRET': config.plaid.secret,
config.plaid.env === 'development'
? config.plaid.secretDevelopment
: config.plaid.secretSandbox,
'Plaid-Version': '2020-09-14', 'Plaid-Version': '2020-09-14',
}, },
}, },

View File

@@ -8,6 +8,9 @@ export default {
}, },
importable: true, importable: true,
exportable: true, exportable: true,
print: {
pageTitle: 'Chart of Accounts',
},
fields: { fields: {
name: { name: {
name: 'account.field.name', name: 'account.field.name',
@@ -121,7 +124,7 @@ export default {
}, },
balance: { balance: {
name: 'account.field.balance', name: 'account.field.balance',
accessor: 'amount', accessor: 'formattedAmount',
}, },
description: { description: {
name: 'account.field.description', name: 'account.field.description',
@@ -133,6 +136,7 @@ export default {
}, },
createdAt: { createdAt: {
name: 'account.field.created_at', name: 'account.field.created_at',
printable: false,
}, },
}, },
fields2: { fields2: {

View File

@@ -10,6 +10,9 @@ export default {
importAggregator: 'group', importAggregator: 'group',
importAggregateOn: 'entries', importAggregateOn: 'entries',
importAggregateBy: 'billNumber', importAggregateBy: 'billNumber',
print: {
pageTitle: 'Bills',
},
fields: { fields: {
vendor: { vendor: {
name: 'bill.field.vendor', name: 'bill.field.vendor',
@@ -83,6 +86,10 @@ export default {
}, },
}, },
columns: { columns: {
billDate: {
name: 'Date',
accessor: 'formattedBillDate',
},
billNumber: { billNumber: {
name: 'Bill No.', name: 'Bill No.',
type: 'text', type: 'text',
@@ -91,13 +98,10 @@ export default {
name: 'Reference No.', name: 'Reference No.',
type: 'text', type: 'text',
}, },
billDate: {
name: 'Date',
type: 'date',
},
dueDate: { dueDate: {
name: 'Due Date', name: 'Due Date',
type: 'date', type: 'date',
accessor: 'formattedDueDate',
}, },
vendorId: { vendorId: {
name: 'Vendor', name: 'Vendor',
@@ -111,10 +115,12 @@ export default {
exchangeRate: { exchangeRate: {
name: 'Exchange Rate', name: 'Exchange Rate',
type: 'number', type: 'number',
printable: false,
}, },
currencyCode: { currencyCode: {
name: 'Currency Code', name: 'Currency Code',
type: 'text', type: 'text',
printable: false,
}, },
dueAmount: { dueAmount: {
name: 'Due Amount', name: 'Due Amount',
@@ -127,10 +133,12 @@ export default {
note: { note: {
name: 'Note', name: 'Note',
type: 'text', type: 'text',
printable: false,
}, },
open: { open: {
name: 'Open', name: 'Open',
type: 'boolean', type: 'boolean',
printable: false,
}, },
entries: { entries: {
name: 'Entries', name: 'Entries',

View File

@@ -77,6 +77,7 @@ export default {
paymentDate: { paymentDate: {
name: 'bill_payment.field.payment_date', name: 'bill_payment.field.payment_date',
type: 'date', type: 'date',
accessor: 'formattedPaymentDate'
}, },
paymentNumber: { paymentNumber: {
name: 'bill_payment.field.payment_number', name: 'bill_payment.field.payment_number',
@@ -94,14 +95,17 @@ export default {
currencyCode: { currencyCode: {
name: 'Currency Code', name: 'Currency Code',
type: 'text', type: 'text',
printable: false,
}, },
exchangeRate: { exchangeRate: {
name: 'bill_payment.field.exchange_rate', name: 'bill_payment.field.exchange_rate',
type: 'number', type: 'number',
printable: false,
}, },
statement: { statement: {
name: 'bill_payment.field.note', name: 'bill_payment.field.note',
type: 'text', type: 'text',
printable: false,
}, },
reference: { reference: {
name: 'bill_payment.field.reference', name: 'bill_payment.field.reference',

View File

@@ -20,6 +20,10 @@ export default {
importAggregateOn: 'entries', importAggregateOn: 'entries',
importAggregateBy: 'creditNoteNumber', importAggregateBy: 'creditNoteNumber',
print: {
pageTitle: 'Credit Notes',
},
fields: { fields: {
customer: { customer: {
name: 'credit_note.field.customer', name: 'credit_note.field.customer',
@@ -88,36 +92,34 @@ export default {
columns: { columns: {
customer: { customer: {
name: 'Customer', name: 'Customer',
type: 'relation',
accessor: 'customer.displayName', accessor: 'customer.displayName',
}, },
exchangeRate: { exchangeRate: {
name: 'Exchange Rate', name: 'Exchange Rate',
type: 'number', printable: false,
}, },
creditNoteDate: { creditNoteDate: {
name: 'Credit Note Date', name: 'Credit Note Date',
type: 'date', accessor: 'formattedCreditNoteDate'
}, },
referenceNo: { referenceNo: {
name: 'Reference No.', name: 'Reference No.',
type: 'text',
}, },
note: { note: {
name: 'Note', name: 'Note',
type: 'text',
}, },
termsConditions: { termsConditions: {
name: 'Terms & Conditions', name: 'Terms & Conditions',
type: 'text', printable: false,
}, },
creditNoteNumber: { creditNoteNumber: {
name: 'Credit Note Number', name: 'Credit Note Number',
type: 'text', printable: false,
}, },
open: { open: {
name: 'Open', name: 'Open',
type: 'boolean', type: 'boolean',
printable: false,
}, },
entries: { entries: {
name: 'Entries', name: 'Entries',

View File

@@ -6,6 +6,9 @@ export default {
sortOrder: 'DESC', sortOrder: 'DESC',
sortField: 'created_at', sortField: 'created_at',
}, },
print: {
pageTitle: 'Customers',
},
fields: { fields: {
first_name: { first_name: {
name: 'vendor.field.first_name', name: 'vendor.field.first_name',
@@ -127,100 +130,121 @@ export default {
balance: { balance: {
name: 'vendor.field.balance', name: 'vendor.field.balance',
type: 'number', type: 'number',
accessor: 'formattedBalance',
}, },
openingBalance: { openingBalance: {
name: 'vendor.field.opening_balance', name: 'vendor.field.opening_balance',
type: 'number', type: 'number',
printable: false
}, },
openingBalanceAt: { openingBalanceAt: {
name: 'vendor.field.opening_balance_at', name: 'vendor.field.opening_balance_at',
type: 'date', type: 'date',
printable: false
}, },
currencyCode: { currencyCode: {
name: 'vendor.field.currency', name: 'vendor.field.currency',
type: 'text', type: 'text',
printable: false
}, },
status: { status: {
name: 'vendor.field.status', name: 'vendor.field.status',
printable: false
}, },
note: { note: {
name: 'vendor.field.note', name: 'vendor.field.note',
printable: false
}, },
// Billing Address // Billing Address
billingAddress1: { billingAddress1: {
name: 'Billing Address 1', name: 'Billing Address 1',
column: 'billing_address1', column: 'billing_address1',
type: 'text', type: 'text',
printable: false
}, },
billingAddress2: { billingAddress2: {
name: 'Billing Address 2', name: 'Billing Address 2',
column: 'billing_address2', column: 'billing_address2',
type: 'text', type: 'text',
printable: false
}, },
billingAddressCity: { billingAddressCity: {
name: 'Billing Address City', name: 'Billing Address City',
column: 'billing_address_city', column: 'billing_address_city',
type: 'text', type: 'text',
printable: false
}, },
billingAddressCountry: { billingAddressCountry: {
name: 'Billing Address Country', name: 'Billing Address Country',
column: 'billing_address_country', column: 'billing_address_country',
type: 'text', type: 'text',
printable: false
}, },
billingAddressPostcode: { billingAddressPostcode: {
name: 'Billing Address Postcode', name: 'Billing Address Postcode',
column: 'billing_address_postcode', column: 'billing_address_postcode',
type: 'text', type: 'text',
printable: false
}, },
billingAddressState: { billingAddressState: {
name: 'Billing Address State', name: 'Billing Address State',
column: 'billing_address_state', column: 'billing_address_state',
type: 'text', type: 'text',
printable: false
}, },
billingAddressPhone: { billingAddressPhone: {
name: 'Billing Address Phone', name: 'Billing Address Phone',
column: 'billing_address_phone', column: 'billing_address_phone',
type: 'text', type: 'text',
printable: false
}, },
// Shipping Address // Shipping Address
shippingAddress1: { shippingAddress1: {
name: 'Shipping Address 1', name: 'Shipping Address 1',
column: 'shipping_address1', column: 'shipping_address1',
type: 'text', type: 'text',
printable: false
}, },
shippingAddress2: { shippingAddress2: {
name: 'Shipping Address 2', name: 'Shipping Address 2',
column: 'shipping_address2', column: 'shipping_address2',
type: 'text', type: 'text',
printable: false
}, },
shippingAddressCity: { shippingAddressCity: {
name: 'Shipping Address City', name: 'Shipping Address City',
column: 'shipping_address_city', column: 'shipping_address_city',
type: 'text', type: 'text',
printable: false
}, },
shippingAddressCountry: { shippingAddressCountry: {
name: 'Shipping Address Country', name: 'Shipping Address Country',
column: 'shipping_address_country', column: 'shipping_address_country',
type: 'text', type: 'text',
printable: false
}, },
shippingAddressPostcode: { shippingAddressPostcode: {
name: 'Shipping Address Postcode', name: 'Shipping Address Postcode',
column: 'shipping_address_postcode', column: 'shipping_address_postcode',
type: 'text', type: 'text',
printable: false
}, },
shippingAddressPhone: { shippingAddressPhone: {
name: 'Shipping Address Phone', name: 'Shipping Address Phone',
column: 'shipping_address_phone', column: 'shipping_address_phone',
type: 'text', type: 'text',
printable: false
}, },
shippingAddressState: { shippingAddressState: {
name: 'Shipping Address State', name: 'Shipping Address State',
column: 'shipping_address_state', column: 'shipping_address_state',
type: 'text', type: 'text',
printable: false
}, },
createdAt: { createdAt: {
name: 'vendor.field.created_at', name: 'vendor.field.created_at',
type: 'date', type: 'date',
printable: false
}, },
}, },
fields2: { fields2: {

View File

@@ -10,6 +10,9 @@ export default {
importable: true, importable: true,
exportFlattenOn: 'categories', exportFlattenOn: 'categories',
exportable: true, exportable: true,
print: {
pageTitle: 'Expenses',
},
fields: { fields: {
payment_date: { payment_date: {
name: 'expense.field.payment_date', name: 'expense.field.payment_date',
@@ -67,7 +70,7 @@ export default {
paymentReceive: { paymentReceive: {
name: 'expense.field.payment_account', name: 'expense.field.payment_account',
type: 'text', type: 'text',
accessor: 'paymentAccount.name' accessor: 'paymentAccount.name',
}, },
referenceNo: { referenceNo: {
name: 'expense.field.reference_no', name: 'expense.field.reference_no',
@@ -75,15 +78,18 @@ export default {
}, },
paymentDate: { paymentDate: {
name: 'expense.field.payment_date', name: 'expense.field.payment_date',
accessor: 'formattedDate',
type: 'date', type: 'date',
}, },
currencyCode: { currencyCode: {
name: 'expense.field.currency_code', name: 'expense.field.currency_code',
type: 'text', type: 'text',
printable: false,
}, },
exchangeRate: { exchangeRate: {
name: 'expense.field.exchange_rate', name: 'expense.field.exchange_rate',
type: 'number', type: 'number',
printable: false,
}, },
description: { description: {
name: 'expense.field.description', name: 'expense.field.description',
@@ -111,6 +117,7 @@ export default {
publish: { publish: {
name: 'expense.field.publish', name: 'expense.field.publish',
type: 'boolean', type: 'boolean',
printable: false,
}, },
}, },
fields2: { fields2: {

View File

@@ -6,6 +6,9 @@ export default {
sortField: 'name', sortField: 'name',
sortOrder: 'DESC', sortOrder: 'DESC',
}, },
print: {
pageTitle: 'Items',
},
fields: { fields: {
type: { type: {
name: 'item.field.type', name: 'item.field.type',
@@ -127,6 +130,7 @@ export default {
name: 'item.field.type', name: 'item.field.type',
type: 'text', type: 'text',
exportable: true, exportable: true,
accessor: 'typeFormatted',
}, },
name: { name: {
name: 'item.field.name', name: 'item.field.name',
@@ -142,11 +146,13 @@ export default {
name: 'item.field.sellable', name: 'item.field.sellable',
type: 'boolean', type: 'boolean',
exportable: true, exportable: true,
printable: false,
}, },
purchasable: { purchasable: {
name: 'item.field.purchasable', name: 'item.field.purchasable',
type: 'boolean', type: 'boolean',
exportable: true, exportable: true,
printable: false,
}, },
sellPrice: { sellPrice: {
name: 'item.field.cost_price', name: 'item.field.cost_price',
@@ -163,12 +169,14 @@ export default {
type: 'text', type: 'text',
accessor: 'costAccount.name', accessor: 'costAccount.name',
exportable: true, exportable: true,
printable: false,
}, },
sellAccount: { sellAccount: {
name: 'item.field.sell_description', name: 'item.field.sell_description',
type: 'text', type: 'text',
accessor: 'sellAccount.name', accessor: 'sellAccount.name',
exportable: true, exportable: true,
printable: false,
}, },
inventoryAccount: { inventoryAccount: {
name: 'item.field.inventory_account', name: 'item.field.inventory_account',
@@ -180,11 +188,13 @@ export default {
name: 'Sell description', name: 'Sell description',
type: 'text', type: 'text',
exportable: true, exportable: true,
printable: false,
}, },
purchaseDescription: { purchaseDescription: {
name: 'Purchase description', name: 'Purchase description',
type: 'text', type: 'text',
exportable: true, exportable: true,
printable: false,
}, },
quantityOnHand: { quantityOnHand: {
name: 'item.field.quantity_on_hand', name: 'item.field.quantity_on_hand',
@@ -206,11 +216,13 @@ export default {
name: 'item.field.active', name: 'item.field.active',
fieldType: 'boolean', fieldType: 'boolean',
exportable: true, exportable: true,
printable: false,
}, },
createdAt: { createdAt: {
name: 'item.field.created_at', name: 'item.field.created_at',
type: 'date', type: 'date',
exportable: true, exportable: true,
printable: false,
}, },
}, },
fields2: { fields2: {

View File

@@ -11,6 +11,11 @@ export default {
importAggregator: 'group', importAggregator: 'group',
importAggregateOn: 'entries', importAggregateOn: 'entries',
importAggregateBy: 'journalNumber', importAggregateBy: 'journalNumber',
print: {
pageTitle: 'Manual Journals',
},
fields: { fields: {
date: { date: {
name: 'manual_journal.field.date', name: 'manual_journal.field.date',
@@ -63,6 +68,7 @@ export default {
date: { date: {
name: 'manual_journal.field.date', name: 'manual_journal.field.date',
type: 'date', type: 'date',
accessor: 'formattedDate',
}, },
journalNumber: { journalNumber: {
name: 'manual_journal.field.journal_number', name: 'manual_journal.field.journal_number',
@@ -83,10 +89,12 @@ export default {
currencyCode: { currencyCode: {
name: 'manual_journal.field.currency', name: 'manual_journal.field.currency',
type: 'text', type: 'text',
printable: false,
}, },
exchangeRate: { exchangeRate: {
name: 'manual_journal.field.exchange_rate', name: 'manual_journal.field.exchange_rate',
type: 'number', type: 'number',
printable: false,
}, },
description: { description: {
name: 'manual_journal.field.description', name: 'manual_journal.field.description',
@@ -120,13 +128,17 @@ export default {
publish: { publish: {
name: 'Publish', name: 'Publish',
type: 'boolean', type: 'boolean',
printable: false,
}, },
publishedAt: { publishedAt: {
name: 'Published At', name: 'Published At',
printable: false,
}, },
}, },
createdAt: { createdAt: {
name: 'Created At', name: 'Created At',
accessor: 'formattedCreatedAt',
printable: false,
}, },
}, },
fields2: { fields2: {

View File

@@ -67,10 +67,12 @@ export default {
paymentDate: { paymentDate: {
name: 'payment_receive.field.payment_date', name: 'payment_receive.field.payment_date',
type: 'date', type: 'date',
accessor: 'formattedPaymentDate',
}, },
amount: { amount: {
name: 'payment_receive.field.amount', name: 'payment_receive.field.amount',
type: 'number', type: 'number',
accessor: 'formattedAmount'
}, },
referenceNo: { referenceNo: {
name: 'payment_receive.field.reference_no', name: 'payment_receive.field.reference_no',
@@ -88,10 +90,12 @@ export default {
statement: { statement: {
name: 'payment_receive.field.statement', name: 'payment_receive.field.statement',
type: 'text', type: 'text',
printable: false,
}, },
created_at: { created_at: {
name: 'payment_receive.field.created_at', name: 'payment_receive.field.created_at',
type: 'date', type: 'date',
printable: false,
}, },
}, },
fields2: { fields2: {

View File

@@ -11,6 +11,11 @@ export default {
importAggregator: 'group', importAggregator: 'group',
importAggregateOn: 'entries', importAggregateOn: 'entries',
importAggregateBy: 'estimateNumber', importAggregateBy: 'estimateNumber',
print: {
pageTitle: 'Sale Estimates'
},
fields: { fields: {
amount: { amount: {
name: 'estimate.field.amount', name: 'estimate.field.amount',
@@ -86,11 +91,13 @@ export default {
estimateDate: { estimateDate: {
name: 'Estimate Date', name: 'Estimate Date',
type: 'date', type: 'date',
accessor: 'formattedEstimateDate',
exportable: true, exportable: true,
}, },
expirationDate: { expirationDate: {
name: 'Expiration Date', name: 'Expiration Date',
type: 'date', type: 'date',
accessor: 'formattedExpirationDate',
exportable: true, exportable: true,
}, },
estimateNumber: { estimateNumber: {
@@ -112,26 +119,31 @@ export default {
name: 'Exchange Rate', name: 'Exchange Rate',
type: 'number', type: 'number',
exportable: true, exportable: true,
printable: false,
}, },
currencyCode: { currencyCode: {
name: 'Currency', name: 'Currency',
type: 'text', type: 'text',
exportable: true, exportable: true,
printable: false,
}, },
note: { note: {
name: 'Note', name: 'Note',
type: 'text', type: 'text',
exportable: true, exportable: true,
printable: false,
}, },
termsConditions: { termsConditions: {
name: 'Terms & Conditions', name: 'Terms & Conditions',
type: 'text', type: 'text',
exportable: true, exportable: true,
printable: false,
}, },
delivered: { delivered: {
name: 'Delivered', name: 'Delivered',
type: 'boolean', type: 'boolean',
exportable: true, exportable: true,
printable: false,
}, },
entries: { entries: {
name: 'Entries', name: 'Entries',
@@ -153,6 +165,7 @@ export default {
}, },
description: { description: {
name: 'Item Description', name: 'Item Description',
printable: false,
}, },
amount: { amount: {
name: 'Item Amount', name: 'Item Amount',

View File

@@ -11,6 +11,10 @@ export default {
importAggregator: 'group', importAggregator: 'group',
importAggregateOn: 'entries', importAggregateOn: 'entries',
importAggregateBy: 'invoiceNo', importAggregateBy: 'invoiceNo',
print: {
pageTitle: 'Sale invoices',
},
fields: { fields: {
customer: { customer: {
name: 'invoice.field.customer', name: 'invoice.field.customer',
@@ -94,10 +98,12 @@ export default {
invoiceDate: { invoiceDate: {
name: 'invoice.field.invoice_date', name: 'invoice.field.invoice_date',
type: 'date', type: 'date',
accessor: 'invoiceDateFormatted',
}, },
dueDate: { dueDate: {
name: 'invoice.field.due_date', name: 'invoice.field.due_date',
type: 'date', type: 'date',
accessor: 'dueDateFormatted',
}, },
referenceNo: { referenceNo: {
name: 'invoice.field.reference_no', name: 'invoice.field.reference_no',
@@ -120,10 +126,12 @@ export default {
exchangeRate: { exchangeRate: {
name: 'invoice.field.exchange_rate', name: 'invoice.field.exchange_rate',
type: 'number', type: 'number',
printable: false,
}, },
currencyCode: { currencyCode: {
name: 'invoice.field.currency', name: 'invoice.field.currency',
type: 'text', type: 'text',
printable: false,
}, },
paidAmount: { paidAmount: {
name: 'Paid Amount', name: 'Paid Amount',
@@ -136,14 +144,17 @@ export default {
invoiceMessage: { invoiceMessage: {
name: 'invoice.field.invoice_message', name: 'invoice.field.invoice_message',
type: 'text', type: 'text',
printable: false,
}, },
termsConditions: { termsConditions: {
name: 'invoice.field.terms_conditions', name: 'invoice.field.terms_conditions',
type: 'text', type: 'text',
printable: false,
}, },
delivered: { delivered: {
name: 'invoice.field.delivered', name: 'invoice.field.delivered',
type: 'boolean', type: 'boolean',
printable: false,
}, },
entries: { entries: {
name: 'Entries', name: 'Entries',
@@ -165,6 +176,7 @@ export default {
}, },
description: { description: {
name: 'Item Description', name: 'Item Description',
printable: false,
}, },
amount: { amount: {
name: 'Item Amount', name: 'Item Amount',
@@ -202,18 +214,22 @@ export default {
exchangeRate: { exchangeRate: {
name: 'invoice.field.exchange_rate', name: 'invoice.field.exchange_rate',
fieldType: 'number', fieldType: 'number',
printable: false,
}, },
currencyCode: { currencyCode: {
name: 'invoice.field.currency', name: 'invoice.field.currency',
fieldType: 'text', fieldType: 'text',
printable: false,
}, },
invoiceMessage: { invoiceMessage: {
name: 'invoice.field.invoice_message', name: 'invoice.field.invoice_message',
fieldType: 'text', fieldType: 'text',
printable: false,
}, },
termsConditions: { termsConditions: {
name: 'invoice.field.terms_conditions', name: 'invoice.field.terms_conditions',
fieldType: 'text', fieldType: 'text',
printable: false,
}, },
entries: { entries: {
name: 'invoice.field.entries', name: 'invoice.field.entries',
@@ -249,6 +265,7 @@ export default {
delivered: { delivered: {
name: 'invoice.field.delivered', name: 'invoice.field.delivered',
fieldType: 'boolean', fieldType: 'boolean',
printable: false,
}, },
}, },
}; };

View File

@@ -11,6 +11,10 @@ export default {
importAggregator: 'group', importAggregator: 'group',
importAggregateOn: 'entries', importAggregateOn: 'entries',
importAggregateBy: 'receiptNumber', importAggregateBy: 'receiptNumber',
print: {
pageTitle: 'Sale Receipts',
},
fields: { fields: {
amount: { amount: {
name: 'receipt.field.amount', name: 'receipt.field.amount',
@@ -81,11 +85,6 @@ export default {
}, },
}, },
columns: { columns: {
amount: {
name: 'receipt.field.amount',
column: 'amount',
type: 'number',
},
depositAccount: { depositAccount: {
name: 'receipt.field.deposit_account', name: 'receipt.field.deposit_account',
type: 'text', type: 'text',
@@ -98,6 +97,7 @@ export default {
}, },
receiptDate: { receiptDate: {
name: 'receipt.field.receipt_date', name: 'receipt.field.receipt_date',
accessor: 'formattedReceiptDate',
type: 'date', type: 'date',
}, },
receiptNumber: { receiptNumber: {
@@ -114,10 +114,17 @@ export default {
name: 'receipt.field.receipt_message', name: 'receipt.field.receipt_message',
column: 'receipt_message', column: 'receipt_message',
type: 'text', type: 'text',
printable: false,
},
amount: {
name: 'receipt.field.amount',
accessor: 'formattedAmount',
type: 'number',
}, },
statement: { statement: {
name: 'receipt.field.statement', name: 'receipt.field.statement',
type: 'text', type: 'text',
printable: false,
}, },
status: { status: {
name: 'receipt.field.status', name: 'receipt.field.status',
@@ -127,6 +134,7 @@ export default {
{ key: 'closed', label: 'receipt.field.status.closed' }, { key: 'closed', label: 'receipt.field.status.closed' },
], ],
exportable: true, exportable: true,
printable: false,
}, },
entries: { entries: {
name: 'Entries', name: 'Entries',
@@ -148,6 +156,7 @@ export default {
}, },
description: { description: {
name: 'Item Description', name: 'Item Description',
printable: false,
}, },
amount: { amount: {
name: 'Item Amount', name: 'Item Amount',
@@ -158,6 +167,7 @@ export default {
createdAt: { createdAt: {
name: 'receipt.field.created_at', name: 'receipt.field.created_at',
type: 'date', type: 'date',
printable: false,
}, },
}, },
fields2: { fields2: {

View File

@@ -104,10 +104,10 @@ export default class UncategorizedCashflowTransaction extends mixin(
*/ */
private async updateUncategorizedTransactionCount( private async updateUncategorizedTransactionCount(
queryContext: QueryContext, queryContext: QueryContext,
increment: boolean increment: boolean,
amount: number = 1
) { ) {
const operation = increment ? 'increment' : 'decrement'; const operation = increment ? 'increment' : 'decrement';
const amount = increment ? 1 : -1;
await Account.query(queryContext.transaction) await Account.query(queryContext.transaction)
.findById(this.accountId) .findById(this.accountId)

View File

@@ -131,21 +131,26 @@ export default {
openingBalance: { openingBalance: {
name: 'vendor.field.opening_balance', name: 'vendor.field.opening_balance',
type: 'number', type: 'number',
printable: false
}, },
openingBalanceAt: { openingBalanceAt: {
name: 'vendor.field.opening_balance_at', name: 'vendor.field.opening_balance_at',
type: 'date', type: 'date',
printable: false
}, },
currencyCode: { currencyCode: {
name: 'vendor.field.currency', name: 'vendor.field.currency',
type: 'text', type: 'text',
printable: false
}, },
status: { status: {
name: 'vendor.field.status', name: 'vendor.field.status',
printable: false
}, },
note: { note: {
name: 'vendor.field.note', name: 'vendor.field.note',
type: 'text', type: 'text',
printable: false
}, },
// Billing Address // Billing Address
billingAddress1: { billingAddress1: {
@@ -153,42 +158,49 @@ export default {
column: 'billing_address1', column: 'billing_address1',
type: 'text', type: 'text',
exportable: true, exportable: true,
printable: false
}, },
billingAddress2: { billingAddress2: {
name: 'Billing Address 2', name: 'Billing Address 2',
column: 'billing_address2', column: 'billing_address2',
type: 'text', type: 'text',
exportable: true, exportable: true,
printable: false
}, },
billingAddressCity: { billingAddressCity: {
name: 'Billing Address City', name: 'Billing Address City',
column: 'billing_address_city', column: 'billing_address_city',
type: 'text', type: 'text',
exportable: true, exportable: true,
printable: false
}, },
billingAddressCountry: { billingAddressCountry: {
name: 'Billing Address Country', name: 'Billing Address Country',
column: 'billing_address_country', column: 'billing_address_country',
type: 'text', type: 'text',
exportable: true, exportable: true,
printable: false
}, },
billingAddressPostcode: { billingAddressPostcode: {
name: 'Billing Address Postcode', name: 'Billing Address Postcode',
column: 'billing_address_postcode', column: 'billing_address_postcode',
type: 'text', type: 'text',
exportable: true, exportable: true,
printable: false
}, },
billingAddressState: { billingAddressState: {
name: 'Billing Address State', name: 'Billing Address State',
column: 'billing_address_state', column: 'billing_address_state',
type: 'text', type: 'text',
exportable: true, exportable: true,
printable: false
}, },
billingAddressPhone: { billingAddressPhone: {
name: 'Billing Address Phone', name: 'Billing Address Phone',
column: 'billing_address_phone', column: 'billing_address_phone',
type: 'text', type: 'text',
exportable: true, exportable: true,
printable: false
}, },
// Shipping Address // Shipping Address
shippingAddress1: { shippingAddress1: {
@@ -196,47 +208,55 @@ export default {
column: 'shipping_address1', column: 'shipping_address1',
type: 'text', type: 'text',
exportable: true, exportable: true,
printable: false
}, },
shippingAddress2: { shippingAddress2: {
name: 'Shipping Address 2', name: 'Shipping Address 2',
column: 'shipping_address2', column: 'shipping_address2',
type: 'text', type: 'text',
exportable: true, exportable: true,
printable: false
}, },
shippingAddressCity: { shippingAddressCity: {
name: 'Shipping Address City', name: 'Shipping Address City',
column: 'shipping_address_city', column: 'shipping_address_city',
type: 'text', type: 'text',
exportable: true, exportable: true,
printable: false
}, },
shippingAddressCountry: { shippingAddressCountry: {
name: 'Shipping Address Country', name: 'Shipping Address Country',
column: 'shipping_address_country', column: 'shipping_address_country',
type: 'text', type: 'text',
exportable: true, exportable: true,
printable: false
}, },
shippingAddressPostcode: { shippingAddressPostcode: {
name: 'Shipping Address Postcode', name: 'Shipping Address Postcode',
column: 'shipping_address_postcode', column: 'shipping_address_postcode',
type: 'text', type: 'text',
exportable: true, exportable: true,
printable: false
}, },
shippingAddressState: { shippingAddressState: {
name: 'Shipping Address State', name: 'Shipping Address State',
column: 'shipping_address_state', column: 'shipping_address_state',
type: 'text', type: 'text',
exportable: true, exportable: true,
printable: false
}, },
shippingAddressPhone: { shippingAddressPhone: {
name: 'Shipping Address Phone', name: 'Shipping Address Phone',
column: 'shipping_address_phone', column: 'shipping_address_phone',
type: 'text', type: 'text',
exportable: true, exportable: true,
printable: false
}, },
createdAt: { createdAt: {
name: 'vendor.field.created_at', name: 'vendor.field.created_at',
type: 'date', type: 'date',
exportable: true, exportable: true,
printable: false
}, },
}, },
fields2: { fields2: {

View File

@@ -20,6 +20,9 @@ export default {
importAggregateOn: 'entries', importAggregateOn: 'entries',
importAggregateBy: 'vendorCreditNumber', importAggregateBy: 'vendorCreditNumber',
print: {
pageTitle: 'Vendor Credits',
},
fields: { fields: {
vendor: { vendor: {
name: 'vendor_credit.field.vendor', name: 'vendor_credit.field.vendor',
@@ -89,6 +92,7 @@ export default {
exchangeRate: { exchangeRate: {
name: 'Echange Rate', name: 'Echange Rate',
type: 'text', type: 'text',
printable: false,
}, },
vendorCreditNumber: { vendorCreditNumber: {
name: 'Vendor Credit No.', name: 'Vendor Credit No.',
@@ -100,7 +104,7 @@ export default {
}, },
vendorCreditDate: { vendorCreditDate: {
name: 'Vendor Credit Date', name: 'Vendor Credit Date',
type: 'date', accessor: 'formattedVendorCreditDate',
}, },
amount: { amount: {
name: 'Amount', name: 'Amount',
@@ -109,10 +113,12 @@ export default {
creditRemaining: { creditRemaining: {
name: 'Credits Remaining', name: 'Credits Remaining',
accessor: 'formattedCreditsRemaining', accessor: 'formattedCreditsRemaining',
printable: false,
}, },
refundedAmount: { refundedAmount: {
name: 'Refunded Amount', name: 'Refunded Amount',
accessor: 'refundedAmount', accessor: 'refundedAmount',
printable: false,
}, },
invoicedAmount: { invoicedAmount: {
name: 'Invoiced Amount', name: 'Invoiced Amount',
@@ -121,10 +127,12 @@ export default {
note: { note: {
name: 'Note', name: 'Note',
type: 'text', type: 'text',
printable: false,
}, },
open: { open: {
name: 'Open', name: 'Open',
type: 'boolean', type: 'boolean',
printable: false,
}, },
entries: { entries: {
name: 'Entries', name: 'Entries',

View File

@@ -7,6 +7,7 @@ import {
IAccountEventCreatedPayload, IAccountEventCreatedPayload,
IAccountEventCreatingPayload, IAccountEventCreatingPayload,
IAccountCreateDTO, IAccountCreateDTO,
CreateAccountParams,
} from '@/interfaces'; } from '@/interfaces';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import UnitOfWork from '@/services/UnitOfWork'; import UnitOfWork from '@/services/UnitOfWork';
@@ -30,19 +31,22 @@ export class CreateAccount {
/** /**
* Authorize the account creation. * Authorize the account creation.
* @param {number} tenantId * @param {number} tenantId
* @param {IAccountCreateDTO} accountDTO * @param {IAccountCreateDTO} accountDTO
*/ */
private authorize = async ( private authorize = async (
tenantId: number, tenantId: number,
accountDTO: IAccountCreateDTO, accountDTO: IAccountCreateDTO,
baseCurrency: string baseCurrency: string,
params?: CreateAccountParams
) => { ) => {
// Validate account name uniquiness. // Validate account name uniquiness.
await this.validator.validateAccountNameUniquiness( if (!params.ignoreUniqueName) {
tenantId, await this.validator.validateAccountNameUniquiness(
accountDTO.name tenantId,
); accountDTO.name
);
}
// Validate the account code uniquiness. // Validate the account code uniquiness.
if (accountDTO.code) { if (accountDTO.code) {
await this.validator.isAccountCodeUniqueOrThrowError( await this.validator.isAccountCodeUniqueOrThrowError(
@@ -82,7 +86,7 @@ export class CreateAccount {
/** /**
* Transformes the create account DTO to input model. * Transformes the create account DTO to input model.
* @param {IAccountCreateDTO} createAccountDTO * @param {IAccountCreateDTO} createAccountDTO
*/ */
private transformDTOToModel = ( private transformDTOToModel = (
createAccountDTO: IAccountCreateDTO, createAccountDTO: IAccountCreateDTO,
@@ -104,7 +108,8 @@ export class CreateAccount {
public createAccount = async ( public createAccount = async (
tenantId: number, tenantId: number,
accountDTO: IAccountCreateDTO, accountDTO: IAccountCreateDTO,
trx?: Knex.Transaction trx?: Knex.Transaction,
params: CreateAccountParams = { ignoreUniqueName: false }
): Promise<IAccount> => { ): Promise<IAccount> => {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);
@@ -112,8 +117,12 @@ export class CreateAccount {
const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
// Authorize the account creation. // Authorize the account creation.
await this.authorize(tenantId, accountDTO, tenantMeta.baseCurrency); await this.authorize(
tenantId,
accountDTO,
tenantMeta.baseCurrency,
params
);
// Transformes the DTO to model. // Transformes the DTO to model.
const accountInputModel = this.transformDTOToModel( const accountInputModel = this.transformDTOToModel(
accountDTO, accountDTO,
@@ -148,3 +157,4 @@ export class CreateAccount {
); );
}; };
} }

View File

@@ -2,11 +2,9 @@ import { Inject, Service } from 'typedi';
import { UploadDocument } from './UploadDocument'; import { UploadDocument } from './UploadDocument';
import { DeleteAttachment } from './DeleteAttachment'; import { DeleteAttachment } from './DeleteAttachment';
import { GetAttachment } from './GetAttachment'; import { GetAttachment } from './GetAttachment';
import { AttachmentUploadPipeline } from './S3UploadPipeline';
import { LinkAttachment } from './LinkAttachment'; import { LinkAttachment } from './LinkAttachment';
import { UnlinkAttachment } from './UnlinkAttachment'; import { UnlinkAttachment } from './UnlinkAttachment';
import { getAttachmentPresignedUrl } from './GetAttachmentPresignedUrl'; import { getAttachmentPresignedUrl } from './GetAttachmentPresignedUrl';
import type { Multer } from 'multer';
@Service() @Service()
export class AttachmentsApplication { export class AttachmentsApplication {
@@ -19,9 +17,6 @@ export class AttachmentsApplication {
@Inject() @Inject()
private getDocumentService: GetAttachment; private getDocumentService: GetAttachment;
@Inject()
private uploadPipelineService: AttachmentUploadPipeline;
@Inject() @Inject()
private linkDocumentService: LinkAttachment; private linkDocumentService: LinkAttachment;
@@ -31,14 +26,6 @@ export class AttachmentsApplication {
@Inject() @Inject()
private getPresignedUrlService: getAttachmentPresignedUrl; private getPresignedUrlService: getAttachmentPresignedUrl;
/**
* Express middleware for uploading attachments to an S3 bucket.
* @returns {Multer}
*/
get uploadPipeline(): Multer {
return this.uploadPipelineService.uploadPipeline();
}
/** /**
* Saves the metadata of uploaded document to S3 on database. * Saves the metadata of uploaded document to S3 on database.
* @param {number} tenantId * @param {number} tenantId

View File

@@ -1,13 +1,19 @@
import { DeleteObjectCommand } from '@aws-sdk/client-s3'; import { DeleteObjectCommand } from '@aws-sdk/client-s3';
import { s3 } from '@/lib/S3/S3';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { s3 } from '@/lib/S3/S3';
import HasTenancyService from '../Tenancy/TenancyService'; import HasTenancyService from '../Tenancy/TenancyService';
import config from '@/config';
import UnitOfWork from '../UnitOfWork';
import { Knex } from 'knex';
@Service() @Service()
export class DeleteAttachment { export class DeleteAttachment {
@Inject() @Inject()
private tenancy: HasTenancyService; private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
/** /**
* Deletes the give file attachment file key. * Deletes the give file attachment file key.
* @param {number} tenantId * @param {number} tenantId
@@ -17,7 +23,7 @@ export class DeleteAttachment {
const { Document, DocumentLink } = this.tenancy.models(tenantId); const { Document, DocumentLink } = this.tenancy.models(tenantId);
const params = { const params = {
Bucket: process.env.AWS_BUCKET, Bucket: config.s3.bucket,
Key: filekey, Key: filekey,
}; };
await s3.send(new DeleteObjectCommand(params)); await s3.send(new DeleteObjectCommand(params));
@@ -26,10 +32,14 @@ export class DeleteAttachment {
.findOne('key', filekey) .findOne('key', filekey)
.throwIfNotFound(); .throwIfNotFound();
// Delete all document links await this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
await DocumentLink.query().where('documentId', foundDocument.id).delete(); // Delete all document links
await DocumentLink.query(trx)
.where('documentId', foundDocument.id)
.delete();
// Delete thedocument. // Delete thedocument.
await Document.query().findById(foundDocument.id).delete(); await Document.query(trx).findById(foundDocument.id).delete();
});
} }
} }

View File

@@ -1,6 +1,7 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import { s3 } from '@/lib/S3/S3';
import { GetObjectCommand } from '@aws-sdk/client-s3'; import { GetObjectCommand } from '@aws-sdk/client-s3';
import { s3 } from '@/lib/S3/S3';
import config from '@/config';
@Service() @Service()
export class GetAttachment { export class GetAttachment {
@@ -11,7 +12,7 @@ export class GetAttachment {
*/ */
async getAttachment(tenantId: number, filekey: string) { async getAttachment(tenantId: number, filekey: string) {
const params = { const params = {
Bucket: process.env.AWS_BUCKET, Bucket: config.s3.bucket,
Key: filekey, Key: filekey,
}; };
const data = await s3.send(new GetObjectCommand(params)); const data = await s3.send(new GetObjectCommand(params));

View File

@@ -1,7 +1,8 @@
import { Service } from 'typedi';
import { GetObjectCommand } from '@aws-sdk/client-s3'; import { GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { s3 } from '@/lib/S3/S3'; import { s3 } from '@/lib/S3/S3';
import { Service } from 'typedi'; import config from '@/config';
@Service() @Service()
export class getAttachmentPresignedUrl { export class getAttachmentPresignedUrl {
@@ -11,13 +12,8 @@ export class getAttachmentPresignedUrl {
* @returns {Promise<string?>} * @returns {Promise<string?>}
*/ */
async getPresignedUrl(key: string) { async getPresignedUrl(key: string) {
const params = {
Bucket: process.env.AWS_BUCKET,
Key: key,
Expires: 60 * 5, // 5 minutes
};
const command = new GetObjectCommand({ const command = new GetObjectCommand({
Bucket: process.env.AWS_BUCKET, Bucket: config.s3.bucket,
Key: key, Key: key,
}); });
const signedUrl = await getSignedUrl(s3, command, { expiresIn: 300 }); const signedUrl = await getSignedUrl(s3, command, { expiresIn: 300 });

View File

@@ -1,12 +1,38 @@
import multer from 'multer'; import multer from 'multer';
import type { Multer } from 'multer' import type { Multer } from 'multer';
import multerS3 from 'multer-s3'; import multerS3 from 'multer-s3';
import { s3 } from '@/lib/S3/S3'; import { s3 } from '@/lib/S3/S3';
import { Service } from 'typedi'; import { Service } from 'typedi';
import config from '@/config'; import config from '@/config';
import { NextFunction, Request, Response } from 'express';
@Service() @Service()
export class AttachmentUploadPipeline { export class AttachmentUploadPipeline {
/**
* Middleware to ensure that S3 configuration is properly set before proceeding.
* This function checks if the necessary S3 configuration keys are present and throws an error if any are missing.
*
* @param req The HTTP request object.
* @param res The HTTP response object.
* @param next The callback to pass control to the next middleware function.
*/
public validateS3Configured(req: Request, res: Response, next: NextFunction) {
if (
!config.s3.region ||
!config.s3.accessKeyId ||
!config.s3.secretAccessKey
) {
const missingKeys = [];
if (!config.s3.region) missingKeys.push('region');
if (!config.s3.accessKeyId) missingKeys.push('accessKeyId');
if (!config.s3.secretAccessKey) missingKeys.push('secretAccessKey');
const missing = missingKeys.join(', ');
throw new Error(`S3 configuration error: Missing ${missing}`);
}
next();
}
/** /**
* Express middleware for uploading attachments to an S3 bucket. * Express middleware for uploading attachments to an S3 bucket.
* It utilizes the multer middleware for handling multipart/form-data, specifically for file uploads. * It utilizes the multer middleware for handling multipart/form-data, specifically for file uploads.

View File

@@ -20,6 +20,10 @@ export class SendVerfiyMailOnSignUp {
private handleSendVerifyMailOnSignup = async ({ private handleSendVerifyMailOnSignup = async ({
user, user,
}: IAuthSignedUpEventPayload) => { }: IAuthSignedUpEventPayload) => {
// Can't continue if the user is verified.
if (user.verified) {
return;
}
const payload = { const payload = {
email: user.email, email: user.email,
token: user.verifyToken, token: user.verifyToken,

View File

@@ -3,7 +3,11 @@ import { Inject, Service } from 'typedi';
import bluebird from 'bluebird'; import bluebird from 'bluebird';
import { entries, groupBy } from 'lodash'; import { entries, groupBy } from 'lodash';
import { CreateAccount } from '@/services/Accounts/CreateAccount'; import { CreateAccount } from '@/services/Accounts/CreateAccount';
import { PlaidAccount, PlaidTransaction } from '@/interfaces'; import {
IAccountCreateDTO,
PlaidAccount,
PlaidTransaction,
} from '@/interfaces';
import { import {
transformPlaidAccountToCreateAccount, transformPlaidAccountToCreateAccount,
transformPlaidTrxsToCashflowCreate, transformPlaidTrxsToCashflowCreate,
@@ -11,6 +15,7 @@ import {
import { DeleteCashflowTransaction } from '@/services/Cashflow/DeleteCashflowTransactionService'; import { DeleteCashflowTransaction } from '@/services/Cashflow/DeleteCashflowTransactionService';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication'; import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
import { Knex } from 'knex';
const CONCURRENCY_ASYNC = 10; const CONCURRENCY_ASYNC = 10;
@@ -28,6 +33,35 @@ export class PlaidSyncDb {
@Inject() @Inject()
private deleteCashflowTransactionService: DeleteCashflowTransaction; private deleteCashflowTransactionService: DeleteCashflowTransaction;
/**
* Syncs the Plaid bank account.
* @param {number} tenantId
* @param {IAccountCreateDTO} createBankAccountDTO
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public async syncBankAccount(
tenantId: number,
createBankAccountDTO: IAccountCreateDTO,
trx?: Knex.Transaction
) {
const { Account } = this.tenancy.models(tenantId);
const plaidAccount = await Account.query().findOne(
'plaidAccountId',
createBankAccountDTO.plaidAccountId
);
// Can't continue if the Plaid account is already created.
if (plaidAccount) {
return;
}
await this.createAccountService.createAccount(
tenantId,
createBankAccountDTO,
trx,
{ ignoreUniqueName: true }
);
}
/** /**
* Syncs the plaid accounts to the system accounts. * Syncs the plaid accounts to the system accounts.
* @param {number} tenantId Tenant ID. * @param {number} tenantId Tenant ID.
@@ -37,7 +71,8 @@ export class PlaidSyncDb {
public async syncBankAccounts( public async syncBankAccounts(
tenantId: number, tenantId: number,
plaidAccounts: PlaidAccount[], plaidAccounts: PlaidAccount[],
institution: any institution: any,
trx?: Knex.Transaction
): Promise<void> { ): Promise<void> {
const transformToPlaidAccounts = const transformToPlaidAccounts =
transformPlaidAccountToCreateAccount(institution); transformPlaidAccountToCreateAccount(institution);
@@ -47,7 +82,7 @@ export class PlaidSyncDb {
await bluebird.map( await bluebird.map(
accountCreateDTOs, accountCreateDTOs,
(createAccountDTO: any) => (createAccountDTO: any) =>
this.createAccountService.createAccount(tenantId, createAccountDTO), this.syncBankAccount(tenantId, createAccountDTO, trx),
{ concurrency: CONCURRENCY_ASYNC } { concurrency: CONCURRENCY_ASYNC }
); );
} }
@@ -61,15 +96,16 @@ export class PlaidSyncDb {
public async syncAccountTranactions( public async syncAccountTranactions(
tenantId: number, tenantId: number,
plaidAccountId: number, plaidAccountId: number,
plaidTranasctions: PlaidTransaction[] plaidTranasctions: PlaidTransaction[],
trx?: Knex.Transaction
): Promise<void> { ): Promise<void> {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);
const cashflowAccount = await Account.query() const cashflowAccount = await Account.query(trx)
.findOne({ plaidAccountId }) .findOne({ plaidAccountId })
.throwIfNotFound(); .throwIfNotFound();
const openingEquityBalance = await Account.query().findOne( const openingEquityBalance = await Account.query(trx).findOne(
'slug', 'slug',
'opening-balance-equity' 'opening-balance-equity'
); );
@@ -87,7 +123,8 @@ export class PlaidSyncDb {
(uncategoriedDTO) => (uncategoriedDTO) =>
this.cashflowApp.createUncategorizedTransaction( this.cashflowApp.createUncategorizedTransaction(
tenantId, tenantId,
uncategoriedDTO uncategoriedDTO,
trx
), ),
{ concurrency: 1 } { concurrency: 1 }
); );
@@ -100,7 +137,8 @@ export class PlaidSyncDb {
*/ */
public async syncAccountsTransactions( public async syncAccountsTransactions(
tenantId: number, tenantId: number,
plaidAccountsTransactions: PlaidTransaction[] plaidAccountsTransactions: PlaidTransaction[],
trx?: Knex.Transaction
): Promise<void> { ): Promise<void> {
const groupedTrnsxByAccountId = entries( const groupedTrnsxByAccountId = entries(
groupBy(plaidAccountsTransactions, 'account_id') groupBy(plaidAccountsTransactions, 'account_id')
@@ -111,7 +149,8 @@ export class PlaidSyncDb {
return this.syncAccountTranactions( return this.syncAccountTranactions(
tenantId, tenantId,
plaidAccountId, plaidAccountId,
plaidTransactions plaidTransactions,
trx
); );
}, },
{ concurrency: CONCURRENCY_ASYNC } { concurrency: CONCURRENCY_ASYNC }
@@ -124,11 +163,12 @@ export class PlaidSyncDb {
*/ */
public async syncRemoveTransactions( public async syncRemoveTransactions(
tenantId: number, tenantId: number,
plaidTransactionsIds: string[] plaidTransactionsIds: string[],
trx?: Knex.Transaction
) { ) {
const { CashflowTransaction } = this.tenancy.models(tenantId); const { CashflowTransaction } = this.tenancy.models(tenantId);
const cashflowTransactions = await CashflowTransaction.query().whereIn( const cashflowTransactions = await CashflowTransaction.query(trx).whereIn(
'plaidTransactionId', 'plaidTransactionId',
plaidTransactionsIds plaidTransactionsIds
); );
@@ -140,7 +180,8 @@ export class PlaidSyncDb {
(transactionId: number) => (transactionId: number) =>
this.deleteCashflowTransactionService.deleteCashflowTransaction( this.deleteCashflowTransactionService.deleteCashflowTransaction(
tenantId, tenantId,
transactionId transactionId,
trx
), ),
{ concurrency: CONCURRENCY_ASYNC } { concurrency: CONCURRENCY_ASYNC }
); );
@@ -155,11 +196,12 @@ export class PlaidSyncDb {
public async syncTransactionsCursor( public async syncTransactionsCursor(
tenantId: number, tenantId: number,
plaidItemId: string, plaidItemId: string,
lastCursor: string lastCursor: string,
trx?: Knex.Transaction
) { ) {
const { PlaidItem } = this.tenancy.models(tenantId); const { PlaidItem } = this.tenancy.models(tenantId);
await PlaidItem.query().findOne({ plaidItemId }).patch({ lastCursor }); await PlaidItem.query(trx).findOne({ plaidItemId }).patch({ lastCursor });
} }
/** /**
@@ -169,13 +211,16 @@ export class PlaidSyncDb {
*/ */
public async updateLastFeedsUpdatedAt( public async updateLastFeedsUpdatedAt(
tenantId: number, tenantId: number,
plaidAccountIds: string[] plaidAccountIds: string[],
trx?: Knex.Transaction
) { ) {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);
await Account.query().whereIn('plaid_account_id', plaidAccountIds).patch({ await Account.query(trx)
lastFeedsUpdatedAt: new Date(), .whereIn('plaid_account_id', plaidAccountIds)
}); .patch({
lastFeedsUpdatedAt: new Date(),
});
} }
/** /**
@@ -187,12 +232,15 @@ export class PlaidSyncDb {
public async updateAccountsFeedsActive( public async updateAccountsFeedsActive(
tenantId: number, tenantId: number,
plaidAccountIds: string[], plaidAccountIds: string[],
isFeedsActive: boolean = true isFeedsActive: boolean = true,
trx?: Knex.Transaction
) { ) {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);
await Account.query().whereIn('plaid_account_id', plaidAccountIds).patch({ await Account.query(trx)
isFeedsActive, .whereIn('plaid_account_id', plaidAccountIds)
}); .patch({
isFeedsActive,
});
} }
} }

View File

@@ -3,6 +3,8 @@ import { Inject, Service } from 'typedi';
import { PlaidClientWrapper } from '@/lib/Plaid/Plaid'; import { PlaidClientWrapper } from '@/lib/Plaid/Plaid';
import { PlaidSyncDb } from './PlaidSyncDB'; import { PlaidSyncDb } from './PlaidSyncDB';
import { PlaidFetchedTransactionsUpdates } from '@/interfaces'; import { PlaidFetchedTransactionsUpdates } from '@/interfaces';
import UnitOfWork from '@/services/UnitOfWork';
import { Knex } from 'knex';
@Service() @Service()
export class PlaidUpdateTransactions { export class PlaidUpdateTransactions {
@@ -12,12 +14,40 @@ export class PlaidUpdateTransactions {
@Inject() @Inject()
private plaidSync: PlaidSyncDb; private plaidSync: PlaidSyncDb;
@Inject()
private uow: UnitOfWork;
/** /**
* Handles the fetching and storing of new, modified, or removed transactions * Handles sync the Plaid item to Bigcaptial under UOW.
* @param {number} tenantId Tenant ID. * @param {number} tenantId
* @param {string} plaidItemId the Plaid ID for the item. * @param {number} plaidItemId
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
*/ */
public async updateTransactions(tenantId: number, plaidItemId: string) { public async updateTransactions(tenantId: number, plaidItemId: string) {
return this.uow.withTransaction(tenantId, (trx: Knex.Transaction) => {
return this.updateTransactionsWork(tenantId, plaidItemId, trx);
});
}
/**
* Handles the fetching and storing the following:
* - New, modified, or removed transactions.
* - New bank accounts.
* - Last accounts feeds updated at.
* - Turn on the accounts feed flag.
* @param {number} tenantId - Tenant ID.
* @param {string} plaidItemId - The Plaid ID for the item.
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
*/
public async updateTransactionsWork(
tenantId: number,
plaidItemId: string,
trx?: Knex.Transaction
): Promise<{
addedCount: number;
modifiedCount: number;
removedCount: number;
}> {
// Fetch new transactions from plaid api. // Fetch new transactions from plaid api.
const { added, modified, removed, cursor, accessToken } = const { added, modified, removed, cursor, accessToken } =
await this.fetchTransactionUpdates(tenantId, plaidItemId); await this.fetchTransactionUpdates(tenantId, plaidItemId);
@@ -29,28 +59,42 @@ export class PlaidUpdateTransactions {
} = await plaidInstance.accountsGet(request); } = await plaidInstance.accountsGet(request);
const plaidAccountsIds = accounts.map((a) => a.account_id); const plaidAccountsIds = accounts.map((a) => a.account_id);
const { const {
data: { institution }, data: { institution },
} = await plaidInstance.institutionsGetById({ } = await plaidInstance.institutionsGetById({
institution_id: item.institution_id, institution_id: item.institution_id,
country_codes: ['US', 'UK'], country_codes: ['US', 'UK'],
}); });
// Update the DB. // Sync bank accounts.
await this.plaidSync.syncBankAccounts(tenantId, accounts, institution); await this.plaidSync.syncBankAccounts(tenantId, accounts, institution, trx);
// Sync bank account transactions.
await this.plaidSync.syncAccountsTransactions( await this.plaidSync.syncAccountsTransactions(
tenantId, tenantId,
added.concat(modified) added.concat(modified),
trx
);
// Sync removed transactions.
await this.plaidSync.syncRemoveTransactions(tenantId, removed, trx);
// Sync transactions cursor.
await this.plaidSync.syncTransactionsCursor(
tenantId,
plaidItemId,
cursor,
trx
); );
await this.plaidSync.syncRemoveTransactions(tenantId, removed);
await this.plaidSync.syncTransactionsCursor(tenantId, plaidItemId, cursor);
// Update the last feeds updated at of the updated accounts. // Update the last feeds updated at of the updated accounts.
await this.plaidSync.updateLastFeedsUpdatedAt(tenantId, plaidAccountsIds); await this.plaidSync.updateLastFeedsUpdatedAt(
tenantId,
plaidAccountsIds,
trx
);
// Turn on the accounts feeds flag. // Turn on the accounts feeds flag.
await this.plaidSync.updateAccountsFeedsActive(tenantId, plaidAccountsIds); await this.plaidSync.updateAccountsFeedsActive(
tenantId,
plaidAccountsIds,
true,
trx
);
return { return {
addedCount: added.length, addedCount: added.length,
modifiedCount: modified.length, modifiedCount: modified.length,

View File

@@ -42,7 +42,12 @@ export const transformPlaidTrxsToCashflowCreate = R.curry(
): CreateUncategorizedTransactionDTO => { ): CreateUncategorizedTransactionDTO => {
return { return {
date: plaidTranasction.date, date: plaidTranasction.date,
amount: plaidTranasction.amount,
// Plaid: Positive values when money moves out of the account; negative values
// when money moves in. For example, debit card purchases are positive;
// credit card payments, direct deposits, and refunds are negative.
amount: -1 * plaidTranasction.amount,
description: plaidTranasction.name, description: plaidTranasction.name,
payee: plaidTranasction.payment_meta?.payee, payee: plaidTranasction.payment_meta?.payee,
currencyCode: plaidTranasction.iso_currency_code, currencyCode: plaidTranasction.iso_currency_code,

View File

@@ -1,3 +1,4 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { DeleteCashflowTransaction } from './DeleteCashflowTransactionService'; import { DeleteCashflowTransaction } from './DeleteCashflowTransactionService';
import { UncategorizeCashflowTransaction } from './UncategorizeCashflowTransaction'; import { UncategorizeCashflowTransaction } from './UncategorizeCashflowTransaction';
@@ -119,11 +120,13 @@ export class CashflowApplication {
*/ */
public createUncategorizedTransaction( public createUncategorizedTransaction(
tenantId: number, tenantId: number,
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO,
trx?: Knex.Transaction
) { ) {
return this.createUncategorizedTransactionService.create( return this.createUncategorizedTransactionService.create(
tenantId, tenantId,
createUncategorizedTransactionDTO createUncategorizedTransactionDTO,
trx
); );
} }

View File

@@ -12,7 +12,6 @@ import { Knex } from 'knex';
import { transformCategorizeTransToCashflow } from './utils'; import { transformCategorizeTransToCashflow } from './utils';
import { CommandCashflowValidator } from './CommandCasflowValidator'; import { CommandCashflowValidator } from './CommandCasflowValidator';
import NewCashflowTransactionService from './NewCashflowTransactionService'; import NewCashflowTransactionService from './NewCashflowTransactionService';
import { TransferAuthorizationGuaranteeDecision } from 'plaid';
@Service() @Service()
export class CategorizeCashflowTransaction { export class CategorizeCashflowTransaction {

View File

@@ -30,7 +30,8 @@ export class DeleteCashflowTransaction {
*/ */
public deleteCashflowTransaction = async ( public deleteCashflowTransaction = async (
tenantId: number, tenantId: number,
cashflowTransactionId: number cashflowTransactionId: number,
trx?: Knex.Transaction
): Promise<{ oldCashflowTransaction: ICashflowTransaction }> => { ): Promise<{ oldCashflowTransaction: ICashflowTransaction }> => {
const { CashflowTransaction, CashflowTransactionLine } = const { CashflowTransaction, CashflowTransactionLine } =
this.tenancy.models(tenantId); this.tenancy.models(tenantId);
@@ -43,34 +44,44 @@ export class DeleteCashflowTransaction {
this.throwErrorIfTransactionNotFound(oldCashflowTransaction); this.throwErrorIfTransactionNotFound(oldCashflowTransaction);
// Starting database transaction. // Starting database transaction.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { return this.uow.withTransaction(
// Triggers `onCashflowTransactionDelete` event. tenantId,
await this.eventPublisher.emitAsync(events.cashflow.onTransactionDeleting, { async (trx: Knex.Transaction) => {
trx, // Triggers `onCashflowTransactionDelete` event.
tenantId, await this.eventPublisher.emitAsync(
oldCashflowTransaction, events.cashflow.onTransactionDeleting,
} as ICommandCashflowDeletingPayload); {
trx,
tenantId,
oldCashflowTransaction,
} as ICommandCashflowDeletingPayload
);
// Delete cashflow transaction associated lines first. // Delete cashflow transaction associated lines first.
await CashflowTransactionLine.query(trx) await CashflowTransactionLine.query(trx)
.where('cashflow_transaction_id', cashflowTransactionId) .where('cashflow_transaction_id', cashflowTransactionId)
.delete(); .delete();
// Delete cashflow transaction. // Delete cashflow transaction.
await CashflowTransaction.query(trx) await CashflowTransaction.query(trx)
.findById(cashflowTransactionId) .findById(cashflowTransactionId)
.delete(); .delete();
// Triggers `onCashflowTransactionDeleted` event. // Triggers `onCashflowTransactionDeleted` event.
await this.eventPublisher.emitAsync(events.cashflow.onTransactionDeleted, { await this.eventPublisher.emitAsync(
trx, events.cashflow.onTransactionDeleted,
tenantId, {
cashflowTransactionId, trx,
oldCashflowTransaction, tenantId,
} as ICommandCashflowDeletedPayload); cashflowTransactionId,
oldCashflowTransaction,
} as ICommandCashflowDeletedPayload
);
return { oldCashflowTransaction }; return { oldCashflowTransaction };
}); },
trx
);
}; };
/** /**

View File

@@ -68,7 +68,11 @@ export const CASHFLOW_TRANSACTION_TYPE_META = {
[`${CASHFLOW_TRANSACTION_TYPE.OTHER_EXPENSE}`]: { [`${CASHFLOW_TRANSACTION_TYPE.OTHER_EXPENSE}`]: {
type: 'OtherExpense', type: 'OtherExpense',
direction: CASHFLOW_DIRECTION.OUT, direction: CASHFLOW_DIRECTION.OUT,
creditType: [ACCOUNT_TYPE.EXPENSE, ACCOUNT_TYPE.OTHER_EXPENSE], creditType: [
ACCOUNT_TYPE.EXPENSE,
ACCOUNT_TYPE.OTHER_EXPENSE,
ACCOUNT_TYPE.COST_OF_GOODS_SOLD,
],
}, },
}; };

View File

@@ -1,4 +1,4 @@
import { upperFirst, camelCase, omit } from 'lodash'; import { upperFirst, camelCase } from 'lodash';
import { import {
CASHFLOW_TRANSACTION_TYPE, CASHFLOW_TRANSACTION_TYPE,
CASHFLOW_TRANSACTION_TYPE_META, CASHFLOW_TRANSACTION_TYPE_META,
@@ -6,7 +6,6 @@ import {
} from './constants'; } from './constants';
import { import {
ICashflowNewCommandDTO, ICashflowNewCommandDTO,
ICashflowTransaction,
ICategorizeCashflowTransactioDTO, ICategorizeCashflowTransactioDTO,
IUncategorizedCashflowTransaction, IUncategorizedCashflowTransaction,
} from '@/interfaces'; } from '@/interfaces';
@@ -62,6 +61,7 @@ export const transformCategorizeTransToCashflow = (
transactionNumber: categorizeDTO.transactionNumber, transactionNumber: categorizeDTO.transactionNumber,
transactionType: categorizeDTO.transactionType, transactionType: categorizeDTO.transactionType,
uncategorizedTransactionId: uncategorizeModel.id, uncategorizedTransactionId: uncategorizeModel.id,
branchId: categorizeDTO?.branchId,
publish: true, publish: true,
}; };
}; };

View File

@@ -5,7 +5,11 @@ import { PageProperties, PdfFormat } from '@/lib/Chromiumly/_types';
import { UrlConverter } from '@/lib/Chromiumly/UrlConvert'; import { UrlConverter } from '@/lib/Chromiumly/UrlConvert';
import HasTenancyService from '../Tenancy/TenancyService'; import HasTenancyService from '../Tenancy/TenancyService';
import { Chromiumly } from '@/lib/Chromiumly/Chromiumly'; import { Chromiumly } from '@/lib/Chromiumly/Chromiumly';
import { PDF_FILE_EXPIRE_IN, getPdfFilesStorageDir } from './utils'; import {
PDF_FILE_EXPIRE_IN,
getPdfFilePath,
getPdfFilesStorageDir,
} from './utils';
@Service() @Service()
export class ChromiumlyHtmlConvert { export class ChromiumlyHtmlConvert {
@@ -22,22 +26,16 @@ export class ChromiumlyHtmlConvert {
tenantId: number, tenantId: number,
content: string content: string
): Promise<[string, () => Promise<void>]> { ): Promise<[string, () => Promise<void>]> {
const { Attachment } = this.tenancy.models(tenantId); const { Document } = this.tenancy.models(tenantId);
const filename = `document-${Date.now()}.html`; const filename = `document-print-${Date.now()}.html`;
const storageDir = getPdfFilesStorageDir(filename); const filePath = getPdfFilePath(filename);
const filePath = path.join(global.__storage_dir, storageDir);
await fs.writeFile(filePath, content); await fs.writeFile(filePath, content);
await Attachment.query().insert({ await Document.query().insert({ key: filename, mimeType: 'text/html' });
key: filename,
path: storageDir,
expire_in: PDF_FILE_EXPIRE_IN, // ms
extension: 'html',
});
const cleanup = async () => { const cleanup = async () => {
await fs.unlink(filePath); await fs.unlink(filePath);
await Attachment.query().where('key', filename).delete(); await Document.query().where('key', filename).delete();
}; };
return [filename, cleanup]; return [filename, cleanup];
} }
@@ -60,6 +58,7 @@ export class ChromiumlyHtmlConvert {
html html
); );
const fileDir = getPdfFilesStorageDir(filename); const fileDir = getPdfFilesStorageDir(filename);
const url = path.join(Chromiumly.GOTENBERG_DOCS_ENDPOINT, fileDir); const url = path.join(Chromiumly.GOTENBERG_DOCS_ENDPOINT, fileDir);
const urlConverter = new UrlConverter(); const urlConverter = new UrlConverter();

View File

@@ -5,4 +5,10 @@ export const PDF_FILE_EXPIRE_IN = 40; // ms
export const getPdfFilesStorageDir = (filename: string) => { export const getPdfFilesStorageDir = (filename: string) => {
return path.join(PDF_FILE_SUB_DIR, filename); return path.join(PDF_FILE_SUB_DIR, filename);
} };
export const getPdfFilePath = (filename: string) => {
const storageDir = getPdfFilesStorageDir(filename);
return path.join(global.__storage_dir, storageDir);
};

View File

@@ -1,5 +1,6 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { ExportResourceService } from './ExportService'; import { ExportResourceService } from './ExportService';
import { ExportFormat } from './common';
@Service() @Service()
export class ExportApplication { export class ExportApplication {
@@ -9,9 +10,9 @@ export class ExportApplication {
/** /**
* Exports the given resource to csv, xlsx or pdf format. * Exports the given resource to csv, xlsx or pdf format.
* @param {string} reosurce * @param {string} reosurce
* @param {string} format * @param {ExportFormat} format
*/ */
public export(tenantId: number, resource: string, format: string) { public export(tenantId: number, resource: string, format: ExportFormat) {
return this.exportResource.export(tenantId, resource, format); return this.exportResource.export(tenantId, resource, format);
} }
} }

View File

@@ -0,0 +1,47 @@
import { Inject, Service } from 'typedi';
import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '../TemplateInjectable/TemplateInjectable';
import { mapPdfRows } from './utils';
@Service()
export class ExportPdf {
@Inject()
private templateInjectable: TemplateInjectable;
@Inject()
private chromiumlyTenancy: ChromiumlyTenancy;
/**
* Generates the pdf table sheet for the given data and columns.
* @param {number} tenantId
* @param {} columns
* @param {Record<string, string>} data
* @param {string} sheetTitle
* @param {string} sheetDescription
* @returns
*/
public async pdf(
tenantId: number,
columns: { accessor: string },
data: Record<string, any>,
sheetTitle: string = '',
sheetDescription: string = ''
) {
const rows = mapPdfRows(columns, data);
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/export-resource-table',
{
table: { rows, columns },
sheetTitle,
sheetDescription,
}
);
// Convert the HTML content to PDF
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
margins: { top: 0.2, bottom: 0.2, left: 0.2, right: 0.2 },
landscape: true,
});
}
}

View File

@@ -6,9 +6,10 @@ import { sanitizeResourceName } from '../Import/_utils';
import ResourceService from '../Resource/ResourceService'; import ResourceService from '../Resource/ResourceService';
import { ExportableResources } from './ExportResources'; import { ExportableResources } from './ExportResources';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { Errors } from './common'; import { Errors, ExportFormat } from './common';
import { IModelMeta, IModelMetaColumn } from '@/interfaces'; import { IModelMeta, IModelMetaColumn } from '@/interfaces';
import { flatDataCollections, getDataAccessor } from './utils'; import { flatDataCollections, getDataAccessor } from './utils';
import { ExportPdf } from './ExportPdf';
@Service() @Service()
export class ExportResourceService { export class ExportResourceService {
@@ -18,13 +19,20 @@ export class ExportResourceService {
@Inject() @Inject()
private exportableResources: ExportableResources; private exportableResources: ExportableResources;
@Inject()
private exportPdf: ExportPdf;
/** /**
* Exports the given resource data through csv, xlsx or pdf. * Exports the given resource data through csv, xlsx or pdf.
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {string} resourceName - Resource name. * @param {string} resourceName - Resource name.
* @param {string} format - File format. * @param {ExportFormat} format - File format.
*/ */
public async export(tenantId: number, resourceName: string, format: string = 'csv') { public async export(
tenantId: number,
resourceName: string,
format: ExportFormat = ExportFormat.Csv
) {
const resource = sanitizeResourceName(resourceName); const resource = sanitizeResourceName(resourceName);
const resourceMeta = this.getResourceMeta(tenantId, resource); const resourceMeta = this.getResourceMeta(tenantId, resource);
@@ -32,10 +40,24 @@ export class ExportResourceService {
const data = await this.getExportableData(tenantId, resource); const data = await this.getExportableData(tenantId, resource);
const transformed = this.transformExportedData(tenantId, resource, data); const transformed = this.transformExportedData(tenantId, resource, data);
const exportableColumns = this.getExportableColumns(resourceMeta);
const workbook = this.createWorkbook(transformed, exportableColumns);
return this.exportWorkbook(workbook, format); // Returns the csv, xlsx format.
if (format === ExportFormat.Csv || format === ExportFormat.Xlsx) {
const exportableColumns = this.getExportableColumns(resourceMeta);
const workbook = this.createWorkbook(transformed, exportableColumns);
return this.exportWorkbook(workbook, format);
// Returns the pdf format.
} else if (format === ExportFormat.Pdf) {
const printableColumns = this.getPrintableColumns(resourceMeta);
return this.exportPdf.pdf(
tenantId,
printableColumns,
transformed,
resourceMeta?.print?.pageTitle
);
}
} }
/** /**
@@ -91,6 +113,7 @@ export class ExportResourceService {
private async getExportableData(tenantId: number, resource: string) { private async getExportableData(tenantId: number, resource: string) {
const exportable = const exportable =
this.exportableResources.registry.getExportable(resource); this.exportableResources.registry.getExportable(resource);
return exportable.exportable(tenantId, {}); return exportable.exportable(tenantId, {});
} }
@@ -125,6 +148,32 @@ export class ExportResourceService {
return processColumns(resourceMeta.columns); return processColumns(resourceMeta.columns);
} }
private getPrintableColumns(resourceMeta: IModelMeta) {
const processColumns = (
columns: { [key: string]: IModelMetaColumn },
parent = ''
) => {
return Object.entries(columns)
.filter(([_, value]) => value.printable !== false)
.flatMap(([key, value]) => {
if (value.type === 'collection' && value.collectionOf === 'object') {
return processColumns(value.columns, key);
} else {
const group = parent;
return [
{
name: value.name,
type: value.type || 'text',
accessor: value.accessor || key,
group,
},
];
}
});
};
return processColumns(resourceMeta.columns);
}
/** /**
* Creates a workbook from the provided data and columns. * Creates a workbook from the provided data and columns.
* @param {any[]} data - The data to be included in the workbook. * @param {any[]} data - The data to be included in the workbook.
@@ -136,7 +185,6 @@ export class ExportResourceService {
const worksheetData = data.map((item) => const worksheetData = data.map((item) =>
exportableColumns.map((col) => get(item, getDataAccessor(col))) exportableColumns.map((col) => get(item, getDataAccessor(col)))
); );
worksheetData.unshift(exportableColumns.map((col) => col.name)); worksheetData.unshift(exportableColumns.map((col) => col.name));
const worksheet = xlsx.utils.aoa_to_sheet(worksheetData); const worksheet = xlsx.utils.aoa_to_sheet(worksheetData);

View File

@@ -1,3 +1,9 @@
export enum Errors { export enum Errors {
RESOURCE_NOT_EXPORTABLE = 'RESOURCE_NOT_EXPORTABLE', RESOURCE_NOT_EXPORTABLE = 'RESOURCE_NOT_EXPORTABLE',
} }
export enum ExportFormat {
Csv = 'csv',
Pdf = 'pdf',
Xlsx = 'xlsx',
}

View File

@@ -1,4 +1,4 @@
import { flatMap } from 'lodash'; import { flatMap, get } from 'lodash';
/** /**
* Flattens the data based on a specified attribute. * Flattens the data based on a specified attribute.
* @param data - The data to be flattened. * @param data - The data to be flattened.
@@ -25,3 +25,21 @@ export const flatDataCollections = (
export const getDataAccessor = (col: any) => { export const getDataAccessor = (col: any) => {
return col.group ? `${col.group}.${col.accessor}` : col.accessor; return col.group ? `${col.group}.${col.accessor}` : col.accessor;
}; };
/**
* Maps the data retrieved from the service layer to the pdf document.
* @param {any} columns
* @param {Record<stringm any>} data
* @returns
*/
export const mapPdfRows = (columns: any, data: Record<string, any>) => {
return data.map((item) => {
const cells = columns.map((column) => {
return {
key: column.accessor,
value: get(item, getDataAccessor(column)),
};
});
return { cells, classNames: '' };
});
};

View File

@@ -108,17 +108,28 @@ export default class ResourceService {
const $hasFields = (field) => const $hasFields = (field) =>
'undefined' !== typeof field.fields ? field : undefined; 'undefined' !== typeof field.fields ? field : undefined;
const $hasColumns = (column) => const $ColumnHasColumns = (column) =>
'undefined' !== typeof column.columns ? column : undefined; 'undefined' !== typeof column.columns ? column : undefined;
const $hasColumns = (columns) =>
'undefined' !== typeof columns ? columns : undefined;
const naviagations = [ const naviagations = [
['fields', qim.$each, 'name'], ['fields', qim.$each, 'name'],
['fields', qim.$each, $enumerationType, 'options', qim.$each, 'label'], ['fields', qim.$each, $enumerationType, 'options', qim.$each, 'label'],
['fields2', qim.$each, 'name'], ['fields2', qim.$each, 'name'],
['fields2', qim.$each, $enumerationType, 'options', qim.$each, 'label'], ['fields2', qim.$each, $enumerationType, 'options', qim.$each, 'label'],
['fields2', qim.$each, $hasFields, 'fields', qim.$each, 'name'], ['fields2', qim.$each, $hasFields, 'fields', qim.$each, 'name'],
['columns', qim.$each, 'name'], ['columns', $hasColumns, qim.$each, 'name'],
['columns', qim.$each, $hasColumns, 'columns', qim.$each, 'name'], [
'columns',
$hasColumns,
qim.$each,
$ColumnHasColumns,
'columns',
qim.$each,
'name',
],
]; ];
return this.i18nService.i18nApply(naviagations, meta, tenantId); return this.i18nService.i18nApply(naviagations, meta, tenantId);
} }

View File

@@ -146,6 +146,7 @@ export class EditPaymentReceive {
paymentReceiveId, paymentReceiveId,
paymentReceive, paymentReceive,
oldPaymentReceive, oldPaymentReceive,
paymentReceiveDTO,
authorizedUser, authorizedUser,
trx, trx,
} as IPaymentReceiveEditedPayload); } as IPaymentReceiveEditedPayload);

View File

@@ -2,6 +2,7 @@ import { Service } from 'typedi';
import { createCheckout } from '@lemonsqueezy/lemonsqueezy.js'; import { createCheckout } from '@lemonsqueezy/lemonsqueezy.js';
import { SystemUser } from '@/system/models'; import { SystemUser } from '@/system/models';
import { configureLemonSqueezy } from './utils'; import { configureLemonSqueezy } from './utils';
import config from '@/config';
@Service() @Service()
export class LemonSqueezyService { export class LemonSqueezyService {
@@ -28,7 +29,7 @@ export class LemonSqueezyService {
}, },
productOptions: { productOptions: {
enabledVariants: [variantId], enabledVariants: [variantId],
redirectUrl: `http://localhost:4000/dashboard/billing/`, redirectUrl: config.lemonSqueezy.redirectTo,
receiptButtonText: 'Go to Dashboard', receiptButtonText: 'Go to Dashboard',
receiptThankYouNote: 'Thank you for signing up to Lemon Stand!', receiptThankYouNote: 'Thank you for signing up to Lemon Stand!',
}, },

View File

@@ -0,0 +1,18 @@
// @ts-nocheck
import { FSuggest } from '../Forms';
interface BranchSuggestFieldProps {
items: any[];
}
export function BranchSuggestField({ ...props }: BranchSuggestFieldProps) {
return (
<FSuggest
valueAccessor={'id'}
labelAccessor={'code'}
textAccessor={'name'}
inputProps={{ placeholder: 'Select a branch' }}
{...props}
/>
);
}

View File

@@ -50,7 +50,6 @@ import InvoiceMailDialog from '@/containers/Sales/Invoices/InvoiceMailDialog/Inv
import EstimateMailDialog from '@/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog'; import EstimateMailDialog from '@/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog';
import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog'; import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog';
import PaymentMailDialog from '@/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialog'; import PaymentMailDialog from '@/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialog';
import { ConnectBankDialog } from '@/containers/CashFlow/ConnectBankDialog';
import { ExportDialog } from '@/containers/Dialogs/ExportDialog'; import { ExportDialog } from '@/containers/Dialogs/ExportDialog';
/** /**
@@ -97,7 +96,6 @@ export default function DialogsContainer() {
<NotifyPaymentReceiveViaSMSDialog <NotifyPaymentReceiveViaSMSDialog
dialogName={DialogsName.NotifyPaymentViaForm} dialogName={DialogsName.NotifyPaymentViaForm}
/> />
<BadDebtDialog dialogName={DialogsName.BadDebtForm} /> <BadDebtDialog dialogName={DialogsName.BadDebtForm} />
<SMSMessageDialog dialogName={DialogsName.SMSMessageForm} /> <SMSMessageDialog dialogName={DialogsName.SMSMessageForm} />
<RefundCreditNoteDialog dialogName={DialogsName.RefundCreditNote} /> <RefundCreditNoteDialog dialogName={DialogsName.RefundCreditNote} />
@@ -148,8 +146,6 @@ export default function DialogsContainer() {
<EstimateMailDialog dialogName={DialogsName.EstimateMail} /> <EstimateMailDialog dialogName={DialogsName.EstimateMail} />
<ReceiptMailDialog dialogName={DialogsName.ReceiptMail} /> <ReceiptMailDialog dialogName={DialogsName.ReceiptMail} />
<PaymentMailDialog dialogName={DialogsName.PaymentMail} /> <PaymentMailDialog dialogName={DialogsName.PaymentMail} />
<ConnectBankDialog dialogName={DialogsName.ConnectBankCreditCard} />
<ExportDialog dialogName={DialogsName.Export} /> <ExportDialog dialogName={DialogsName.Export} />
</div> </div>
); );

View File

@@ -30,6 +30,7 @@ import withSettings from '@/containers/Settings/withSettings';
import withSettingsActions from '@/containers/Settings/withSettingsActions'; import withSettingsActions from '@/containers/Settings/withSettingsActions';
import withDialogActions from '@/containers/Dialog/withDialogActions'; import withDialogActions from '@/containers/Dialog/withDialogActions';
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { DialogsName } from '@/constants/dialogs'; import { DialogsName } from '@/constants/dialogs';
@@ -50,7 +51,7 @@ function ManualJournalActionsBar({
addSetting, addSetting,
// #withDialogActions // #withDialogActions
openDialog openDialog,
}) { }) {
// History context. // History context.
const history = useHistory(); const history = useHistory();
@@ -58,6 +59,9 @@ function ManualJournalActionsBar({
// Manual journals context. // Manual journals context.
const { journalsViews, fields } = useManualJournalsContext(); const { journalsViews, fields } = useManualJournalsContext();
// Exports pdf document.
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
// Manual journals refresh action. // Manual journals refresh action.
const { refresh } = useRefreshJournals(); const { refresh } = useRefreshJournals();
@@ -91,6 +95,11 @@ function ManualJournalActionsBar({
openDialog(DialogsName.Export, { resource: 'manual_journal' }); openDialog(DialogsName.Export, { resource: 'manual_journal' });
}; };
// Handle the pdf print button click.
const handlePdfPrintBtnSubmit = () => {
downloadExportPdf({ resource: 'ManualJournal' });
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
<NavbarGroup> <NavbarGroup>
@@ -134,10 +143,12 @@ function ManualJournalActionsBar({
/> />
</If> </If>
<NavbarDivider />
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon="print-16" iconSize={16} />} icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />} text={<T id={'print'} />}
onClick={handlePdfPrintBtnSubmit}
/> />
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}

View File

@@ -1,6 +1,6 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import { isEmpty } from 'lodash'; import { isEmpty, isUndefined } from 'lodash';
import { import {
Button, Button,
NavbarGroup, NavbarGroup,
@@ -9,7 +9,11 @@ import {
Intent, Intent,
Switch, Switch,
Alignment, Alignment,
ProgressBar,
ToastProps,
Text,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import clsx from 'classnames';
import { import {
AdvancedFilterPopover, AdvancedFilterPopover,
@@ -26,8 +30,10 @@ import {
import { AccountAction, AbilitySubject } from '@/constants/abilityOption'; import { AccountAction, AbilitySubject } from '@/constants/abilityOption';
import { DialogsName } from '@/constants/dialogs'; import { DialogsName } from '@/constants/dialogs';
import { useHistory } from 'react-router-dom';
import { useRefreshAccounts } from '@/hooks/query/accounts'; import { useRefreshAccounts } from '@/hooks/query/accounts';
import { useAccountsChartContext } from './AccountsChartProvider'; import { useAccountsChartContext } from './AccountsChartProvider';
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
import withAccounts from './withAccounts'; import withAccounts from './withAccounts';
import withAccountsTableActions from './withAccountsTableActions'; import withAccountsTableActions from './withAccountsTableActions';
@@ -37,7 +43,6 @@ import withSettings from '@/containers/Settings/withSettings';
import withSettingsActions from '@/containers/Settings/withSettingsActions'; import withSettingsActions from '@/containers/Settings/withSettingsActions';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { useHistory } from 'react-router-dom';
/** /**
* Accounts actions bar. * Accounts actions bar.
@@ -57,22 +62,18 @@ function AccountsActionsBar({
// #withAccountsTableActions // #withAccountsTableActions
setAccountsTableState, setAccountsTableState,
// #ownProps
onFilterChanged,
// #withSettings // #withSettings
accountsTableSize, accountsTableSize,
// #withSettingsActions // #withSettingsActions
addSetting, addSetting,
}) { }) {
const { resourceViews, fields } = useAccountsChartContext();
const history = useHistory(); const history = useHistory();
const onClickNewAccount = () => { const { resourceViews, fields } = useAccountsChartContext();
openDialog(DialogsName.AccountForm, {});
}; // Exports pdf document.
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
// Accounts refresh action. // Accounts refresh action.
const { refresh } = useRefreshAccounts(); const { refresh } = useRefreshAccounts();
@@ -81,35 +82,29 @@ function AccountsActionsBar({
const handleBulkDelete = () => { const handleBulkDelete = () => {
openAlert('accounts-bulk-delete', { accountsIds: accountsSelectedRows }); openAlert('accounts-bulk-delete', { accountsIds: accountsSelectedRows });
}; };
// Handle bulk accounts activate. // Handle bulk accounts activate.
const handelBulkActivate = () => { const handelBulkActivate = () => {
openAlert('accounts-bulk-activate', { accountsIds: accountsSelectedRows }); openAlert('accounts-bulk-activate', { accountsIds: accountsSelectedRows });
}; };
// Handle bulk accounts inactivate. // Handle bulk accounts inactivate.
const handelBulkInactive = () => { const handelBulkInactive = () => {
openAlert('accounts-bulk-inactivate', { openAlert('accounts-bulk-inactivate', {
accountsIds: accountsSelectedRows, accountsIds: accountsSelectedRows,
}); });
}; };
// Handle tab changing. // Handle tab changing.
const handleTabChange = (view) => { const handleTabChange = (view) => {
setAccountsTableState({ viewSlug: view ? view.slug : null }); setAccountsTableState({ viewSlug: view ? view.slug : null });
}; };
// Handle inactive switch changing. // Handle inactive switch changing.
const handleInactiveSwitchChange = (event) => { const handleInactiveSwitchChange = (event) => {
const checked = event.target.checked; const checked = event.target.checked;
setAccountsTableState({ inactiveMode: checked }); setAccountsTableState({ inactiveMode: checked });
}; };
// Handle click a refresh accounts // Handle click a refresh accounts
const handleRefreshBtnClick = () => { const handleRefreshBtnClick = () => {
refresh(); refresh();
}; };
// Handle table row size change. // Handle table row size change.
const handleTableRowSizeChange = (size) => { const handleTableRowSizeChange = (size) => {
addSetting('accounts', 'tableSize', size); addSetting('accounts', 'tableSize', size);
@@ -122,6 +117,14 @@ function AccountsActionsBar({
const handleExportBtnClick = () => { const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'account' }); openDialog(DialogsName.Export, { resource: 'account' });
}; };
// Handle the print button click.
const handlePrintBtnClick = () => {
downloadExportPdf({ resource: 'Account' });
};
// Handle click new account.
const onClickNewAccount = () => {
openDialog(DialogsName.AccountForm, {});
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
@@ -185,6 +188,7 @@ function AccountsActionsBar({
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon="print-16" iconSize={16} />} icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />} text={<T id={'print'} />}
onClick={handlePrintBtnClick}
/> />
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}

View File

@@ -15,6 +15,7 @@ import {
FeatureCan, FeatureCan,
} from '@/components'; } from '@/components';
import { useRefreshCashflowAccounts } from '@/hooks/query'; import { useRefreshCashflowAccounts } from '@/hooks/query';
import { useOpenPlaidConnect } from '@/hooks/utils/useOpenPlaidConnect';
import { CashflowAction, AbilitySubject } from '@/constants/abilityOption'; import { CashflowAction, AbilitySubject } from '@/constants/abilityOption';
import withDialogActions from '@/containers/Dialog/withDialogActions'; import withDialogActions from '@/containers/Dialog/withDialogActions';
@@ -39,6 +40,9 @@ function CashFlowAccountsActionsBar({
}) { }) {
const { refresh } = useRefreshCashflowAccounts(); const { refresh } = useRefreshCashflowAccounts();
// Opens the Plaid popup.
const { openPlaidAsync, isPlaidLoading } = useOpenPlaidConnect();
// Handle refresh button click. // Handle refresh button click.
const handleRefreshBtnClick = () => { const handleRefreshBtnClick = () => {
refresh(); refresh();
@@ -64,7 +68,7 @@ function CashFlowAccountsActionsBar({
}; };
// Handle connect button click. // Handle connect button click.
const handleConnectToBank = () => { const handleConnectToBank = () => {
openDialog(DialogsName.ConnectBankCreditCard); openPlaidAsync();
}; };
return ( return (
@@ -116,6 +120,7 @@ function CashFlowAccountsActionsBar({
className={Classes.MINIMAL} className={Classes.MINIMAL}
text={'Connect to Bank / Credit Card'} text={'Connect to Bank / Credit Card'}
onClick={handleConnectToBank} onClick={handleConnectToBank}
disabled={isPlaidLoading}
/> />
<NavbarDivider /> <NavbarDivider />
</FeatureCan> </FeatureCan>

View File

@@ -1,5 +1,6 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React, { useMemo } from 'react';
import { first } from 'lodash';
import { DrawerHeaderContent, DrawerLoading } from '@/components'; import { DrawerHeaderContent, DrawerLoading } from '@/components';
import { DRAWERS } from '@/constants/drawers'; import { DRAWERS } from '@/constants/drawers';
import { import {
@@ -34,6 +35,12 @@ function CategorizeTransactionBoot({ uncategorizedTransactionId, ...props }) {
isLoading: isUncategorizedTransactionLoading, isLoading: isUncategorizedTransactionLoading,
} = useUncategorizedTransaction(uncategorizedTransactionId); } = useUncategorizedTransaction(uncategorizedTransactionId);
// Retrieves the primary branch.
const primaryBranch = useMemo(
() => branches?.find((b) => b.primary) || first(branches),
[branches],
);
const provider = { const provider = {
uncategorizedTransactionId, uncategorizedTransactionId,
uncategorizedTransaction, uncategorizedTransaction,
@@ -42,6 +49,7 @@ function CategorizeTransactionBoot({ uncategorizedTransactionId, ...props }) {
accounts, accounts,
isBranchesLoading, isBranchesLoading,
isAccountsLoading, isAccountsLoading,
primaryBranch,
}; };
const isLoading = const isLoading =
isBranchesLoading || isUncategorizedTransactionLoading || isAccountsLoading; isBranchesLoading || isUncategorizedTransactionLoading || isAccountsLoading;

View File

@@ -0,0 +1,22 @@
// @ts-nocheck
import { FFormGroup, FeatureCan } from '@/components';
import { useCategorizeTransactionBoot } from './CategorizeTransactionBoot';
import { Features } from '@/constants';
import { BranchSuggestField } from '@/components/Branches/BranchSuggestField_';
export function CategorizeTransactionBranchField() {
const { branches } = useCategorizeTransactionBoot();
return (
<FFormGroup name={'branchId'} label={'Branch'} fastField inline>
<FeatureCan feature={Features.Branches}>
<BranchSuggestField
name={'branchId'}
items={branches}
popoverProps={{ minimal: true }}
fill
/>
</FeatureCan>
</FFormGroup>
);
}

View File

@@ -24,8 +24,11 @@ function CategorizeTransactionFormRoot({
// #withDrawerActions // #withDrawerActions
closeDrawer, closeDrawer,
}) { }) {
const { uncategorizedTransactionId, uncategorizedTransaction } = const {
useCategorizeTransactionBoot(); uncategorizedTransactionId,
uncategorizedTransaction,
primaryBranch,
} = useCategorizeTransactionBoot();
const { mutateAsync: categorizeTransaction } = useCategorizeTransaction(); const { mutateAsync: categorizeTransaction } = useCategorizeTransaction();
// Callbacks handles form submit. // Callbacks handles form submit.
@@ -43,12 +46,22 @@ function CategorizeTransactionFormRoot({
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
}); });
}) })
.catch(() => { .catch((err) => {
setSubmitting(false); setSubmitting(false);
AppToaster.show({ if (
message: 'Something went wrong!', err.response.data?.errors?.some(
intent: Intent.DANGER, (e) => e.type === 'BRANCH_ID_REQUIRED',
}); )
) {
setErrors({
branchId: 'The branch is required.',
});
} else {
AppToaster.show({
message: 'Something went wrong!',
intent: Intent.DANGER,
});
}
}); });
}; };
// Form initial values in create and edit mode. // Form initial values in create and edit mode.
@@ -60,6 +73,9 @@ function CategorizeTransactionFormRoot({
* as well. * as well.
*/ */
...transformToCategorizeForm(uncategorizedTransaction), ...transformToCategorizeForm(uncategorizedTransaction),
/** Assign the primary branch id as default value. */
branchId: primaryBranch?.id || null,
}; };
return ( return (

View File

@@ -8,6 +8,7 @@ import {
FTextArea, FTextArea,
} from '@/components'; } from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot'; import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
export default function CategorizeTransactionOtherIncome() { export default function CategorizeTransactionOtherIncome() {
const { accounts } = useCategorizeTransactionBoot(); const { accounts } = useCategorizeTransactionBoot();
@@ -68,6 +69,8 @@ export default function CategorizeTransactionOtherIncome() {
fill={true} fill={true}
/> />
</FFormGroup> </FFormGroup>
<CategorizeTransactionBranchField />
</> </>
); );
} }

View File

@@ -8,6 +8,7 @@ import {
FTextArea, FTextArea,
} from '@/components'; } from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot'; import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
export default function CategorizeTransactionOwnerContribution() { export default function CategorizeTransactionOwnerContribution() {
const { accounts } = useCategorizeTransactionBoot(); const { accounts } = useCategorizeTransactionBoot();
@@ -63,6 +64,8 @@ export default function CategorizeTransactionOwnerContribution() {
<FFormGroup name={'description'} label={'Description'} fastField inline> <FFormGroup name={'description'} label={'Description'} fastField inline>
<FTextArea name={'description'} growVertically large fill /> <FTextArea name={'description'} growVertically large fill />
</FFormGroup> </FFormGroup>
<CategorizeTransactionBranchField />
</> </>
); );
} }

View File

@@ -8,6 +8,7 @@ import {
FTextArea, FTextArea,
} from '@/components'; } from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot'; import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
export default function CategorizeTransactionTransferFrom() { export default function CategorizeTransactionTransferFrom() {
const { accounts } = useCategorizeTransactionBoot(); const { accounts } = useCategorizeTransactionBoot();
@@ -47,7 +48,7 @@ export default function CategorizeTransactionTransferFrom() {
inline inline
> >
<AccountsSelect <AccountsSelect
name={'to_account_id'} name={'creditAccountId'}
items={accounts} items={accounts}
filterByRootTypes={['asset']} filterByRootTypes={['asset']}
fastField fastField
@@ -68,6 +69,8 @@ export default function CategorizeTransactionTransferFrom() {
fill={true} fill={true}
/> />
</FFormGroup> </FFormGroup>
<CategorizeTransactionBranchField />
</> </>
); );
} }

View File

@@ -8,6 +8,7 @@ import {
FTextArea, FTextArea,
} from '@/components'; } from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot'; import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
export default function CategorizeTransactionOtherExpense() { export default function CategorizeTransactionOtherExpense() {
const { accounts } = useCategorizeTransactionBoot(); const { accounts } = useCategorizeTransactionBoot();
@@ -68,6 +69,8 @@ export default function CategorizeTransactionOtherExpense() {
fill={true} fill={true}
/> />
</FFormGroup> </FFormGroup>
<CategorizeTransactionBranchField />
</> </>
); );
} }

View File

@@ -8,6 +8,7 @@ import {
FTextArea, FTextArea,
} from '@/components'; } from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot'; import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
export default function CategorizeTransactionOwnerDrawings() { export default function CategorizeTransactionOwnerDrawings() {
const { accounts } = useCategorizeTransactionBoot(); const { accounts } = useCategorizeTransactionBoot();
@@ -68,6 +69,8 @@ export default function CategorizeTransactionOwnerDrawings() {
fill={true} fill={true}
/> />
</FFormGroup> </FFormGroup>
<CategorizeTransactionBranchField />
</> </>
); );
} }

View File

@@ -8,6 +8,7 @@ import {
FTextArea, FTextArea,
} from '@/components'; } from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot'; import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
export default function CategorizeTransactionToAccount() { export default function CategorizeTransactionToAccount() {
const { accounts } = useCategorizeTransactionBoot(); const { accounts } = useCategorizeTransactionBoot();
@@ -49,7 +50,7 @@ export default function CategorizeTransactionToAccount() {
<AccountsSelect <AccountsSelect
name={'creditAccountId'} name={'creditAccountId'}
items={accounts} items={accounts}
filterByRootTypes={['assset']} filterByRootTypes={['asset']}
fastField fastField
fill fill
allowCreate allowCreate
@@ -68,6 +69,8 @@ export default function CategorizeTransactionToAccount() {
fill={true} fill={true}
/> />
</FFormGroup> </FFormGroup>
<CategorizeTransactionBranchField />
</> </>
); );
} }

View File

@@ -11,6 +11,7 @@ export const defaultInitialValues = {
transactionType: '', transactionType: '',
referenceNo: '', referenceNo: '',
description: '', description: '',
branchId: '',
}; };
export const transformToCategorizeForm = (uncategorizedTransaction) => { export const transformToCategorizeForm = (uncategorizedTransaction) => {

View File

@@ -1,32 +0,0 @@
// @ts-nocheck
import React from 'react';
import { Dialog, DialogSuspense } from '@/components';
import withDialogRedux from '@/components/DialogReduxConnect';
import { compose } from '@/utils';
const ConnectBankDialogBody = React.lazy(
() => import('./ConnectBankDialogBody'),
);
/**
* Connect bank dialog.
*/
function ConnectBankDialogRoot({ dialogName, payload = {}, isOpen }) {
return (
<Dialog
name={dialogName}
title={'Securly connect your bank or credit card.'}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
>
<DialogSuspense>
<ConnectBankDialogBody dialogName={dialogName} />
</DialogSuspense>
</Dialog>
);
}
export const ConnectBankDialog = compose(withDialogRedux())(
ConnectBankDialogRoot,
);

View File

@@ -1,61 +0,0 @@
// @ts-nocheck
import * as R from 'ramda';
import { Form, Formik, FormikHelpers } from 'formik';
import classNames from 'classnames';
import { ConnectBankDialogContent } from './ConnectBankDialogContent';
import { useGetPlaidLinkToken } from '@/hooks/query';
import { useSetBankingPlaidToken } from '@/hooks/state/banking';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { CLASSES } from '@/constants';
import { AppToaster } from '@/components';
import { Intent } from '@blueprintjs/core';
import { DialogsName } from '@/constants/dialogs';
const initialValues: ConnectBankDialogForm = {
serviceProvider: 'plaid',
};
interface ConnectBankDialogForm {
serviceProvider: 'plaid';
}
function ConnectBankDialogBodyRoot({
// #withDialogActions
closeDialog,
}) {
const { mutateAsync: getPlaidLinkToken } = useGetPlaidLinkToken();
const setPlaidId = useSetBankingPlaidToken();
// Handles the form submitting.
const handleSubmit = (
values: ConnectBankDialogForm,
{ setSubmitting }: FormikHelpers<ConnectBankDialogForm>,
) => {
setSubmitting(true);
getPlaidLinkToken()
.then((res) => {
setSubmitting(false);
closeDialog(DialogsName.ConnectBankCreditCard);
setPlaidId(res.data.link_token);
})
.catch(() => {
setSubmitting(false);
AppToaster.show({
message: 'Something went wrong.',
intent: Intent.DANGER,
});
});
};
return (
<div className={classNames(CLASSES.DIALOG_BODY)}>
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
<Form>
<ConnectBankDialogContent />
</Form>
</Formik>
</div>
);
}
export default R.compose(withDialogActions)(ConnectBankDialogBodyRoot);

View File

@@ -1,48 +0,0 @@
// @ts-nocheck
import styled from 'styled-components';
import { Stack } from '@/components';
import { TellerIcon } from '../Icons/TellerIcon';
import { YodleeIcon } from '../Icons/YodleeIcon';
import { PlaidIcon } from '../Icons/PlaidIcon';
import { BankServiceCard } from './ConnectBankServiceCard';
const TopDesc = styled('p')`
margin-bottom: 20px;
color: #5f6b7c;
`;
export function ConnectBankDialogContent() {
return (
<div>
<TopDesc>
Connect your bank accounts and fetch the bank transactions using
one of our supported third-party service providers.
</TopDesc>
<Stack>
<BankServiceCard
title={'Plaid (US, UK & Canada)'}
icon={<PlaidIcon />}
>
Plaid gives the connection to 12,000 financial institutions across US, UK and Canada.
</BankServiceCard>
<BankServiceCard
title={'Teller (US) — Soon'}
icon={<TellerIcon />}
disabled
>
Connect instantly with more than 5,000 financial institutions across US.
</BankServiceCard>
<BankServiceCard
title={'Yodlee (Global) — Soon'}
icon={<YodleeIcon />}
disabled
>
Connect instantly with a global network of financial institutions.
</BankServiceCard>
</Stack>
</div>
);
}

View File

@@ -1,72 +0,0 @@
import styled from 'styled-components';
import { Group } from '@/components';
const BankServiceIcon = styled('div')`
height: 40px;
width: 40px;
border: 1px solid #c8cad0;
border-radius: 3px;
display: flex;
svg {
margin: auto;
}
`;
const BankServiceContent = styled(`div`)`
flex: 1 0;
`;
const BankServiceCardRoot = styled('button')`
border-radius: 3px;
border: 1px solid #c8cad0;
transition: all 0.1s ease-in-out;
background: transparent;
text-align: inherit;
padding: 14px;
&:not(:disabled) {
cursor: pointer;
}
&:hover:not(:disabled) {
border-color: #0153cc;
}
&:disabled {
background: #f9fdff;
}
`;
const BankServiceTitle = styled(`h3`)`
font-weight: 600;
font-size: 14px;
color: #2d333d;
`;
const BankServiceDesc = styled('p')`
margin-top: 4px;
margin-bottom: 6px;
font-size: 13px;
color: #738091;
`;
interface BankServiceCardProps {
title: string;
children: React.ReactNode;
disabled?: boolean;
icon: React.ReactNode;
}
export function BankServiceCard({
title,
children,
icon,
disabled,
}: BankServiceCardProps) {
return (
<BankServiceCardRoot disabled={disabled}>
<Group>
<BankServiceIcon>{icon}</BankServiceIcon>
<BankServiceContent>
<BankServiceTitle>{title}</BankServiceTitle>
<BankServiceDesc>{children}</BankServiceDesc>
</BankServiceContent>
</Group>
</BankServiceCardRoot>
);
}

View File

@@ -1 +0,0 @@
export * from './ConnectBankDialog';

View File

@@ -25,6 +25,7 @@ import {
import { useCustomersListContext } from './CustomersListProvider'; import { useCustomersListContext } from './CustomersListProvider';
import { useRefreshCustomers } from '@/hooks/query/customers'; import { useRefreshCustomers } from '@/hooks/query/customers';
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
import withCustomers from './withCustomers'; import withCustomers from './withCustomers';
import withCustomersActions from './withCustomersActions'; import withCustomersActions from './withCustomersActions';
@@ -70,6 +71,9 @@ function CustomerActionsBar({
// Customers refresh action. // Customers refresh action.
const { refresh } = useRefreshCustomers(); const { refresh } = useRefreshCustomers();
// Exports pdf document.
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
const onClickNewCustomer = () => { const onClickNewCustomer = () => {
history.push('/customers/new'); history.push('/customers/new');
}; };
@@ -109,6 +113,10 @@ function CustomerActionsBar({
const handleExportBtnClick = () => { const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'customer' }); openDialog(DialogsName.Export, { resource: 'customer' });
}; };
// Handle the print button click.
const handlePrintBtnClick = () => {
downloadExportPdf({ resource: 'Customer' });
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
@@ -154,6 +162,13 @@ function CustomerActionsBar({
onClick={handleBulkDelete} onClick={handleBulkDelete}
/> />
</If> </If>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
onClick={handlePrintBtnClick}
/>
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />} icon={<Icon icon="file-import-16" iconSize={16} />}

View File

@@ -23,8 +23,11 @@ import {
} from '@/components'; } from '@/components';
import { ExpenseAction, AbilitySubject } from '@/constants/abilityOption'; import { ExpenseAction, AbilitySubject } from '@/constants/abilityOption';
import { DialogsName } from '@/constants/dialogs';
import { useRefreshExpenses } from '@/hooks/query/expenses'; import { useRefreshExpenses } from '@/hooks/query/expenses';
import { useExpensesListContext } from './ExpensesListProvider'; import { useExpensesListContext } from './ExpensesListProvider';
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
import withExpenses from './withExpenses'; import withExpenses from './withExpenses';
import withExpensesActions from './withExpensesActions'; import withExpensesActions from './withExpensesActions';
@@ -33,7 +36,6 @@ import withDialogActions from '@/containers/Dialog/withDialogActions';
import withSettings from '@/containers/Settings/withSettings'; import withSettings from '@/containers/Settings/withSettings';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { DialogsName } from '@/constants/dialogs';
/** /**
* Expenses actions bar. * Expenses actions bar.
@@ -60,6 +62,9 @@ function ExpensesActionsBar({
// Expenses list context. // Expenses list context.
const { expensesViews, fields } = useExpensesListContext(); const { expensesViews, fields } = useExpensesListContext();
// Exports pdf document.
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
// Expenses refresh action. // Expenses refresh action.
const { refresh } = useRefreshExpenses(); const { refresh } = useRefreshExpenses();
@@ -92,6 +97,10 @@ function ExpensesActionsBar({
const handleExportBtnClick = () => { const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'expense' }); openDialog(DialogsName.Export, { resource: 'expense' });
}; };
// Handles the print button click.
const handlePrintBtnClick = () => {
downloadExportPdf({ resource: 'Expense' });
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
@@ -135,11 +144,12 @@ function ExpensesActionsBar({
onClick={handleBulkDelete} onClick={handleBulkDelete}
/> />
</If> </If>
<NavbarDivider />
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon="print-16" iconSize={16} />} icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />} text={<T id={'print'} />}
onClick={handlePrintBtnClick}
/> />
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}

View File

@@ -27,6 +27,7 @@ import {
import { ItemAction, AbilitySubject } from '@/constants/abilityOption'; import { ItemAction, AbilitySubject } from '@/constants/abilityOption';
import { useItemsListContext } from './ItemsListProvider'; import { useItemsListContext } from './ItemsListProvider';
import { useRefreshItems } from '@/hooks/query/items'; import { useRefreshItems } from '@/hooks/query/items';
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
import withItems from './withItems'; import withItems from './withItems';
import withItemsActions from './withItemsActions'; import withItemsActions from './withItemsActions';
@@ -60,11 +61,14 @@ function ItemsActionsBar({
addSetting, addSetting,
// #withDialogActions // #withDialogActions
openDialog openDialog,
}) { }) {
// Items list context. // Items list context.
const { itemsViews, fields } = useItemsListContext(); const { itemsViews, fields } = useItemsListContext();
// Exports pdf document.
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
// Items refresh action. // Items refresh action.
const { refresh } = useRefreshItems(); const { refresh } = useRefreshItems();
@@ -107,7 +111,12 @@ function ItemsActionsBar({
// Handle the export button click. // Handle the export button click.
const handleExportBtnClick = () => { const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'item' }); openDialog(DialogsName.Export, { resource: 'item' });
} };
// Handle the print button click.
const handlePrintBtnClick = () => {
downloadExportPdf({ resource: 'Item' });
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
@@ -153,7 +162,12 @@ function ItemsActionsBar({
onClick={handleBulkDelete} onClick={handleBulkDelete}
/> />
</If> </If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'print-16'} iconSize={'16'} />}
text={<T id={'print'} />}
onClick={handlePrintBtnClick}
/>
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />} icon={<Icon icon="file-import-16" iconSize={16} />}
@@ -204,5 +218,5 @@ export default compose(
})), })),
withItemsActions, withItemsActions,
withAlertActions, withAlertActions,
withDialogActions withDialogActions,
)(ItemsActionsBar); )(ItemsActionsBar);

View File

@@ -28,11 +28,13 @@ import withBills from './withBills';
import withBillsActions from './withBillsActions'; import withBillsActions from './withBillsActions';
import withSettings from '@/containers/Settings/withSettings'; import withSettings from '@/containers/Settings/withSettings';
import withSettingsActions from '@/containers/Settings/withSettingsActions'; import withSettingsActions from '@/containers/Settings/withSettingsActions';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { useBillsListContext } from './BillsListProvider'; import { useBillsListContext } from './BillsListProvider';
import { useRefreshBills } from '@/hooks/query/bills'; import { useRefreshBills } from '@/hooks/query/bills';
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
import { compose } from '@/utils'; import { compose } from '@/utils';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs'; import { DialogsName } from '@/constants/dialogs';
/** /**
@@ -62,11 +64,13 @@ function BillActionsBar({
// Bills list context. // Bills list context.
const { billsViews, fields } = useBillsListContext(); const { billsViews, fields } = useBillsListContext();
// Exports pdf document.
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
// Handle click a new bill. // Handle click a new bill.
const handleClickNewBill = () => { const handleClickNewBill = () => {
history.push('/bills/new'); history.push('/bills/new');
}; };
// Handle tab change. // Handle tab change.
const handleTabChange = (view) => { const handleTabChange = (view) => {
setBillsTableState({ setBillsTableState({
@@ -77,21 +81,22 @@ function BillActionsBar({
const handleRefreshBtnClick = () => { const handleRefreshBtnClick = () => {
refresh(); refresh();
}; };
// Handle table row size change. // Handle table row size change.
const handleTableRowSizeChange = (size) => { const handleTableRowSizeChange = (size) => {
addSetting('bills', 'tableSize', size); addSetting('bills', 'tableSize', size);
}; };
// Handle the import button click. // Handle the import button click.
const handleImportBtnClick = () => { const handleImportBtnClick = () => {
history.push('/bills/import'); history.push('/bills/import');
}; };
// Handle the export button click. // Handle the export button click.
const handleExportBtnClick = () => { const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'bill' }); openDialog(DialogsName.Export, { resource: 'bill' });
}; };
// Handle the print button click.
const handlePrintBtnClick = () => {
downloadExportPdf({ resource: 'Bill' });
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
@@ -133,13 +138,14 @@ function BillActionsBar({
icon={<Icon icon={'trash-16'} iconSize={16} />} icon={<Icon icon={'trash-16'} iconSize={16} />}
text={<T id={'delete'} />} text={<T id={'delete'} />}
intent={Intent.DANGER} intent={Intent.DANGER}
// onClick={handleBulkDelete}
/> />
</If> </If>
<NavbarDivider />
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon={'print-16'} iconSize={'16'} />} icon={<Icon icon={'print-16'} iconSize={'16'} />}
text={<T id={'print'} />} text={<T id={'print'} />}
onClick={handlePrintBtnClick}
/> />
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
@@ -153,7 +159,6 @@ function BillActionsBar({
text={<T id={'export'} />} text={<T id={'export'} />}
onClick={handleExportBtnClick} onClick={handleExportBtnClick}
/> />
<NavbarDivider /> <NavbarDivider />
<DashboardRowsHeightButton <DashboardRowsHeightButton
initialValue={billsTableSize} initialValue={billsTableSize}

View File

@@ -21,6 +21,7 @@ import {
} from '@/components'; } from '@/components';
import { useVendorsCreditNoteListContext } from './VendorsCreditNoteListProvider'; import { useVendorsCreditNoteListContext } from './VendorsCreditNoteListProvider';
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
import { VendorCreditAction, AbilitySubject } from '@/constants/abilityOption'; import { VendorCreditAction, AbilitySubject } from '@/constants/abilityOption';
import withVendorsCreditNotesActions from './withVendorsCreditNotesActions'; import withVendorsCreditNotesActions from './withVendorsCreditNotesActions';
@@ -60,35 +61,37 @@ function VendorsCreditNoteActionsBar({
const { VendorCreditsViews, fields, refresh } = const { VendorCreditsViews, fields, refresh } =
useVendorsCreditNoteListContext(); useVendorsCreditNoteListContext();
// Exports pdf document.
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
// Handle click a new Vendor. // Handle click a new Vendor.
const handleClickNewVendorCredit = () => { const handleClickNewVendorCredit = () => {
history.push('/vendor-credits/new'); history.push('/vendor-credits/new');
}; };
// Handle view tab change. // Handle view tab change.
const handleTabChange = (view) => { const handleTabChange = (view) => {
setVendorCreditsTableState({ viewSlug: view ? view.slug : null }); setVendorCreditsTableState({ viewSlug: view ? view.slug : null });
}; };
// Handle click a refresh credit note. // Handle click a refresh credit note.
const handleRefreshBtnClick = () => { const handleRefreshBtnClick = () => {
refresh(); refresh();
}; };
// Handle table row size change. // Handle table row size change.
const handleTableRowSizeChange = (size) => { const handleTableRowSizeChange = (size) => {
addSetting('vendorCredit', 'tableSize', size); addSetting('vendorCredit', 'tableSize', size);
}; };
// Handle import button click. // Handle import button click.
const handleImportBtnClick = () => { const handleImportBtnClick = () => {
history.push('/vendor-credits/import'); history.push('/vendor-credits/import');
}; };
// Handle the export button click. // Handle the export button click.
const handleExportBtnClick = () => { const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'vendor_credit' }); openDialog(DialogsName.Export, { resource: 'vendor_credit' });
}; };
// Handle the print button click.
const handlePrintBtnClick = () => {
downloadExportPdf({ resource: 'VendorCredit' });
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
@@ -127,6 +130,7 @@ function VendorsCreditNoteActionsBar({
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon={'print-16'} iconSize={'16'} />} icon={<Icon icon={'print-16'} iconSize={'16'} />}
text={<T id={'print'} />} text={<T id={'print'} />}
onClick={handlePrintBtnClick}
/> />
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}

View File

@@ -1,5 +1,4 @@
// @ts-nocheck // @ts-nocheck
import React from 'react';
import { import {
Button, Button,
Classes, Classes,
@@ -22,19 +21,20 @@ import {
DashboardRowsHeightButton, DashboardRowsHeightButton,
DashboardActionsBar, DashboardActionsBar,
} from '@/components'; } from '@/components';
import { PaymentMadeAction, AbilitySubject } from '@/constants/abilityOption';
import withPaymentMade from './withPaymentMade'; import withPaymentMade from './withPaymentMade';
import withPaymentMadeActions from './withPaymentMadeActions'; import withPaymentMadeActions from './withPaymentMadeActions';
import withSettings from '@/containers/Settings/withSettings'; import withSettings from '@/containers/Settings/withSettings';
import withSettingsActions from '@/containers/Settings/withSettingsActions'; import withSettingsActions from '@/containers/Settings/withSettingsActions';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { usePaymentMadesListContext } from './PaymentMadesListProvider'; import { usePaymentMadesListContext } from './PaymentMadesListProvider';
import { useRefreshPaymentMades } from '@/hooks/query/paymentMades'; import { useRefreshPaymentMades } from '@/hooks/query/paymentMades';
import { PaymentMadeAction, AbilitySubject } from '@/constants/abilityOption'; import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
import { compose } from '@/utils';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs'; import { DialogsName } from '@/constants/dialogs';
import { compose } from '@/utils';
/** /**
* Payment made actions bar. * Payment made actions bar.
@@ -57,6 +57,9 @@ function PaymentMadeActionsBar({
}) { }) {
const history = useHistory(); const history = useHistory();
// Exports pdf document.
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
// Payment receives list context. // Payment receives list context.
const { paymentMadesViews, fields } = usePaymentMadesListContext(); const { paymentMadesViews, fields } = usePaymentMadesListContext();
@@ -67,31 +70,30 @@ function PaymentMadeActionsBar({
const handleClickNewPaymentMade = () => { const handleClickNewPaymentMade = () => {
history.push('/payment-mades/new'); history.push('/payment-mades/new');
}; };
// Handle tab changing. // Handle tab changing.
const handleTabChange = (viewSlug) => { const handleTabChange = (viewSlug) => {
setPaymentMadesTableState({ viewSlug }); setPaymentMadesTableState({ viewSlug });
}; };
// Handle click a refresh payment receives. // Handle click a refresh payment receives.
const handleRefreshBtnClick = () => { const handleRefreshBtnClick = () => {
refresh(); refresh();
}; };
// Handle table row size change. // Handle table row size change.
const handleTableRowSizeChange = (size) => { const handleTableRowSizeChange = (size) => {
addSetting('billPayments', 'tableSize', size); addSetting('billPayments', 'tableSize', size);
}; };
// Handle the import button click. // Handle the import button click.
const handleImportBtnClick = () => { const handleImportBtnClick = () => {
history.push('/payment-mades/import'); history.push('/payment-mades/import');
}; };
// Handle the export button click. // Handle the export button click.
const handleExportBtnClick = () => { const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'bill_payment' }); openDialog(DialogsName.Export, { resource: 'bill_payment' });
}; };
// Handle the print button click.
const handlePrintBtnClick = () => {
downloadExportPdf({ resource: 'BillPayment' });
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
@@ -138,6 +140,7 @@ function PaymentMadeActionsBar({
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon={'print-16'} iconSize={'16'} />} icon={<Icon icon={'print-16'} iconSize={'16'} />}
text={<T id={'print'} />} text={<T id={'print'} />}
onClick={handlePrintBtnClick}
/> />
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}

View File

@@ -20,6 +20,8 @@ import {
} from '@/components'; } from '@/components';
import { useCreditNoteListContext } from './CreditNotesListProvider'; import { useCreditNoteListContext } from './CreditNotesListProvider';
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
import { CreditNoteAction, AbilitySubject } from '@/constants/abilityOption'; import { CreditNoteAction, AbilitySubject } from '@/constants/abilityOption';
import withCreditNotes from './withCreditNotes'; import withCreditNotes from './withCreditNotes';
import withCreditNotesActions from './withCreditNotesActions'; import withCreditNotesActions from './withCreditNotesActions';
@@ -27,8 +29,8 @@ import withSettings from '@/containers/Settings/withSettings';
import withSettingsActions from '@/containers/Settings/withSettingsActions'; import withSettingsActions from '@/containers/Settings/withSettingsActions';
import withDialogActions from '@/containers/Dialog/withDialogActions'; import withDialogActions from '@/containers/Dialog/withDialogActions';
import { compose } from '@/utils';
import { DialogsName } from '@/constants/dialogs'; import { DialogsName } from '@/constants/dialogs';
import { compose } from '@/utils';
/** /**
* Credit note table actions bar. * Credit note table actions bar.
@@ -54,6 +56,9 @@ function CreditNotesActionsBar({
// credit note list context. // credit note list context.
const { CreditNotesView, fields, refresh } = useCreditNoteListContext(); const { CreditNotesView, fields, refresh } = useCreditNoteListContext();
// Exports pdf document.
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
// Handle view tab change. // Handle view tab change.
const handleTabChange = (view) => { const handleTabChange = (view) => {
setCreditNotesTableState({ viewSlug: view ? view.slug : null }); setCreditNotesTableState({ viewSlug: view ? view.slug : null });
@@ -68,21 +73,22 @@ function CreditNotesActionsBar({
const handleRefreshBtnClick = () => { const handleRefreshBtnClick = () => {
refresh(); refresh();
}; };
// Handle table row size change. // Handle table row size change.
const handleTableRowSizeChange = (size) => { const handleTableRowSizeChange = (size) => {
addSetting('creditNote', 'tableSize', size); addSetting('creditNote', 'tableSize', size);
}; };
// Handle import button click. // Handle import button click.
const handleImportBtnClick = () => { const handleImportBtnClick = () => {
history.push('/credit-notes/import'); history.push('/credit-notes/import');
}; };
// Handle the export button click. // Handle the export button click.
const handleExportBtnClick = () => { const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'credit_note' }); openDialog(DialogsName.Export, { resource: 'credit_note' });
}; };
// Handle print button click.
const handlePrintBtnClick = () => {
downloadExportPdf({ resource: 'CreditNote' });
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
@@ -121,6 +127,7 @@ function CreditNotesActionsBar({
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon={'print-16'} iconSize={'16'} />} icon={<Icon icon={'print-16'} iconSize={'16'} />}
text={<T id={'print'} />} text={<T id={'print'} />}
onClick={handlePrintBtnClick}
/> />
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}

View File

@@ -26,12 +26,14 @@ import withEstimates from './withEstimates';
import withEstimatesActions from './withEstimatesActions'; import withEstimatesActions from './withEstimatesActions';
import withSettings from '@/containers/Settings/withSettings'; import withSettings from '@/containers/Settings/withSettings';
import withSettingsActions from '@/containers/Settings/withSettingsActions'; import withSettingsActions from '@/containers/Settings/withSettingsActions';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { useEstimatesListContext } from './EstimatesListProvider'; import { useEstimatesListContext } from './EstimatesListProvider';
import { useRefreshEstimates } from '@/hooks/query/estimates'; import { useRefreshEstimates } from '@/hooks/query/estimates';
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
import { SaleEstimateAction, AbilitySubject } from '@/constants/abilityOption'; import { SaleEstimateAction, AbilitySubject } from '@/constants/abilityOption';
import { compose } from '@/utils'; import { compose } from '@/utils';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs'; import { DialogsName } from '@/constants/dialogs';
/** /**
@@ -58,6 +60,9 @@ function EstimateActionsBar({
// Estimates list context. // Estimates list context.
const { estimatesViews, fields } = useEstimatesListContext(); const { estimatesViews, fields } = useEstimatesListContext();
// Exports pdf document.
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
// Handle click a new sale estimate. // Handle click a new sale estimate.
const onClickNewEstimate = () => { const onClickNewEstimate = () => {
history.push('/estimates/new'); history.push('/estimates/new');
@@ -71,17 +76,14 @@ function EstimateActionsBar({
viewSlug: view ? view.slug : null, viewSlug: view ? view.slug : null,
}); });
}; };
// Handle click a refresh sale estimates // Handle click a refresh sale estimates
const handleRefreshBtnClick = () => { const handleRefreshBtnClick = () => {
refresh(); refresh();
}; };
// Handle table row size change. // Handle table row size change.
const handleTableRowSizeChange = (size) => { const handleTableRowSizeChange = (size) => {
addSetting('salesEstimates', 'tableSize', size); addSetting('salesEstimates', 'tableSize', size);
}; };
// Handle the import button click. // Handle the import button click.
const handleImportBtnClick = () => { const handleImportBtnClick = () => {
history.push('/estimates/import'); history.push('/estimates/import');
@@ -90,6 +92,10 @@ function EstimateActionsBar({
const handleExportBtnClick = () => { const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'sale_estimate' }); openDialog(DialogsName.Export, { resource: 'sale_estimate' });
}; };
// Handles the print button click.
const handlePrintBtnClick = () => {
downloadExportPdf({ resource: 'SaleEstimate' });
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
@@ -138,8 +144,8 @@ function EstimateActionsBar({
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon={'print-16'} iconSize={'16'} />} icon={<Icon icon={'print-16'} iconSize={'16'} />}
text={<T id={'print'} />} text={<T id={'print'} />}
onClick={handlePrintBtnClick}
/> />
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon={'file-import-16'} />} icon={<Icon icon={'file-import-16'} />}
@@ -180,5 +186,5 @@ export default compose(
withSettings(({ estimatesSettings }) => ({ withSettings(({ estimatesSettings }) => ({
estimatesTableSize: estimatesSettings?.tableSize, estimatesTableSize: estimatesSettings?.tableSize,
})), })),
withDialogActions withDialogActions,
)(EstimateActionsBar); )(EstimateActionsBar);

View File

@@ -23,6 +23,7 @@ import { SaleInvoiceAction, AbilitySubject } from '@/constants/abilityOption';
import { useRefreshInvoices } from '@/hooks/query/invoices'; import { useRefreshInvoices } from '@/hooks/query/invoices';
import { useInvoicesListContext } from './InvoicesListProvider'; import { useInvoicesListContext } from './InvoicesListProvider';
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
import withInvoices from './withInvoices'; import withInvoices from './withInvoices';
import withInvoiceActions from './withInvoiceActions'; import withInvoiceActions from './withInvoiceActions';
@@ -49,13 +50,16 @@ function InvoiceActionsBar({
addSetting, addSetting,
// #withDialogsActions // #withDialogsActions
openDialog openDialog,
}) { }) {
const history = useHistory(); const history = useHistory();
// Sale invoices list context. // Sale invoices list context.
const { invoicesViews, invoicesFields } = useInvoicesListContext(); const { invoicesViews, invoicesFields } = useInvoicesListContext();
// Exports pdf document.
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
// Handle new invoice button click. // Handle new invoice button click.
const handleClickNewInvoice = () => { const handleClickNewInvoice = () => {
history.push('/invoices/new'); history.push('/invoices/new');
@@ -88,6 +92,10 @@ function InvoiceActionsBar({
const handleExportBtnClick = () => { const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'sale_invoice' }); openDialog(DialogsName.Export, { resource: 'sale_invoice' });
}; };
// Handles the print button click.
const handlePrintBtnClick = () => {
downloadExportPdf({ resource: 'SaleInvoice' });
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
@@ -134,6 +142,7 @@ function InvoiceActionsBar({
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon={'print-16'} iconSize={'16'} />} icon={<Icon icon={'print-16'} iconSize={'16'} />}
text={<T id={'print'} />} text={<T id={'print'} />}
onClick={handlePrintBtnClick}
/> />
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}

View File

@@ -31,8 +31,11 @@ import {
PaymentReceiveAction, PaymentReceiveAction,
AbilitySubject, AbilitySubject,
} from '@/constants/abilityOption'; } from '@/constants/abilityOption';
import { usePaymentReceivesListContext } from './PaymentReceiptsListProvider'; import { usePaymentReceivesListContext } from './PaymentReceiptsListProvider';
import { useRefreshPaymentReceive } from '@/hooks/query/paymentReceives'; import { useRefreshPaymentReceive } from '@/hooks/query/paymentReceives';
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { DialogsName } from '@/constants/dialogs'; import { DialogsName } from '@/constants/dialogs';
@@ -61,6 +64,9 @@ function PaymentReceiveActionsBar({
// Payment receives list context. // Payment receives list context.
const { paymentReceivesViews, fields } = usePaymentReceivesListContext(); const { paymentReceivesViews, fields } = usePaymentReceivesListContext();
// Exports pdf document.
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
// Handle new payment button click. // Handle new payment button click.
const handleClickNewPaymentReceive = () => { const handleClickNewPaymentReceive = () => {
history.push('/payment-receives/new'); history.push('/payment-receives/new');
@@ -91,6 +97,10 @@ function PaymentReceiveActionsBar({
const handleExportBtnClick = () => { const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'payment_receive' }); openDialog(DialogsName.Export, { resource: 'payment_receive' });
}; };
// Handles the print button click.
const handlePrintBtnClick = () => {
downloadExportPdf({ resource: 'PaymentReceive' });
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
@@ -137,6 +147,7 @@ function PaymentReceiveActionsBar({
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon={'print-16'} iconSize={'16'} />} icon={<Icon icon={'print-16'} iconSize={'16'} />}
text={<T id={'print'} />} text={<T id={'print'} />}
onClick={handlePrintBtnClick}
/> />
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}

View File

@@ -29,14 +29,15 @@ import withReceipts from './withReceipts';
import withReceiptsActions from './withReceiptsActions'; import withReceiptsActions from './withReceiptsActions';
import withSettings from '@/containers/Settings/withSettings'; import withSettings from '@/containers/Settings/withSettings';
import withSettingsActions from '@/containers/Settings/withSettingsActions'; import withSettingsActions from '@/containers/Settings/withSettingsActions';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { useReceiptsListContext } from './ReceiptsListProvider'; import { useReceiptsListContext } from './ReceiptsListProvider';
import { useRefreshReceipts } from '@/hooks/query/receipts'; import { useRefreshReceipts } from '@/hooks/query/receipts';
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
import { SaleReceiptAction, AbilitySubject } from '@/constants/abilityOption'; import { SaleReceiptAction, AbilitySubject } from '@/constants/abilityOption';
import { compose } from '@/utils';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs'; import { DialogsName } from '@/constants/dialogs';
import { compose } from '@/utils';
/** /**
* Receipts actions bar. * Receipts actions bar.
@@ -62,6 +63,9 @@ function ReceiptActionsBar({
// Sale receipts list context. // Sale receipts list context.
const { receiptsViews, fields } = useReceiptsListContext(); const { receiptsViews, fields } = useReceiptsListContext();
// Exports pdf document.
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
// Handle new receipt button click. // Handle new receipt button click.
const onClickNewReceipt = () => { const onClickNewReceipt = () => {
history.push('/receipts/new'); history.push('/receipts/new');
@@ -95,6 +99,10 @@ function ReceiptActionsBar({
const handleExportBtnClick = () => { const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'sale_receipt' }); openDialog(DialogsName.Export, { resource: 'sale_receipt' });
}; };
// Handle print button click.
const handlePrintButtonClick = () => {
downloadExportPdf({ resource: 'SaleReceipt' });
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
@@ -143,8 +151,8 @@ function ReceiptActionsBar({
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon={'print-16'} iconSize={'16'} />} icon={<Icon icon={'print-16'} iconSize={'16'} />}
text={<T id={'print'} />} text={<T id={'print'} />}
onClick={handlePrintButtonClick}
/> />
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon={'file-import-16'} />} icon={<Icon icon={'file-import-16'} />}

View File

@@ -22,10 +22,12 @@ import {
AdvancedFilterPopover, AdvancedFilterPopover,
} from '@/components'; } from '@/components';
import { useRefreshVendors } from '@/hooks/query/vendors';
import { VendorAction, AbilitySubject } from '@/constants/abilityOption'; import { VendorAction, AbilitySubject } from '@/constants/abilityOption';
import { useRefreshVendors } from '@/hooks/query/vendors';
import { useVendorsListContext } from './VendorsListProvider'; import { useVendorsListContext } from './VendorsListProvider';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
import withVendors from './withVendors'; import withVendors from './withVendors';
import withVendorsActions from './withVendorsActions'; import withVendorsActions from './withVendorsActions';
@@ -61,11 +63,13 @@ function VendorActionsBar({
// Vendors list context. // Vendors list context.
const { vendorsViews, fields } = useVendorsListContext(); const { vendorsViews, fields } = useVendorsListContext();
// Exports pdf document.
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
// Handles new vendor button click. // Handles new vendor button click.
const onClickNewVendor = () => { const onClickNewVendor = () => {
history.push('/vendors/new'); history.push('/vendors/new');
}; };
// Vendors refresh action. // Vendors refresh action.
const { refresh } = useRefreshVendors(); const { refresh } = useRefreshVendors();
@@ -73,31 +77,30 @@ function VendorActionsBar({
const handleTabChange = (viewSlug) => { const handleTabChange = (viewSlug) => {
setVendorsTableState({ viewSlug }); setVendorsTableState({ viewSlug });
}; };
// Handle inactive switch changing. // Handle inactive switch changing.
const handleInactiveSwitchChange = (event) => { const handleInactiveSwitchChange = (event) => {
const checked = event.target.checked; const checked = event.target.checked;
setVendorsTableState({ inactiveMode: checked }); setVendorsTableState({ inactiveMode: checked });
}; };
// Handle click a refresh sale estimates // Handle click a refresh sale estimates
const handleRefreshBtnClick = () => { const handleRefreshBtnClick = () => {
refresh(); refresh();
}; };
const handleTableRowSizeChange = (size) => { const handleTableRowSizeChange = (size) => {
addSetting('vendors', 'tableSize', size); addSetting('vendors', 'tableSize', size);
}; };
// Handle import button success. // Handle import button success.
const handleImportBtnSuccess = () => { const handleImportBtnSuccess = () => {
history.push('/vendors/import'); history.push('/vendors/import');
}; };
// Handle the export button click. // Handle the export button click.
const handleExportBtnClick = () => { const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'vendor' }); openDialog(DialogsName.Export, { resource: 'vendor' });
}; };
// Handle the print button click.
const handlePrintBtnClick = () => {
downloadExportPdf({ resource: 'Vendor' });
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
@@ -140,6 +143,13 @@ function VendorActionsBar({
intent={Intent.DANGER} intent={Intent.DANGER}
/> />
</If> </If>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
onClick={handlePrintBtnClick}
/>
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />} icon={<Icon icon="file-import-16" iconSize={16} />}

View File

@@ -0,0 +1,62 @@
// @ts-nocheck
import { downloadFile } from '@/hooks/useDownloadFile';
import useApiRequest from '@/hooks/useRequest';
import { AxiosError } from 'axios';
import { useMutation } from 'react-query';
import { asyncToastProgress } from '@/utils/async-toast-progress';
interface ResourceExportValues {
resource: string;
}
/**
* Initiates a download of the balance sheet in XLSX format.
* @param {Object} query - The query parameters for the request.
* @param {Object} args - Additional configurations for the download.
* @returns {Function} A function to trigger the file download.
*/
export const useResourceExportPdf = (props) => {
const apiRequest = useApiRequest();
return useMutation<void, AxiosError, any>((data: ResourceExportValues) => {
return apiRequest.get(
'/export',
{
responseType: 'blob',
headers: {
accept: 'application/pdf',
},
params: {
resource: data.resource,
format: data.format,
},
},
props,
);
});
};
export const useDownloadExportPdf = () => {
const { startProgress, stopProgress } = asyncToastProgress();
const resourceExportPdfMutation = useResourceExportPdf({
onMutate: () => {},
});
const { mutateAsync, isLoading: isExportPdfLoading } =
resourceExportPdfMutation;
const downloadAsync = (values) => {
if (!isExportPdfLoading) {
startProgress();
return mutateAsync(values).then((res) => {
downloadFile(res.data, `${values.resource}.pdf`);
stopProgress();
return res;
});
}
};
return {
...resourceExportPdfMutation,
downloadAsync,
};
};

View File

@@ -134,7 +134,6 @@ const invalidateResourcesOnImport = (
queryClient: QueryClient, queryClient: QueryClient,
resource: string, resource: string,
) => { ) => {
debugger;
switch (resource) { switch (resource) {
case 'Item': case 'Item':
queryClient.invalidateQueries(T.ITEMS); queryClient.invalidateQueries(T.ITEMS);

View File

@@ -0,0 +1,25 @@
import { useCallback } from 'react';
import { useSetBankingPlaidToken } from '../state/banking';
import { AppToaster } from '@/components';
import { useGetPlaidLinkToken } from '../query';
import { Intent } from '@blueprintjs/core';
export const useOpenPlaidConnect = () => {
const { mutateAsync: getPlaidLinkToken, isLoading } = useGetPlaidLinkToken();
const setPlaidId = useSetBankingPlaidToken();
const openPlaidAsync = useCallback(() => {
return getPlaidLinkToken()
.then((res) => {
setPlaidId(res.data.link_token);
})
.catch(() => {
AppToaster.show({
message: 'Something went wrong.',
intent: Intent.DANGER,
});
});
}, [getPlaidLinkToken, setPlaidId]);
return { openPlaidAsync, isPlaidLoading: isLoading };
};

View File

@@ -190,7 +190,10 @@ $dashboard-views-bar-height: 44px;
background: #a7b6c21f; background: #a7b6c21f;
color: #32304a; color: #32304a;
} }
&.bp4-disabled{
background: transparent;
color: rgba(50, 48, 74, 0.4);
}
&.has-active-filters { &.has-active-filters {
&, &,

View File

@@ -0,0 +1,76 @@
import { isUndefined } from 'lodash';
import clsx from 'classnames';
import {
Classes,
Intent,
ProgressBar,
Text,
ToastProps,
} from '@blueprintjs/core';
import { AppToaster } from '@/components';
interface AsyncToastProgress {
renderProgressProps?: (amount: number) => ToastProps;
}
export function asyncToastProgress({
renderProgressProps,
}: AsyncToastProgress = {}) {
let progressToastInterval: number;
let progress = 0;
let key: string = '';
const renderProgress = (amount: number): ToastProps => {
const customProgressProps = !isUndefined(renderProgressProps)
? renderProgressProps(amount)
: {};
return {
icon: 'hand',
message: (
<>
<Text style={{ fontSize: 12, marginBottom: 6 }}>
Preparing the document.
</Text>
<ProgressBar
className={clsx({
[Classes.PROGRESS_NO_STRIPES]: amount >= 100,
})}
intent={amount < 100 ? Intent.PRIMARY : Intent.SUCCESS}
value={amount / 100}
/>
</>
),
onDismiss: (didTimeoutExpire: boolean) => {
if (!didTimeoutExpire) {
// user dismissed toast with click
window.clearInterval(progressToastInterval);
}
},
timeout: amount < 100 ? 0 : 2000,
...customProgressProps,
};
};
const startProgress = () => {
key = AppToaster.show(renderProgress(0));
progressToastInterval = window.setInterval(() => {
if (progress > 100) {
window.clearInterval(progressToastInterval);
} else {
progress += 10 + Math.random() * 20;
progress = Math.min(progress, 95); // Ensure progress never reaches 100
AppToaster.show(renderProgress(progress), key);
}
}, 1000);
};
const stopProgress = () => {
progress = 100;
AppToaster.show(renderProgress(progress), key);
window.clearInterval(progressToastInterval);
};
return { startProgress, stopProgress };
}