mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 13:50:31 +00:00
Compare commits
33 Commits
dependabot
...
v0.17.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8862810706 | ||
|
|
3dadbeac4d | ||
|
|
494d2c1fe0 | ||
|
|
d27562bd43 | ||
|
|
fc9995c4da | ||
|
|
7dc769004d | ||
|
|
909a70e2c5 | ||
|
|
84dd0fa86b | ||
|
|
a4719fe15b | ||
|
|
fd915b503f | ||
|
|
bbba54c08e | ||
|
|
f241e2bede | ||
|
|
175bc243f3 | ||
|
|
7c06c8bb8a | ||
|
|
8fd930caac | ||
|
|
e175307da4 | ||
|
|
b1bf932e88 | ||
|
|
aa897212ab | ||
|
|
890903e08b | ||
|
|
16b2a33cf6 | ||
|
|
382d4ab028 | ||
|
|
85f26e1079 | ||
|
|
1b237323f6 | ||
|
|
f15fecde54 | ||
|
|
79be4266bb | ||
|
|
08ad117331 | ||
|
|
958f78e7a4 | ||
|
|
ba77351e44 | ||
|
|
09a15966f0 | ||
|
|
7ff36e8c4f | ||
|
|
dc5bdf0b66 | ||
|
|
fe41f7976d | ||
|
|
2c7da86a00 |
30
.env.example
30
.env.example
@@ -75,31 +75,17 @@ PLAID_ENV=sandbox
|
||||
# Your Plaid keys, which can be found in the Plaid Dashboard.
|
||||
# https://dashboard.plaid.com/account/keys
|
||||
PLAID_CLIENT_ID=
|
||||
PLAID_SECRET_DEVELOPMENT=
|
||||
PLAID_SECRET_SANDBOX=
|
||||
|
||||
PLAID_SECRET=
|
||||
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
|
||||
LEMONSQUEEZY_API_KEY=
|
||||
LEMONSQUEEZY_STORE_ID=
|
||||
LEMONSQUEEZY_WEBHOOK_SECRET=
|
||||
|
||||
# S3 documents and attachments
|
||||
S3_REGION=
|
||||
S3_ACCESS_KEY_ID=
|
||||
S3_SECRET_ACCESS_KEY=
|
||||
S3_ENDPOINT=
|
||||
S3_BUCKET=
|
||||
127
.github/workflows/build-deploy-develop-container.yaml
vendored
Normal file
127
.github/workflows/build-deploy-develop-container.yaml
vendored
Normal 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 }}
|
||||
35
CHANGELOG.md
35
CHANGELOG.md
@@ -2,6 +2,41 @@
|
||||
|
||||
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
|
||||
|
||||
### improvements
|
||||
|
||||
@@ -22,11 +22,15 @@ services:
|
||||
- server
|
||||
- webapp
|
||||
restart: on-failure
|
||||
networks:
|
||||
- bigcapital_network
|
||||
|
||||
webapp:
|
||||
container_name: bigcapital-webapp
|
||||
image: bigcapitalhq/webapp:latest
|
||||
restart: on-failure
|
||||
networks:
|
||||
- bigcapital_network
|
||||
|
||||
server:
|
||||
container_name: bigcapital-server
|
||||
@@ -89,14 +93,17 @@ services:
|
||||
- GOTENBERG_URL=${GOTENBERG_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
|
||||
- BANKING_CONNECT=${BANKING_CONNECT}
|
||||
|
||||
# Plaid
|
||||
- PLAID_ENV=${PLAID_ENV}
|
||||
- PLAID_CLIENT_ID=${PLAID_CLIENT_ID}
|
||||
- PLAID_SECRET_DEVELOPMENT=${PLAID_SECRET_DEVELOPMENT}
|
||||
- PLAID_SECRET_SANDBOX=${b8cf42b441e110451e2f69ad7e1e9f}
|
||||
- PLAID_SECRET=${PLAID_SECRET}
|
||||
- PLAID_LINK_WEBHOOK=${PLAID_LINK_WEBHOOK}
|
||||
|
||||
# Lemon Squeez
|
||||
@@ -114,6 +121,15 @@ services:
|
||||
- NEW_RELIC_LICENSE_KEY=${NEW_RELIC_LICENSE_KEY}
|
||||
- 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:
|
||||
container_name: bigcapital-database-migration
|
||||
build:
|
||||
@@ -130,6 +146,8 @@ services:
|
||||
- TENANT_DB_NAME_PERFIX=${TENANT_DB_NAME_PERFIX}
|
||||
depends_on:
|
||||
- mysql
|
||||
networks:
|
||||
- bigcapital_network
|
||||
|
||||
mysql:
|
||||
container_name: bigcapital-mysql
|
||||
@@ -145,6 +163,8 @@ services:
|
||||
- mysql:/var/lib/mysql
|
||||
expose:
|
||||
- '3306'
|
||||
networks:
|
||||
- bigcapital_network
|
||||
|
||||
mongo:
|
||||
container_name: bigcapital-mongo
|
||||
@@ -154,6 +174,8 @@ services:
|
||||
- '27017'
|
||||
volumes:
|
||||
- mongo:/var/lib/mongodb
|
||||
networks:
|
||||
- bigcapital_network
|
||||
|
||||
redis:
|
||||
container_name: bigcapital-redis
|
||||
@@ -164,11 +186,15 @@ services:
|
||||
- '6379'
|
||||
volumes:
|
||||
- redis:/data
|
||||
networks:
|
||||
- bigcapital_network
|
||||
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:7
|
||||
expose:
|
||||
- '9000'
|
||||
networks:
|
||||
- bigcapital_network
|
||||
|
||||
# Volumes
|
||||
volumes:
|
||||
@@ -183,3 +209,8 @@ volumes:
|
||||
redis:
|
||||
name: bigcapital_prod_redis
|
||||
driver: local
|
||||
|
||||
# Networks
|
||||
networks:
|
||||
bigcapital_network:
|
||||
driver: bridge
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -70,6 +70,10 @@ module.exports = {
|
||||
src: `${RESOURCES_PATH}/scss/modules/financial-sheet.scss`,
|
||||
dest: `${RESOURCES_PATH}/css/modules`,
|
||||
},
|
||||
{
|
||||
src: `${RESOURCES_PATH}/scss/modules/export-resource-table.scss`,
|
||||
dest: `${RESOURCES_PATH}/css/modules`,
|
||||
},
|
||||
],
|
||||
// RTL builds.
|
||||
rtl: [
|
||||
|
||||
@@ -4,12 +4,16 @@ import { Router, Response, NextFunction, Request } from 'express';
|
||||
import { body, param } from 'express-validator';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { AttachmentsApplication } from '@/services/Attachments/AttachmentsApplication';
|
||||
import { AttachmentUploadPipeline } from '@/services/Attachments/S3UploadPipeline';
|
||||
|
||||
@Service()
|
||||
export class AttachmentsController extends BaseController {
|
||||
@Inject()
|
||||
private attachmentsApplication: AttachmentsApplication;
|
||||
|
||||
@Inject()
|
||||
private uploadPipelineService: AttachmentUploadPipeline;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
@@ -18,7 +22,8 @@ export class AttachmentsController extends BaseController {
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
this.attachmentsApplication.uploadPipeline.single('file'),
|
||||
this.uploadPipelineService.validateS3Configured,
|
||||
this.uploadPipelineService.uploadPipeline().single('file'),
|
||||
this.validateUploadedFileExistance,
|
||||
this.uploadAttachment.bind(this)
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import BaseController from '@/api/controllers/BaseController';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { ExportApplication } from '@/services/Export/ExportApplication';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
import { convertAcceptFormatToFormat } from './_utils';
|
||||
|
||||
@Service()
|
||||
export class ExportController extends BaseController {
|
||||
@@ -25,7 +26,6 @@ export class ExportController extends BaseController {
|
||||
],
|
||||
this.validationResult,
|
||||
this.export.bind(this),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
@@ -48,10 +48,12 @@ export class ExportController extends BaseController {
|
||||
ACCEPT_TYPE.APPLICATION_CSV,
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
]);
|
||||
const applicationFormat = convertAcceptFormatToFormat(acceptType);
|
||||
|
||||
const data = await this.exportResourceApp.export(
|
||||
tenantId,
|
||||
query.resource,
|
||||
acceptType === ACCEPT_TYPE.APPLICATION_XLSX ? 'xlsx' : 'csv'
|
||||
applicationFormat
|
||||
);
|
||||
// Retrieves the csv format.
|
||||
if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
|
||||
@@ -70,31 +72,16 @@ export class ExportController extends BaseController {
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
13
packages/server/src/api/controllers/Export/_utils.ts
Normal file
13
packages/server/src/api/controllers/Export/_utils.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Router } from 'express';
|
||||
import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication';
|
||||
import { Request, Response } from 'express';
|
||||
import { NextFunction, Router, Request, Response } from 'express';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication';
|
||||
import BaseController from '../BaseController';
|
||||
import { LemonSqueezyWebhooks } from '@/services/Subscription/LemonSqueezyWebhooks';
|
||||
import { PlaidWebhookTenantBootMiddleware } from '@/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware';
|
||||
@@ -34,7 +33,7 @@ export class Webhooks extends BaseController {
|
||||
* @param {Response} res
|
||||
* @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 signature = req.headers['x-signature'] ?? '';
|
||||
const rawBody = req.rawBody;
|
||||
@@ -57,20 +56,25 @@ export class Webhooks extends BaseController {
|
||||
* @param {Response} res
|
||||
* @returns {Response}
|
||||
*/
|
||||
public async plaidWebhooks(req: Request, res: Response) {
|
||||
public async plaidWebhooks(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const {
|
||||
webhook_type: webhookType,
|
||||
webhook_code: webhookCode,
|
||||
item_id: plaidItemId,
|
||||
} = req.body;
|
||||
|
||||
await this.plaidApp.webhooks(
|
||||
tenantId,
|
||||
plaidItemId,
|
||||
webhookType,
|
||||
webhookCode
|
||||
);
|
||||
return res.status(200).send({ code: 200, message: 'ok' });
|
||||
try {
|
||||
const {
|
||||
webhook_type: webhookType,
|
||||
webhook_code: webhookCode,
|
||||
item_id: plaidItemId,
|
||||
} = req.body;
|
||||
|
||||
await this.plaidApp.webhooks(
|
||||
tenantId,
|
||||
plaidItemId,
|
||||
webhookType,
|
||||
webhookCode
|
||||
);
|
||||
return res.status(200).send({ code: 200, message: 'ok' });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,10 @@ function getAllSystemTenants(knex) {
|
||||
return knex('tenants');
|
||||
}
|
||||
|
||||
function getAllInitializedSystemTenants(knex) {
|
||||
return knex('tenants').whereNotNull('initializedAt');
|
||||
}
|
||||
|
||||
// module.exports = {
|
||||
// log,
|
||||
// success,
|
||||
@@ -183,7 +187,7 @@ commander
|
||||
.action(async (cmd) => {
|
||||
try {
|
||||
const sysKnex = await initSystemKnex();
|
||||
const tenants = await getAllSystemTenants(sysKnex);
|
||||
const tenants = await getAllInitializedSystemTenants(sysKnex);
|
||||
const tenantsOrgsIds = tenants.map((tenant) => tenant.organizationId);
|
||||
|
||||
if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) {
|
||||
@@ -220,7 +224,6 @@ commander
|
||||
const oper = migrateTenant(cmd.tenant_id);
|
||||
migrateOpers.push(oper);
|
||||
}
|
||||
|
||||
Promise.all(migrateOpers).then(() => {
|
||||
success('All tenants are migrated.');
|
||||
});
|
||||
@@ -280,4 +283,3 @@ commander
|
||||
exit(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -204,10 +204,7 @@ module.exports = {
|
||||
plaid: {
|
||||
env: process.env.PLAID_ENV || 'sandbox',
|
||||
clientId: process.env.PLAID_CLIENT_ID,
|
||||
secretDevelopment: process.env.PLAID_SECRET_DEVELOPMENT,
|
||||
secretSandbox: process.env.PLAID_SECRET_SANDBOX,
|
||||
redirectSandBox: process.env.PLAID_SANDBOX_REDIRECT_URI,
|
||||
redirectDevelopment: process.env.PLAID_DEVELOPMENT_REDIRECT_URI,
|
||||
secret: process.env.PLAID_SECRET,
|
||||
linkWebhook: process.env.PLAID_LINK_WEBHOOK,
|
||||
},
|
||||
|
||||
@@ -218,6 +215,7 @@ module.exports = {
|
||||
key: process.env.LEMONSQUEEZY_API_KEY,
|
||||
storeId: process.env.LEMONSQUEEZY_STORE_ID,
|
||||
webhookSecret: process.env.LEMONSQUEEZY_WEBHOOK_SECRET,
|
||||
redirectTo: `${process.env.BASE_URL}/setup`,
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -233,10 +231,10 @@ module.exports = {
|
||||
* S3 for documents.
|
||||
*/
|
||||
s3: {
|
||||
region: process.env.AWS_REGION,
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
||||
endpoint: process.env.AWS_ENDPOINT,
|
||||
bucket: process.env.AWS_BUCKET,
|
||||
region: process.env.S3_REGION,
|
||||
accessKeyId: process.env.S3_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
||||
endpoint: process.env.S3_ENDPOINT,
|
||||
bucket: process.env.S3_BUCKET || 'bigcapital-documents',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.dropTableIfExists('storage');
|
||||
};
|
||||
|
||||
exports.down = function (knex) {};
|
||||
@@ -3,7 +3,7 @@ exports.up = function (knex) {
|
||||
table.increments('id').primary();
|
||||
table.string('key').notNullable();
|
||||
table.string('mime_type').notNullable();
|
||||
table.integer('size').unsigned().notNullable();
|
||||
table.integer('size').unsigned();
|
||||
table.string('origin_name');
|
||||
table.timestamps();
|
||||
});
|
||||
@@ -164,3 +164,7 @@ export enum TaxRateAction {
|
||||
DELETE = 'Delete',
|
||||
VIEW = 'View',
|
||||
}
|
||||
|
||||
export interface CreateAccountParams {
|
||||
ignoreUniqueName: boolean;
|
||||
}
|
||||
|
||||
@@ -122,6 +122,10 @@ export type IModelMetaCollectionField = IModelMetaCollectionFieldCommon &
|
||||
export type IModelMetaRelationField = IModelMetaRelationFieldCommon &
|
||||
IModelMetaRelationEnumerationField;
|
||||
|
||||
interface IModelPrintMeta{
|
||||
pageTitle: string;
|
||||
}
|
||||
|
||||
export interface IModelMeta {
|
||||
defaultFilterField: string;
|
||||
defaultSort: IModelMetaDefaultSort;
|
||||
@@ -134,6 +138,8 @@ export interface IModelMeta {
|
||||
importAggregateOn?: string;
|
||||
importAggregateBy?: string;
|
||||
|
||||
print?: IModelPrintMeta;
|
||||
|
||||
fields: { [key: string]: IModelMetaField };
|
||||
columns: { [key: string]: IModelMetaColumn };
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ export interface ISystemUser extends Model {
|
||||
password: string;
|
||||
email: string;
|
||||
|
||||
verifyToken: string;
|
||||
verified: boolean;
|
||||
|
||||
roleId: number;
|
||||
tenantId: number;
|
||||
|
||||
|
||||
@@ -70,10 +70,7 @@ export class PlaidClientWrapper {
|
||||
baseOptions: {
|
||||
headers: {
|
||||
'PLAID-CLIENT-ID': config.plaid.clientId,
|
||||
'PLAID-SECRET':
|
||||
config.plaid.env === 'development'
|
||||
? config.plaid.secretDevelopment
|
||||
: config.plaid.secretSandbox,
|
||||
'PLAID-SECRET': config.plaid.secret,
|
||||
'Plaid-Version': '2020-09-14',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,6 +8,9 @@ export default {
|
||||
},
|
||||
importable: true,
|
||||
exportable: true,
|
||||
print: {
|
||||
pageTitle: 'Chart of Accounts',
|
||||
},
|
||||
fields: {
|
||||
name: {
|
||||
name: 'account.field.name',
|
||||
@@ -121,7 +124,7 @@ export default {
|
||||
},
|
||||
balance: {
|
||||
name: 'account.field.balance',
|
||||
accessor: 'amount',
|
||||
accessor: 'formattedAmount',
|
||||
},
|
||||
description: {
|
||||
name: 'account.field.description',
|
||||
@@ -133,6 +136,7 @@ export default {
|
||||
},
|
||||
createdAt: {
|
||||
name: 'account.field.created_at',
|
||||
printable: false,
|
||||
},
|
||||
},
|
||||
fields2: {
|
||||
|
||||
@@ -10,6 +10,9 @@ export default {
|
||||
importAggregator: 'group',
|
||||
importAggregateOn: 'entries',
|
||||
importAggregateBy: 'billNumber',
|
||||
print: {
|
||||
pageTitle: 'Bills',
|
||||
},
|
||||
fields: {
|
||||
vendor: {
|
||||
name: 'bill.field.vendor',
|
||||
@@ -83,6 +86,10 @@ export default {
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
billDate: {
|
||||
name: 'Date',
|
||||
accessor: 'formattedBillDate',
|
||||
},
|
||||
billNumber: {
|
||||
name: 'Bill No.',
|
||||
type: 'text',
|
||||
@@ -91,13 +98,10 @@ export default {
|
||||
name: 'Reference No.',
|
||||
type: 'text',
|
||||
},
|
||||
billDate: {
|
||||
name: 'Date',
|
||||
type: 'date',
|
||||
},
|
||||
dueDate: {
|
||||
name: 'Due Date',
|
||||
type: 'date',
|
||||
accessor: 'formattedDueDate',
|
||||
},
|
||||
vendorId: {
|
||||
name: 'Vendor',
|
||||
@@ -111,10 +115,12 @@ export default {
|
||||
exchangeRate: {
|
||||
name: 'Exchange Rate',
|
||||
type: 'number',
|
||||
printable: false,
|
||||
},
|
||||
currencyCode: {
|
||||
name: 'Currency Code',
|
||||
type: 'text',
|
||||
printable: false,
|
||||
},
|
||||
dueAmount: {
|
||||
name: 'Due Amount',
|
||||
@@ -127,10 +133,12 @@ export default {
|
||||
note: {
|
||||
name: 'Note',
|
||||
type: 'text',
|
||||
printable: false,
|
||||
},
|
||||
open: {
|
||||
name: 'Open',
|
||||
type: 'boolean',
|
||||
printable: false,
|
||||
},
|
||||
entries: {
|
||||
name: 'Entries',
|
||||
|
||||
@@ -77,6 +77,7 @@ export default {
|
||||
paymentDate: {
|
||||
name: 'bill_payment.field.payment_date',
|
||||
type: 'date',
|
||||
accessor: 'formattedPaymentDate'
|
||||
},
|
||||
paymentNumber: {
|
||||
name: 'bill_payment.field.payment_number',
|
||||
@@ -94,14 +95,17 @@ export default {
|
||||
currencyCode: {
|
||||
name: 'Currency Code',
|
||||
type: 'text',
|
||||
printable: false,
|
||||
},
|
||||
exchangeRate: {
|
||||
name: 'bill_payment.field.exchange_rate',
|
||||
type: 'number',
|
||||
printable: false,
|
||||
},
|
||||
statement: {
|
||||
name: 'bill_payment.field.note',
|
||||
type: 'text',
|
||||
printable: false,
|
||||
},
|
||||
reference: {
|
||||
name: 'bill_payment.field.reference',
|
||||
|
||||
@@ -20,6 +20,10 @@ export default {
|
||||
importAggregateOn: 'entries',
|
||||
importAggregateBy: 'creditNoteNumber',
|
||||
|
||||
print: {
|
||||
pageTitle: 'Credit Notes',
|
||||
},
|
||||
|
||||
fields: {
|
||||
customer: {
|
||||
name: 'credit_note.field.customer',
|
||||
@@ -88,36 +92,34 @@ export default {
|
||||
columns: {
|
||||
customer: {
|
||||
name: 'Customer',
|
||||
type: 'relation',
|
||||
accessor: 'customer.displayName',
|
||||
},
|
||||
exchangeRate: {
|
||||
name: 'Exchange Rate',
|
||||
type: 'number',
|
||||
printable: false,
|
||||
},
|
||||
creditNoteDate: {
|
||||
name: 'Credit Note Date',
|
||||
type: 'date',
|
||||
accessor: 'formattedCreditNoteDate'
|
||||
},
|
||||
referenceNo: {
|
||||
name: 'Reference No.',
|
||||
type: 'text',
|
||||
},
|
||||
note: {
|
||||
name: 'Note',
|
||||
type: 'text',
|
||||
},
|
||||
termsConditions: {
|
||||
name: 'Terms & Conditions',
|
||||
type: 'text',
|
||||
printable: false,
|
||||
},
|
||||
creditNoteNumber: {
|
||||
name: 'Credit Note Number',
|
||||
type: 'text',
|
||||
printable: false,
|
||||
},
|
||||
open: {
|
||||
name: 'Open',
|
||||
type: 'boolean',
|
||||
printable: false,
|
||||
},
|
||||
entries: {
|
||||
name: 'Entries',
|
||||
|
||||
@@ -6,6 +6,9 @@ export default {
|
||||
sortOrder: 'DESC',
|
||||
sortField: 'created_at',
|
||||
},
|
||||
print: {
|
||||
pageTitle: 'Customers',
|
||||
},
|
||||
fields: {
|
||||
first_name: {
|
||||
name: 'vendor.field.first_name',
|
||||
@@ -127,100 +130,121 @@ export default {
|
||||
balance: {
|
||||
name: 'vendor.field.balance',
|
||||
type: 'number',
|
||||
accessor: 'formattedBalance',
|
||||
},
|
||||
openingBalance: {
|
||||
name: 'vendor.field.opening_balance',
|
||||
type: 'number',
|
||||
printable: false
|
||||
},
|
||||
openingBalanceAt: {
|
||||
name: 'vendor.field.opening_balance_at',
|
||||
type: 'date',
|
||||
printable: false
|
||||
},
|
||||
currencyCode: {
|
||||
name: 'vendor.field.currency',
|
||||
type: 'text',
|
||||
printable: false
|
||||
},
|
||||
status: {
|
||||
name: 'vendor.field.status',
|
||||
printable: false
|
||||
},
|
||||
note: {
|
||||
name: 'vendor.field.note',
|
||||
printable: false
|
||||
},
|
||||
// Billing Address
|
||||
billingAddress1: {
|
||||
name: 'Billing Address 1',
|
||||
column: 'billing_address1',
|
||||
type: 'text',
|
||||
printable: false
|
||||
},
|
||||
billingAddress2: {
|
||||
name: 'Billing Address 2',
|
||||
column: 'billing_address2',
|
||||
type: 'text',
|
||||
printable: false
|
||||
},
|
||||
billingAddressCity: {
|
||||
name: 'Billing Address City',
|
||||
column: 'billing_address_city',
|
||||
type: 'text',
|
||||
printable: false
|
||||
},
|
||||
billingAddressCountry: {
|
||||
name: 'Billing Address Country',
|
||||
column: 'billing_address_country',
|
||||
type: 'text',
|
||||
printable: false
|
||||
},
|
||||
billingAddressPostcode: {
|
||||
name: 'Billing Address Postcode',
|
||||
column: 'billing_address_postcode',
|
||||
type: 'text',
|
||||
printable: false
|
||||
},
|
||||
billingAddressState: {
|
||||
name: 'Billing Address State',
|
||||
column: 'billing_address_state',
|
||||
type: 'text',
|
||||
printable: false
|
||||
},
|
||||
billingAddressPhone: {
|
||||
name: 'Billing Address Phone',
|
||||
column: 'billing_address_phone',
|
||||
type: 'text',
|
||||
printable: false
|
||||
},
|
||||
// Shipping Address
|
||||
shippingAddress1: {
|
||||
name: 'Shipping Address 1',
|
||||
column: 'shipping_address1',
|
||||
type: 'text',
|
||||
printable: false
|
||||
},
|
||||
shippingAddress2: {
|
||||
name: 'Shipping Address 2',
|
||||
column: 'shipping_address2',
|
||||
type: 'text',
|
||||
printable: false
|
||||
},
|
||||
shippingAddressCity: {
|
||||
name: 'Shipping Address City',
|
||||
column: 'shipping_address_city',
|
||||
type: 'text',
|
||||
printable: false
|
||||
},
|
||||
shippingAddressCountry: {
|
||||
name: 'Shipping Address Country',
|
||||
column: 'shipping_address_country',
|
||||
type: 'text',
|
||||
printable: false
|
||||
},
|
||||
shippingAddressPostcode: {
|
||||
name: 'Shipping Address Postcode',
|
||||
column: 'shipping_address_postcode',
|
||||
type: 'text',
|
||||
printable: false
|
||||
},
|
||||
shippingAddressPhone: {
|
||||
name: 'Shipping Address Phone',
|
||||
column: 'shipping_address_phone',
|
||||
type: 'text',
|
||||
printable: false
|
||||
},
|
||||
shippingAddressState: {
|
||||
name: 'Shipping Address State',
|
||||
column: 'shipping_address_state',
|
||||
type: 'text',
|
||||
printable: false
|
||||
},
|
||||
createdAt: {
|
||||
name: 'vendor.field.created_at',
|
||||
type: 'date',
|
||||
printable: false
|
||||
},
|
||||
},
|
||||
fields2: {
|
||||
|
||||
@@ -10,6 +10,9 @@ export default {
|
||||
importable: true,
|
||||
exportFlattenOn: 'categories',
|
||||
exportable: true,
|
||||
print: {
|
||||
pageTitle: 'Expenses',
|
||||
},
|
||||
fields: {
|
||||
payment_date: {
|
||||
name: 'expense.field.payment_date',
|
||||
@@ -67,7 +70,7 @@ export default {
|
||||
paymentReceive: {
|
||||
name: 'expense.field.payment_account',
|
||||
type: 'text',
|
||||
accessor: 'paymentAccount.name'
|
||||
accessor: 'paymentAccount.name',
|
||||
},
|
||||
referenceNo: {
|
||||
name: 'expense.field.reference_no',
|
||||
@@ -75,15 +78,18 @@ export default {
|
||||
},
|
||||
paymentDate: {
|
||||
name: 'expense.field.payment_date',
|
||||
accessor: 'formattedDate',
|
||||
type: 'date',
|
||||
},
|
||||
currencyCode: {
|
||||
name: 'expense.field.currency_code',
|
||||
type: 'text',
|
||||
printable: false,
|
||||
},
|
||||
exchangeRate: {
|
||||
name: 'expense.field.exchange_rate',
|
||||
type: 'number',
|
||||
printable: false,
|
||||
},
|
||||
description: {
|
||||
name: 'expense.field.description',
|
||||
@@ -111,6 +117,7 @@ export default {
|
||||
publish: {
|
||||
name: 'expense.field.publish',
|
||||
type: 'boolean',
|
||||
printable: false,
|
||||
},
|
||||
},
|
||||
fields2: {
|
||||
|
||||
@@ -6,6 +6,9 @@ export default {
|
||||
sortField: 'name',
|
||||
sortOrder: 'DESC',
|
||||
},
|
||||
print: {
|
||||
pageTitle: 'Items',
|
||||
},
|
||||
fields: {
|
||||
type: {
|
||||
name: 'item.field.type',
|
||||
@@ -127,6 +130,7 @@ export default {
|
||||
name: 'item.field.type',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
accessor: 'typeFormatted',
|
||||
},
|
||||
name: {
|
||||
name: 'item.field.name',
|
||||
@@ -142,11 +146,13 @@ export default {
|
||||
name: 'item.field.sellable',
|
||||
type: 'boolean',
|
||||
exportable: true,
|
||||
printable: false,
|
||||
},
|
||||
purchasable: {
|
||||
name: 'item.field.purchasable',
|
||||
type: 'boolean',
|
||||
exportable: true,
|
||||
printable: false,
|
||||
},
|
||||
sellPrice: {
|
||||
name: 'item.field.cost_price',
|
||||
@@ -163,12 +169,14 @@ export default {
|
||||
type: 'text',
|
||||
accessor: 'costAccount.name',
|
||||
exportable: true,
|
||||
printable: false,
|
||||
},
|
||||
sellAccount: {
|
||||
name: 'item.field.sell_description',
|
||||
type: 'text',
|
||||
accessor: 'sellAccount.name',
|
||||
exportable: true,
|
||||
printable: false,
|
||||
},
|
||||
inventoryAccount: {
|
||||
name: 'item.field.inventory_account',
|
||||
@@ -180,11 +188,13 @@ export default {
|
||||
name: 'Sell description',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
printable: false,
|
||||
},
|
||||
purchaseDescription: {
|
||||
name: 'Purchase description',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
printable: false,
|
||||
},
|
||||
quantityOnHand: {
|
||||
name: 'item.field.quantity_on_hand',
|
||||
@@ -206,11 +216,13 @@ export default {
|
||||
name: 'item.field.active',
|
||||
fieldType: 'boolean',
|
||||
exportable: true,
|
||||
printable: false,
|
||||
},
|
||||
createdAt: {
|
||||
name: 'item.field.created_at',
|
||||
type: 'date',
|
||||
exportable: true,
|
||||
printable: false,
|
||||
},
|
||||
},
|
||||
fields2: {
|
||||
|
||||
@@ -11,6 +11,11 @@ export default {
|
||||
importAggregator: 'group',
|
||||
importAggregateOn: 'entries',
|
||||
importAggregateBy: 'journalNumber',
|
||||
|
||||
print: {
|
||||
pageTitle: 'Manual Journals',
|
||||
},
|
||||
|
||||
fields: {
|
||||
date: {
|
||||
name: 'manual_journal.field.date',
|
||||
@@ -63,6 +68,7 @@ export default {
|
||||
date: {
|
||||
name: 'manual_journal.field.date',
|
||||
type: 'date',
|
||||
accessor: 'formattedDate',
|
||||
},
|
||||
journalNumber: {
|
||||
name: 'manual_journal.field.journal_number',
|
||||
@@ -83,10 +89,12 @@ export default {
|
||||
currencyCode: {
|
||||
name: 'manual_journal.field.currency',
|
||||
type: 'text',
|
||||
printable: false,
|
||||
},
|
||||
exchangeRate: {
|
||||
name: 'manual_journal.field.exchange_rate',
|
||||
type: 'number',
|
||||
printable: false,
|
||||
},
|
||||
description: {
|
||||
name: 'manual_journal.field.description',
|
||||
@@ -120,13 +128,17 @@ export default {
|
||||
publish: {
|
||||
name: 'Publish',
|
||||
type: 'boolean',
|
||||
printable: false,
|
||||
},
|
||||
publishedAt: {
|
||||
name: 'Published At',
|
||||
printable: false,
|
||||
},
|
||||
},
|
||||
createdAt: {
|
||||
name: 'Created At',
|
||||
accessor: 'formattedCreatedAt',
|
||||
printable: false,
|
||||
},
|
||||
},
|
||||
fields2: {
|
||||
|
||||
@@ -67,10 +67,12 @@ export default {
|
||||
paymentDate: {
|
||||
name: 'payment_receive.field.payment_date',
|
||||
type: 'date',
|
||||
accessor: 'formattedPaymentDate',
|
||||
},
|
||||
amount: {
|
||||
name: 'payment_receive.field.amount',
|
||||
type: 'number',
|
||||
accessor: 'formattedAmount'
|
||||
},
|
||||
referenceNo: {
|
||||
name: 'payment_receive.field.reference_no',
|
||||
@@ -88,10 +90,12 @@ export default {
|
||||
statement: {
|
||||
name: 'payment_receive.field.statement',
|
||||
type: 'text',
|
||||
printable: false,
|
||||
},
|
||||
created_at: {
|
||||
name: 'payment_receive.field.created_at',
|
||||
type: 'date',
|
||||
printable: false,
|
||||
},
|
||||
},
|
||||
fields2: {
|
||||
|
||||
@@ -11,6 +11,11 @@ export default {
|
||||
importAggregator: 'group',
|
||||
importAggregateOn: 'entries',
|
||||
importAggregateBy: 'estimateNumber',
|
||||
|
||||
print: {
|
||||
pageTitle: 'Sale Estimates'
|
||||
},
|
||||
|
||||
fields: {
|
||||
amount: {
|
||||
name: 'estimate.field.amount',
|
||||
@@ -86,11 +91,13 @@ export default {
|
||||
estimateDate: {
|
||||
name: 'Estimate Date',
|
||||
type: 'date',
|
||||
accessor: 'formattedEstimateDate',
|
||||
exportable: true,
|
||||
},
|
||||
expirationDate: {
|
||||
name: 'Expiration Date',
|
||||
type: 'date',
|
||||
accessor: 'formattedExpirationDate',
|
||||
exportable: true,
|
||||
},
|
||||
estimateNumber: {
|
||||
@@ -112,26 +119,31 @@ export default {
|
||||
name: 'Exchange Rate',
|
||||
type: 'number',
|
||||
exportable: true,
|
||||
printable: false,
|
||||
},
|
||||
currencyCode: {
|
||||
name: 'Currency',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
printable: false,
|
||||
},
|
||||
note: {
|
||||
name: 'Note',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
printable: false,
|
||||
},
|
||||
termsConditions: {
|
||||
name: 'Terms & Conditions',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
printable: false,
|
||||
},
|
||||
delivered: {
|
||||
name: 'Delivered',
|
||||
type: 'boolean',
|
||||
exportable: true,
|
||||
printable: false,
|
||||
},
|
||||
entries: {
|
||||
name: 'Entries',
|
||||
@@ -153,6 +165,7 @@ export default {
|
||||
},
|
||||
description: {
|
||||
name: 'Item Description',
|
||||
printable: false,
|
||||
},
|
||||
amount: {
|
||||
name: 'Item Amount',
|
||||
|
||||
@@ -11,6 +11,10 @@ export default {
|
||||
importAggregator: 'group',
|
||||
importAggregateOn: 'entries',
|
||||
importAggregateBy: 'invoiceNo',
|
||||
|
||||
print: {
|
||||
pageTitle: 'Sale invoices',
|
||||
},
|
||||
fields: {
|
||||
customer: {
|
||||
name: 'invoice.field.customer',
|
||||
@@ -94,10 +98,12 @@ export default {
|
||||
invoiceDate: {
|
||||
name: 'invoice.field.invoice_date',
|
||||
type: 'date',
|
||||
accessor: 'invoiceDateFormatted',
|
||||
},
|
||||
dueDate: {
|
||||
name: 'invoice.field.due_date',
|
||||
type: 'date',
|
||||
accessor: 'dueDateFormatted',
|
||||
},
|
||||
referenceNo: {
|
||||
name: 'invoice.field.reference_no',
|
||||
@@ -120,10 +126,12 @@ export default {
|
||||
exchangeRate: {
|
||||
name: 'invoice.field.exchange_rate',
|
||||
type: 'number',
|
||||
printable: false,
|
||||
},
|
||||
currencyCode: {
|
||||
name: 'invoice.field.currency',
|
||||
type: 'text',
|
||||
printable: false,
|
||||
},
|
||||
paidAmount: {
|
||||
name: 'Paid Amount',
|
||||
@@ -136,14 +144,17 @@ export default {
|
||||
invoiceMessage: {
|
||||
name: 'invoice.field.invoice_message',
|
||||
type: 'text',
|
||||
printable: false,
|
||||
},
|
||||
termsConditions: {
|
||||
name: 'invoice.field.terms_conditions',
|
||||
type: 'text',
|
||||
printable: false,
|
||||
},
|
||||
delivered: {
|
||||
name: 'invoice.field.delivered',
|
||||
type: 'boolean',
|
||||
printable: false,
|
||||
},
|
||||
entries: {
|
||||
name: 'Entries',
|
||||
@@ -165,6 +176,7 @@ export default {
|
||||
},
|
||||
description: {
|
||||
name: 'Item Description',
|
||||
printable: false,
|
||||
},
|
||||
amount: {
|
||||
name: 'Item Amount',
|
||||
@@ -202,18 +214,22 @@ export default {
|
||||
exchangeRate: {
|
||||
name: 'invoice.field.exchange_rate',
|
||||
fieldType: 'number',
|
||||
printable: false,
|
||||
},
|
||||
currencyCode: {
|
||||
name: 'invoice.field.currency',
|
||||
fieldType: 'text',
|
||||
printable: false,
|
||||
},
|
||||
invoiceMessage: {
|
||||
name: 'invoice.field.invoice_message',
|
||||
fieldType: 'text',
|
||||
printable: false,
|
||||
},
|
||||
termsConditions: {
|
||||
name: 'invoice.field.terms_conditions',
|
||||
fieldType: 'text',
|
||||
printable: false,
|
||||
},
|
||||
entries: {
|
||||
name: 'invoice.field.entries',
|
||||
@@ -249,6 +265,7 @@ export default {
|
||||
delivered: {
|
||||
name: 'invoice.field.delivered',
|
||||
fieldType: 'boolean',
|
||||
printable: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,6 +11,10 @@ export default {
|
||||
importAggregator: 'group',
|
||||
importAggregateOn: 'entries',
|
||||
importAggregateBy: 'receiptNumber',
|
||||
|
||||
print: {
|
||||
pageTitle: 'Sale Receipts',
|
||||
},
|
||||
fields: {
|
||||
amount: {
|
||||
name: 'receipt.field.amount',
|
||||
@@ -81,11 +85,6 @@ export default {
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
amount: {
|
||||
name: 'receipt.field.amount',
|
||||
column: 'amount',
|
||||
type: 'number',
|
||||
},
|
||||
depositAccount: {
|
||||
name: 'receipt.field.deposit_account',
|
||||
type: 'text',
|
||||
@@ -98,6 +97,7 @@ export default {
|
||||
},
|
||||
receiptDate: {
|
||||
name: 'receipt.field.receipt_date',
|
||||
accessor: 'formattedReceiptDate',
|
||||
type: 'date',
|
||||
},
|
||||
receiptNumber: {
|
||||
@@ -114,10 +114,17 @@ export default {
|
||||
name: 'receipt.field.receipt_message',
|
||||
column: 'receipt_message',
|
||||
type: 'text',
|
||||
printable: false,
|
||||
},
|
||||
amount: {
|
||||
name: 'receipt.field.amount',
|
||||
accessor: 'formattedAmount',
|
||||
type: 'number',
|
||||
},
|
||||
statement: {
|
||||
name: 'receipt.field.statement',
|
||||
type: 'text',
|
||||
printable: false,
|
||||
},
|
||||
status: {
|
||||
name: 'receipt.field.status',
|
||||
@@ -127,6 +134,7 @@ export default {
|
||||
{ key: 'closed', label: 'receipt.field.status.closed' },
|
||||
],
|
||||
exportable: true,
|
||||
printable: false,
|
||||
},
|
||||
entries: {
|
||||
name: 'Entries',
|
||||
@@ -148,6 +156,7 @@ export default {
|
||||
},
|
||||
description: {
|
||||
name: 'Item Description',
|
||||
printable: false,
|
||||
},
|
||||
amount: {
|
||||
name: 'Item Amount',
|
||||
@@ -158,6 +167,7 @@ export default {
|
||||
createdAt: {
|
||||
name: 'receipt.field.created_at',
|
||||
type: 'date',
|
||||
printable: false,
|
||||
},
|
||||
},
|
||||
fields2: {
|
||||
|
||||
@@ -131,21 +131,26 @@ export default {
|
||||
openingBalance: {
|
||||
name: 'vendor.field.opening_balance',
|
||||
type: 'number',
|
||||
printable: false
|
||||
},
|
||||
openingBalanceAt: {
|
||||
name: 'vendor.field.opening_balance_at',
|
||||
type: 'date',
|
||||
printable: false
|
||||
},
|
||||
currencyCode: {
|
||||
name: 'vendor.field.currency',
|
||||
type: 'text',
|
||||
printable: false
|
||||
},
|
||||
status: {
|
||||
name: 'vendor.field.status',
|
||||
printable: false
|
||||
},
|
||||
note: {
|
||||
name: 'vendor.field.note',
|
||||
type: 'text',
|
||||
printable: false
|
||||
},
|
||||
// Billing Address
|
||||
billingAddress1: {
|
||||
@@ -153,42 +158,49 @@ export default {
|
||||
column: 'billing_address1',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
printable: false
|
||||
},
|
||||
billingAddress2: {
|
||||
name: 'Billing Address 2',
|
||||
column: 'billing_address2',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
printable: false
|
||||
},
|
||||
billingAddressCity: {
|
||||
name: 'Billing Address City',
|
||||
column: 'billing_address_city',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
printable: false
|
||||
},
|
||||
billingAddressCountry: {
|
||||
name: 'Billing Address Country',
|
||||
column: 'billing_address_country',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
printable: false
|
||||
},
|
||||
billingAddressPostcode: {
|
||||
name: 'Billing Address Postcode',
|
||||
column: 'billing_address_postcode',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
printable: false
|
||||
},
|
||||
billingAddressState: {
|
||||
name: 'Billing Address State',
|
||||
column: 'billing_address_state',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
printable: false
|
||||
},
|
||||
billingAddressPhone: {
|
||||
name: 'Billing Address Phone',
|
||||
column: 'billing_address_phone',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
printable: false
|
||||
},
|
||||
// Shipping Address
|
||||
shippingAddress1: {
|
||||
@@ -196,47 +208,55 @@ export default {
|
||||
column: 'shipping_address1',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
printable: false
|
||||
},
|
||||
shippingAddress2: {
|
||||
name: 'Shipping Address 2',
|
||||
column: 'shipping_address2',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
printable: false
|
||||
},
|
||||
shippingAddressCity: {
|
||||
name: 'Shipping Address City',
|
||||
column: 'shipping_address_city',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
printable: false
|
||||
},
|
||||
shippingAddressCountry: {
|
||||
name: 'Shipping Address Country',
|
||||
column: 'shipping_address_country',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
printable: false
|
||||
},
|
||||
shippingAddressPostcode: {
|
||||
name: 'Shipping Address Postcode',
|
||||
column: 'shipping_address_postcode',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
printable: false
|
||||
},
|
||||
shippingAddressState: {
|
||||
name: 'Shipping Address State',
|
||||
column: 'shipping_address_state',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
printable: false
|
||||
},
|
||||
shippingAddressPhone: {
|
||||
name: 'Shipping Address Phone',
|
||||
column: 'shipping_address_phone',
|
||||
type: 'text',
|
||||
exportable: true,
|
||||
printable: false
|
||||
},
|
||||
createdAt: {
|
||||
name: 'vendor.field.created_at',
|
||||
type: 'date',
|
||||
exportable: true,
|
||||
printable: false
|
||||
},
|
||||
},
|
||||
fields2: {
|
||||
|
||||
@@ -20,6 +20,9 @@ export default {
|
||||
importAggregateOn: 'entries',
|
||||
importAggregateBy: 'vendorCreditNumber',
|
||||
|
||||
print: {
|
||||
pageTitle: 'Vendor Credits',
|
||||
},
|
||||
fields: {
|
||||
vendor: {
|
||||
name: 'vendor_credit.field.vendor',
|
||||
@@ -89,6 +92,7 @@ export default {
|
||||
exchangeRate: {
|
||||
name: 'Echange Rate',
|
||||
type: 'text',
|
||||
printable: false,
|
||||
},
|
||||
vendorCreditNumber: {
|
||||
name: 'Vendor Credit No.',
|
||||
@@ -100,7 +104,7 @@ export default {
|
||||
},
|
||||
vendorCreditDate: {
|
||||
name: 'Vendor Credit Date',
|
||||
type: 'date',
|
||||
accessor: 'formattedVendorCreditDate',
|
||||
},
|
||||
amount: {
|
||||
name: 'Amount',
|
||||
@@ -109,10 +113,12 @@ export default {
|
||||
creditRemaining: {
|
||||
name: 'Credits Remaining',
|
||||
accessor: 'formattedCreditsRemaining',
|
||||
printable: false,
|
||||
},
|
||||
refundedAmount: {
|
||||
name: 'Refunded Amount',
|
||||
accessor: 'refundedAmount',
|
||||
printable: false,
|
||||
},
|
||||
invoicedAmount: {
|
||||
name: 'Invoiced Amount',
|
||||
@@ -121,10 +127,12 @@ export default {
|
||||
note: {
|
||||
name: 'Note',
|
||||
type: 'text',
|
||||
printable: false,
|
||||
},
|
||||
open: {
|
||||
name: 'Open',
|
||||
type: 'boolean',
|
||||
printable: false,
|
||||
},
|
||||
entries: {
|
||||
name: 'Entries',
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
IAccountEventCreatedPayload,
|
||||
IAccountEventCreatingPayload,
|
||||
IAccountCreateDTO,
|
||||
CreateAccountParams,
|
||||
} from '@/interfaces';
|
||||
import events from '@/subscribers/events';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
@@ -30,19 +31,22 @@ export class CreateAccount {
|
||||
|
||||
/**
|
||||
* Authorize the account creation.
|
||||
* @param {number} tenantId
|
||||
* @param {IAccountCreateDTO} accountDTO
|
||||
* @param {number} tenantId
|
||||
* @param {IAccountCreateDTO} accountDTO
|
||||
*/
|
||||
private authorize = async (
|
||||
tenantId: number,
|
||||
accountDTO: IAccountCreateDTO,
|
||||
baseCurrency: string
|
||||
baseCurrency: string,
|
||||
params?: CreateAccountParams
|
||||
) => {
|
||||
// Validate account name uniquiness.
|
||||
await this.validator.validateAccountNameUniquiness(
|
||||
tenantId,
|
||||
accountDTO.name
|
||||
);
|
||||
if (!params.ignoreUniqueName) {
|
||||
await this.validator.validateAccountNameUniquiness(
|
||||
tenantId,
|
||||
accountDTO.name
|
||||
);
|
||||
}
|
||||
// Validate the account code uniquiness.
|
||||
if (accountDTO.code) {
|
||||
await this.validator.isAccountCodeUniqueOrThrowError(
|
||||
@@ -82,7 +86,7 @@ export class CreateAccount {
|
||||
|
||||
/**
|
||||
* Transformes the create account DTO to input model.
|
||||
* @param {IAccountCreateDTO} createAccountDTO
|
||||
* @param {IAccountCreateDTO} createAccountDTO
|
||||
*/
|
||||
private transformDTOToModel = (
|
||||
createAccountDTO: IAccountCreateDTO,
|
||||
@@ -104,7 +108,8 @@ export class CreateAccount {
|
||||
public createAccount = async (
|
||||
tenantId: number,
|
||||
accountDTO: IAccountCreateDTO,
|
||||
trx?: Knex.Transaction
|
||||
trx?: Knex.Transaction,
|
||||
params: CreateAccountParams = { ignoreUniqueName: false }
|
||||
): Promise<IAccount> => {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
@@ -112,8 +117,12 @@ export class CreateAccount {
|
||||
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
|
||||
|
||||
// Authorize the account creation.
|
||||
await this.authorize(tenantId, accountDTO, tenantMeta.baseCurrency);
|
||||
|
||||
await this.authorize(
|
||||
tenantId,
|
||||
accountDTO,
|
||||
tenantMeta.baseCurrency,
|
||||
params
|
||||
);
|
||||
// Transformes the DTO to model.
|
||||
const accountInputModel = this.transformDTOToModel(
|
||||
accountDTO,
|
||||
@@ -148,3 +157,4 @@ export class CreateAccount {
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@ import { Inject, Service } from 'typedi';
|
||||
import { UploadDocument } from './UploadDocument';
|
||||
import { DeleteAttachment } from './DeleteAttachment';
|
||||
import { GetAttachment } from './GetAttachment';
|
||||
import { AttachmentUploadPipeline } from './S3UploadPipeline';
|
||||
import { LinkAttachment } from './LinkAttachment';
|
||||
import { UnlinkAttachment } from './UnlinkAttachment';
|
||||
import { getAttachmentPresignedUrl } from './GetAttachmentPresignedUrl';
|
||||
import type { Multer } from 'multer';
|
||||
|
||||
@Service()
|
||||
export class AttachmentsApplication {
|
||||
@@ -19,9 +17,6 @@ export class AttachmentsApplication {
|
||||
@Inject()
|
||||
private getDocumentService: GetAttachment;
|
||||
|
||||
@Inject()
|
||||
private uploadPipelineService: AttachmentUploadPipeline;
|
||||
|
||||
@Inject()
|
||||
private linkDocumentService: LinkAttachment;
|
||||
|
||||
@@ -31,14 +26,6 @@ export class AttachmentsApplication {
|
||||
@Inject()
|
||||
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.
|
||||
* @param {number} tenantId
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { DeleteObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { s3 } from '@/lib/S3/S3';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { s3 } from '@/lib/S3/S3';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import config from '@/config';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
@Service()
|
||||
export class DeleteAttachment {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
/**
|
||||
* Deletes the give file attachment file key.
|
||||
* @param {number} tenantId
|
||||
@@ -17,7 +23,7 @@ export class DeleteAttachment {
|
||||
const { Document, DocumentLink } = this.tenancy.models(tenantId);
|
||||
|
||||
const params = {
|
||||
Bucket: process.env.AWS_BUCKET,
|
||||
Bucket: config.s3.bucket,
|
||||
Key: filekey,
|
||||
};
|
||||
await s3.send(new DeleteObjectCommand(params));
|
||||
@@ -26,10 +32,14 @@ export class DeleteAttachment {
|
||||
.findOne('key', filekey)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Delete all document links
|
||||
await DocumentLink.query().where('documentId', foundDocument.id).delete();
|
||||
await this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Delete all document links
|
||||
await DocumentLink.query(trx)
|
||||
.where('documentId', foundDocument.id)
|
||||
.delete();
|
||||
|
||||
// Delete thedocument.
|
||||
await Document.query().findById(foundDocument.id).delete();
|
||||
// Delete thedocument.
|
||||
await Document.query(trx).findById(foundDocument.id).delete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Service } from 'typedi';
|
||||
import { s3 } from '@/lib/S3/S3';
|
||||
import { GetObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { s3 } from '@/lib/S3/S3';
|
||||
import config from '@/config';
|
||||
|
||||
@Service()
|
||||
export class GetAttachment {
|
||||
@@ -11,7 +12,7 @@ export class GetAttachment {
|
||||
*/
|
||||
async getAttachment(tenantId: number, filekey: string) {
|
||||
const params = {
|
||||
Bucket: process.env.AWS_BUCKET,
|
||||
Bucket: config.s3.bucket,
|
||||
Key: filekey,
|
||||
};
|
||||
const data = await s3.send(new GetObjectCommand(params));
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import { Service } from 'typedi';
|
||||
import { GetObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { s3 } from '@/lib/S3/S3';
|
||||
import { Service } from 'typedi';
|
||||
import config from '@/config';
|
||||
|
||||
@Service()
|
||||
export class getAttachmentPresignedUrl {
|
||||
/**
|
||||
* Retrieves the presigned url of the given attachment key.
|
||||
* @param {string} key
|
||||
* @param {string} key
|
||||
* @returns {Promise<string?>}
|
||||
*/
|
||||
async getPresignedUrl(key: string) {
|
||||
const params = {
|
||||
Bucket: process.env.AWS_BUCKET,
|
||||
Key: key,
|
||||
Expires: 60 * 5, // 5 minutes
|
||||
};
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: process.env.AWS_BUCKET,
|
||||
Bucket: config.s3.bucket,
|
||||
Key: key,
|
||||
});
|
||||
const signedUrl = await getSignedUrl(s3, command, { expiresIn: 300 });
|
||||
|
||||
@@ -1,12 +1,38 @@
|
||||
import multer from 'multer';
|
||||
import type { Multer } from 'multer'
|
||||
import type { Multer } from 'multer';
|
||||
import multerS3 from 'multer-s3';
|
||||
import { s3 } from '@/lib/S3/S3';
|
||||
import { Service } from 'typedi';
|
||||
import config from '@/config';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
|
||||
@Service()
|
||||
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.
|
||||
* It utilizes the multer middleware for handling multipart/form-data, specifically for file uploads.
|
||||
|
||||
@@ -20,6 +20,10 @@ export class SendVerfiyMailOnSignUp {
|
||||
private handleSendVerifyMailOnSignup = async ({
|
||||
user,
|
||||
}: IAuthSignedUpEventPayload) => {
|
||||
// Can't continue if the user is verified.
|
||||
if (user.verified) {
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
email: user.email,
|
||||
token: user.verifyToken,
|
||||
|
||||
@@ -3,7 +3,11 @@ import { Inject, Service } from 'typedi';
|
||||
import bluebird from 'bluebird';
|
||||
import { entries, groupBy } from 'lodash';
|
||||
import { CreateAccount } from '@/services/Accounts/CreateAccount';
|
||||
import { PlaidAccount, PlaidTransaction } from '@/interfaces';
|
||||
import {
|
||||
IAccountCreateDTO,
|
||||
PlaidAccount,
|
||||
PlaidTransaction,
|
||||
} from '@/interfaces';
|
||||
import {
|
||||
transformPlaidAccountToCreateAccount,
|
||||
transformPlaidTrxsToCashflowCreate,
|
||||
@@ -11,6 +15,7 @@ import {
|
||||
import { DeleteCashflowTransaction } from '@/services/Cashflow/DeleteCashflowTransactionService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
const CONCURRENCY_ASYNC = 10;
|
||||
|
||||
@@ -28,6 +33,35 @@ export class PlaidSyncDb {
|
||||
@Inject()
|
||||
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.
|
||||
* @param {number} tenantId Tenant ID.
|
||||
@@ -37,7 +71,8 @@ export class PlaidSyncDb {
|
||||
public async syncBankAccounts(
|
||||
tenantId: number,
|
||||
plaidAccounts: PlaidAccount[],
|
||||
institution: any
|
||||
institution: any,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> {
|
||||
const transformToPlaidAccounts =
|
||||
transformPlaidAccountToCreateAccount(institution);
|
||||
@@ -47,7 +82,7 @@ export class PlaidSyncDb {
|
||||
await bluebird.map(
|
||||
accountCreateDTOs,
|
||||
(createAccountDTO: any) =>
|
||||
this.createAccountService.createAccount(tenantId, createAccountDTO),
|
||||
this.syncBankAccount(tenantId, createAccountDTO, trx),
|
||||
{ concurrency: CONCURRENCY_ASYNC }
|
||||
);
|
||||
}
|
||||
@@ -61,15 +96,16 @@ export class PlaidSyncDb {
|
||||
public async syncAccountTranactions(
|
||||
tenantId: number,
|
||||
plaidAccountId: number,
|
||||
plaidTranasctions: PlaidTransaction[]
|
||||
plaidTranasctions: PlaidTransaction[],
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
const cashflowAccount = await Account.query()
|
||||
const cashflowAccount = await Account.query(trx)
|
||||
.findOne({ plaidAccountId })
|
||||
.throwIfNotFound();
|
||||
|
||||
const openingEquityBalance = await Account.query().findOne(
|
||||
const openingEquityBalance = await Account.query(trx).findOne(
|
||||
'slug',
|
||||
'opening-balance-equity'
|
||||
);
|
||||
@@ -87,7 +123,8 @@ export class PlaidSyncDb {
|
||||
(uncategoriedDTO) =>
|
||||
this.cashflowApp.createUncategorizedTransaction(
|
||||
tenantId,
|
||||
uncategoriedDTO
|
||||
uncategoriedDTO,
|
||||
trx
|
||||
),
|
||||
{ concurrency: 1 }
|
||||
);
|
||||
@@ -100,7 +137,8 @@ export class PlaidSyncDb {
|
||||
*/
|
||||
public async syncAccountsTransactions(
|
||||
tenantId: number,
|
||||
plaidAccountsTransactions: PlaidTransaction[]
|
||||
plaidAccountsTransactions: PlaidTransaction[],
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> {
|
||||
const groupedTrnsxByAccountId = entries(
|
||||
groupBy(plaidAccountsTransactions, 'account_id')
|
||||
@@ -111,7 +149,8 @@ export class PlaidSyncDb {
|
||||
return this.syncAccountTranactions(
|
||||
tenantId,
|
||||
plaidAccountId,
|
||||
plaidTransactions
|
||||
plaidTransactions,
|
||||
trx
|
||||
);
|
||||
},
|
||||
{ concurrency: CONCURRENCY_ASYNC }
|
||||
@@ -124,11 +163,12 @@ export class PlaidSyncDb {
|
||||
*/
|
||||
public async syncRemoveTransactions(
|
||||
tenantId: number,
|
||||
plaidTransactionsIds: string[]
|
||||
plaidTransactionsIds: string[],
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
const { CashflowTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
const cashflowTransactions = await CashflowTransaction.query().whereIn(
|
||||
const cashflowTransactions = await CashflowTransaction.query(trx).whereIn(
|
||||
'plaidTransactionId',
|
||||
plaidTransactionsIds
|
||||
);
|
||||
@@ -140,7 +180,8 @@ export class PlaidSyncDb {
|
||||
(transactionId: number) =>
|
||||
this.deleteCashflowTransactionService.deleteCashflowTransaction(
|
||||
tenantId,
|
||||
transactionId
|
||||
transactionId,
|
||||
trx
|
||||
),
|
||||
{ concurrency: CONCURRENCY_ASYNC }
|
||||
);
|
||||
@@ -155,11 +196,12 @@ export class PlaidSyncDb {
|
||||
public async syncTransactionsCursor(
|
||||
tenantId: number,
|
||||
plaidItemId: string,
|
||||
lastCursor: string
|
||||
lastCursor: string,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
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(
|
||||
tenantId: number,
|
||||
plaidAccountIds: string[]
|
||||
plaidAccountIds: string[],
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
await Account.query().whereIn('plaid_account_id', plaidAccountIds).patch({
|
||||
lastFeedsUpdatedAt: new Date(),
|
||||
});
|
||||
await Account.query(trx)
|
||||
.whereIn('plaid_account_id', plaidAccountIds)
|
||||
.patch({
|
||||
lastFeedsUpdatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,12 +232,15 @@ export class PlaidSyncDb {
|
||||
public async updateAccountsFeedsActive(
|
||||
tenantId: number,
|
||||
plaidAccountIds: string[],
|
||||
isFeedsActive: boolean = true
|
||||
isFeedsActive: boolean = true,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
await Account.query().whereIn('plaid_account_id', plaidAccountIds).patch({
|
||||
isFeedsActive,
|
||||
});
|
||||
await Account.query(trx)
|
||||
.whereIn('plaid_account_id', plaidAccountIds)
|
||||
.patch({
|
||||
isFeedsActive,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Inject, Service } from 'typedi';
|
||||
import { PlaidClientWrapper } from '@/lib/Plaid/Plaid';
|
||||
import { PlaidSyncDb } from './PlaidSyncDB';
|
||||
import { PlaidFetchedTransactionsUpdates } from '@/interfaces';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
@Service()
|
||||
export class PlaidUpdateTransactions {
|
||||
@@ -12,12 +14,40 @@ export class PlaidUpdateTransactions {
|
||||
@Inject()
|
||||
private plaidSync: PlaidSyncDb;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
/**
|
||||
* Handles the fetching and storing of new, modified, or removed transactions
|
||||
* @param {number} tenantId Tenant ID.
|
||||
* @param {string} plaidItemId the Plaid ID for the item.
|
||||
* Handles sync the Plaid item to Bigcaptial under UOW.
|
||||
* @param {number} tenantId
|
||||
* @param {number} plaidItemId
|
||||
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
|
||||
*/
|
||||
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.
|
||||
const { added, modified, removed, cursor, accessToken } =
|
||||
await this.fetchTransactionUpdates(tenantId, plaidItemId);
|
||||
@@ -29,28 +59,42 @@ export class PlaidUpdateTransactions {
|
||||
} = await plaidInstance.accountsGet(request);
|
||||
|
||||
const plaidAccountsIds = accounts.map((a) => a.account_id);
|
||||
|
||||
const {
|
||||
data: { institution },
|
||||
} = await plaidInstance.institutionsGetById({
|
||||
institution_id: item.institution_id,
|
||||
country_codes: ['US', 'UK'],
|
||||
});
|
||||
// Update the DB.
|
||||
await this.plaidSync.syncBankAccounts(tenantId, accounts, institution);
|
||||
// Sync bank accounts.
|
||||
await this.plaidSync.syncBankAccounts(tenantId, accounts, institution, trx);
|
||||
// Sync bank account transactions.
|
||||
await this.plaidSync.syncAccountsTransactions(
|
||||
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.
|
||||
await this.plaidSync.updateLastFeedsUpdatedAt(tenantId, plaidAccountsIds);
|
||||
|
||||
await this.plaidSync.updateLastFeedsUpdatedAt(
|
||||
tenantId,
|
||||
plaidAccountsIds,
|
||||
trx
|
||||
);
|
||||
// Turn on the accounts feeds flag.
|
||||
await this.plaidSync.updateAccountsFeedsActive(tenantId, plaidAccountsIds);
|
||||
|
||||
await this.plaidSync.updateAccountsFeedsActive(
|
||||
tenantId,
|
||||
plaidAccountsIds,
|
||||
true,
|
||||
trx
|
||||
);
|
||||
return {
|
||||
addedCount: added.length,
|
||||
modifiedCount: modified.length,
|
||||
|
||||
@@ -42,7 +42,12 @@ export const transformPlaidTrxsToCashflowCreate = R.curry(
|
||||
): CreateUncategorizedTransactionDTO => {
|
||||
return {
|
||||
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,
|
||||
payee: plaidTranasction.payment_meta?.payee,
|
||||
currencyCode: plaidTranasction.iso_currency_code,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { DeleteCashflowTransaction } from './DeleteCashflowTransactionService';
|
||||
import { UncategorizeCashflowTransaction } from './UncategorizeCashflowTransaction';
|
||||
@@ -119,11 +120,13 @@ export class CashflowApplication {
|
||||
*/
|
||||
public createUncategorizedTransaction(
|
||||
tenantId: number,
|
||||
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO
|
||||
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
return this.createUncategorizedTransactionService.create(
|
||||
tenantId,
|
||||
createUncategorizedTransactionDTO
|
||||
createUncategorizedTransactionDTO,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@ export class DeleteCashflowTransaction {
|
||||
*/
|
||||
public deleteCashflowTransaction = async (
|
||||
tenantId: number,
|
||||
cashflowTransactionId: number
|
||||
cashflowTransactionId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<{ oldCashflowTransaction: ICashflowTransaction }> => {
|
||||
const { CashflowTransaction, CashflowTransactionLine } =
|
||||
this.tenancy.models(tenantId);
|
||||
@@ -43,34 +44,44 @@ export class DeleteCashflowTransaction {
|
||||
this.throwErrorIfTransactionNotFound(oldCashflowTransaction);
|
||||
|
||||
// Starting database transaction.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onCashflowTransactionDelete` event.
|
||||
await this.eventPublisher.emitAsync(events.cashflow.onTransactionDeleting, {
|
||||
trx,
|
||||
tenantId,
|
||||
oldCashflowTransaction,
|
||||
} as ICommandCashflowDeletingPayload);
|
||||
return this.uow.withTransaction(
|
||||
tenantId,
|
||||
async (trx: Knex.Transaction) => {
|
||||
// Triggers `onCashflowTransactionDelete` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.cashflow.onTransactionDeleting,
|
||||
{
|
||||
trx,
|
||||
tenantId,
|
||||
oldCashflowTransaction,
|
||||
} as ICommandCashflowDeletingPayload
|
||||
);
|
||||
|
||||
// Delete cashflow transaction associated lines first.
|
||||
await CashflowTransactionLine.query(trx)
|
||||
.where('cashflow_transaction_id', cashflowTransactionId)
|
||||
.delete();
|
||||
// Delete cashflow transaction associated lines first.
|
||||
await CashflowTransactionLine.query(trx)
|
||||
.where('cashflow_transaction_id', cashflowTransactionId)
|
||||
.delete();
|
||||
|
||||
// Delete cashflow transaction.
|
||||
await CashflowTransaction.query(trx)
|
||||
.findById(cashflowTransactionId)
|
||||
.delete();
|
||||
// Delete cashflow transaction.
|
||||
await CashflowTransaction.query(trx)
|
||||
.findById(cashflowTransactionId)
|
||||
.delete();
|
||||
|
||||
// Triggers `onCashflowTransactionDeleted` event.
|
||||
await this.eventPublisher.emitAsync(events.cashflow.onTransactionDeleted, {
|
||||
trx,
|
||||
tenantId,
|
||||
cashflowTransactionId,
|
||||
oldCashflowTransaction,
|
||||
} as ICommandCashflowDeletedPayload);
|
||||
// Triggers `onCashflowTransactionDeleted` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.cashflow.onTransactionDeleted,
|
||||
{
|
||||
trx,
|
||||
tenantId,
|
||||
cashflowTransactionId,
|
||||
oldCashflowTransaction,
|
||||
} as ICommandCashflowDeletedPayload
|
||||
);
|
||||
|
||||
return { oldCashflowTransaction };
|
||||
});
|
||||
return { oldCashflowTransaction };
|
||||
},
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,11 @@ import { PageProperties, PdfFormat } from '@/lib/Chromiumly/_types';
|
||||
import { UrlConverter } from '@/lib/Chromiumly/UrlConvert';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { Chromiumly } from '@/lib/Chromiumly/Chromiumly';
|
||||
import { PDF_FILE_EXPIRE_IN, getPdfFilesStorageDir } from './utils';
|
||||
import {
|
||||
PDF_FILE_EXPIRE_IN,
|
||||
getPdfFilePath,
|
||||
getPdfFilesStorageDir,
|
||||
} from './utils';
|
||||
|
||||
@Service()
|
||||
export class ChromiumlyHtmlConvert {
|
||||
@@ -22,22 +26,16 @@ export class ChromiumlyHtmlConvert {
|
||||
tenantId: number,
|
||||
content: string
|
||||
): Promise<[string, () => Promise<void>]> {
|
||||
const { Attachment } = this.tenancy.models(tenantId);
|
||||
const { Document } = this.tenancy.models(tenantId);
|
||||
|
||||
const filename = `document-${Date.now()}.html`;
|
||||
const storageDir = getPdfFilesStorageDir(filename);
|
||||
const filePath = path.join(global.__storage_dir, storageDir);
|
||||
const filename = `document-print-${Date.now()}.html`;
|
||||
const filePath = getPdfFilePath(filename);
|
||||
|
||||
await fs.writeFile(filePath, content);
|
||||
await Attachment.query().insert({
|
||||
key: filename,
|
||||
path: storageDir,
|
||||
expire_in: PDF_FILE_EXPIRE_IN, // ms
|
||||
extension: 'html',
|
||||
});
|
||||
await Document.query().insert({ key: filename, mimeType: 'text/html' });
|
||||
const cleanup = async () => {
|
||||
await fs.unlink(filePath);
|
||||
await Attachment.query().where('key', filename).delete();
|
||||
await Document.query().where('key', filename).delete();
|
||||
};
|
||||
return [filename, cleanup];
|
||||
}
|
||||
@@ -60,6 +58,7 @@ export class ChromiumlyHtmlConvert {
|
||||
html
|
||||
);
|
||||
const fileDir = getPdfFilesStorageDir(filename);
|
||||
|
||||
const url = path.join(Chromiumly.GOTENBERG_DOCS_ENDPOINT, fileDir);
|
||||
const urlConverter = new UrlConverter();
|
||||
|
||||
|
||||
@@ -5,4 +5,10 @@ export const PDF_FILE_EXPIRE_IN = 40; // ms
|
||||
|
||||
export const getPdfFilesStorageDir = (filename: string) => {
|
||||
return path.join(PDF_FILE_SUB_DIR, filename);
|
||||
}
|
||||
};
|
||||
|
||||
export const getPdfFilePath = (filename: string) => {
|
||||
const storageDir = getPdfFilesStorageDir(filename);
|
||||
|
||||
return path.join(global.__storage_dir, storageDir);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ExportResourceService } from './ExportService';
|
||||
import { ExportFormat } from './common';
|
||||
|
||||
@Service()
|
||||
export class ExportApplication {
|
||||
@@ -9,9 +10,9 @@ export class ExportApplication {
|
||||
/**
|
||||
* Exports the given resource to csv, xlsx or pdf format.
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
47
packages/server/src/services/Export/ExportPdf.ts
Normal file
47
packages/server/src/services/Export/ExportPdf.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,10 @@ import { sanitizeResourceName } from '../Import/_utils';
|
||||
import ResourceService from '../Resource/ResourceService';
|
||||
import { ExportableResources } from './ExportResources';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { Errors } from './common';
|
||||
import { Errors, ExportFormat } from './common';
|
||||
import { IModelMeta, IModelMetaColumn } from '@/interfaces';
|
||||
import { flatDataCollections, getDataAccessor } from './utils';
|
||||
import { ExportPdf } from './ExportPdf';
|
||||
|
||||
@Service()
|
||||
export class ExportResourceService {
|
||||
@@ -18,13 +19,20 @@ export class ExportResourceService {
|
||||
@Inject()
|
||||
private exportableResources: ExportableResources;
|
||||
|
||||
@Inject()
|
||||
private exportPdf: ExportPdf;
|
||||
|
||||
/**
|
||||
* Exports the given resource data through csv, xlsx or pdf.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @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 resourceMeta = this.getResourceMeta(tenantId, resource);
|
||||
|
||||
@@ -32,10 +40,24 @@ export class ExportResourceService {
|
||||
|
||||
const data = await this.getExportableData(tenantId, resource);
|
||||
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) {
|
||||
const exportable =
|
||||
this.exportableResources.registry.getExportable(resource);
|
||||
|
||||
return exportable.exportable(tenantId, {});
|
||||
}
|
||||
|
||||
@@ -125,6 +148,32 @@ export class ExportResourceService {
|
||||
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.
|
||||
* @param {any[]} data - The data to be included in the workbook.
|
||||
@@ -136,7 +185,6 @@ export class ExportResourceService {
|
||||
const worksheetData = data.map((item) =>
|
||||
exportableColumns.map((col) => get(item, getDataAccessor(col)))
|
||||
);
|
||||
|
||||
worksheetData.unshift(exportableColumns.map((col) => col.name));
|
||||
|
||||
const worksheet = xlsx.utils.aoa_to_sheet(worksheetData);
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
export enum Errors {
|
||||
RESOURCE_NOT_EXPORTABLE = 'RESOURCE_NOT_EXPORTABLE',
|
||||
}
|
||||
|
||||
export enum ExportFormat {
|
||||
Csv = 'csv',
|
||||
Pdf = 'pdf',
|
||||
Xlsx = 'xlsx',
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { flatMap } from 'lodash';
|
||||
import { flatMap, get } from 'lodash';
|
||||
/**
|
||||
* Flattens the data based on a specified attribute.
|
||||
* @param data - The data to be flattened.
|
||||
@@ -25,3 +25,21 @@ export const flatDataCollections = (
|
||||
export const getDataAccessor = (col: any) => {
|
||||
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: '' };
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Service } from 'typedi';
|
||||
import { createCheckout } from '@lemonsqueezy/lemonsqueezy.js';
|
||||
import { SystemUser } from '@/system/models';
|
||||
import { configureLemonSqueezy } from './utils';
|
||||
import config from '@/config';
|
||||
|
||||
@Service()
|
||||
export class LemonSqueezyService {
|
||||
@@ -28,7 +29,7 @@ export class LemonSqueezyService {
|
||||
},
|
||||
productOptions: {
|
||||
enabledVariants: [variantId],
|
||||
redirectUrl: `http://localhost:4000/dashboard/billing/`,
|
||||
redirectUrl: config.lemonSqueezy.redirectTo,
|
||||
receiptButtonText: 'Go to Dashboard',
|
||||
receiptThankYouNote: 'Thank you for signing up to Lemon Stand!',
|
||||
},
|
||||
|
||||
@@ -50,7 +50,6 @@ import InvoiceMailDialog from '@/containers/Sales/Invoices/InvoiceMailDialog/Inv
|
||||
import EstimateMailDialog from '@/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog';
|
||||
import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog';
|
||||
import PaymentMailDialog from '@/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialog';
|
||||
import { ConnectBankDialog } from '@/containers/CashFlow/ConnectBankDialog';
|
||||
import { ExportDialog } from '@/containers/Dialogs/ExportDialog';
|
||||
|
||||
/**
|
||||
@@ -97,7 +96,6 @@ export default function DialogsContainer() {
|
||||
<NotifyPaymentReceiveViaSMSDialog
|
||||
dialogName={DialogsName.NotifyPaymentViaForm}
|
||||
/>
|
||||
|
||||
<BadDebtDialog dialogName={DialogsName.BadDebtForm} />
|
||||
<SMSMessageDialog dialogName={DialogsName.SMSMessageForm} />
|
||||
<RefundCreditNoteDialog dialogName={DialogsName.RefundCreditNote} />
|
||||
@@ -148,8 +146,6 @@ export default function DialogsContainer() {
|
||||
<EstimateMailDialog dialogName={DialogsName.EstimateMail} />
|
||||
<ReceiptMailDialog dialogName={DialogsName.ReceiptMail} />
|
||||
<PaymentMailDialog dialogName={DialogsName.PaymentMail} />
|
||||
<ConnectBankDialog dialogName={DialogsName.ConnectBankCreditCard} />
|
||||
|
||||
<ExportDialog dialogName={DialogsName.Export} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -30,6 +30,7 @@ import withSettings from '@/containers/Settings/withSettings';
|
||||
import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
|
||||
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
|
||||
import { compose } from '@/utils';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
@@ -50,7 +51,7 @@ function ManualJournalActionsBar({
|
||||
addSetting,
|
||||
|
||||
// #withDialogActions
|
||||
openDialog
|
||||
openDialog,
|
||||
}) {
|
||||
// History context.
|
||||
const history = useHistory();
|
||||
@@ -58,6 +59,9 @@ function ManualJournalActionsBar({
|
||||
// Manual journals context.
|
||||
const { journalsViews, fields } = useManualJournalsContext();
|
||||
|
||||
// Exports pdf document.
|
||||
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
|
||||
|
||||
// Manual journals refresh action.
|
||||
const { refresh } = useRefreshJournals();
|
||||
|
||||
@@ -91,6 +95,11 @@ function ManualJournalActionsBar({
|
||||
openDialog(DialogsName.Export, { resource: 'manual_journal' });
|
||||
};
|
||||
|
||||
// Handle the pdf print button click.
|
||||
const handlePdfPrintBtnSubmit = () => {
|
||||
downloadExportPdf({ resource: 'ManualJournal' });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
<NavbarGroup>
|
||||
@@ -134,10 +143,12 @@ function ManualJournalActionsBar({
|
||||
/>
|
||||
</If>
|
||||
|
||||
<NavbarDivider />
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="print-16" iconSize={16} />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePdfPrintBtnSubmit}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { isEmpty, isUndefined } from 'lodash';
|
||||
import {
|
||||
Button,
|
||||
NavbarGroup,
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
Intent,
|
||||
Switch,
|
||||
Alignment,
|
||||
ProgressBar,
|
||||
ToastProps,
|
||||
Text,
|
||||
} from '@blueprintjs/core';
|
||||
import clsx from 'classnames';
|
||||
|
||||
import {
|
||||
AdvancedFilterPopover,
|
||||
@@ -26,8 +30,10 @@ import {
|
||||
import { AccountAction, AbilitySubject } from '@/constants/abilityOption';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useRefreshAccounts } from '@/hooks/query/accounts';
|
||||
import { useAccountsChartContext } from './AccountsChartProvider';
|
||||
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
|
||||
|
||||
import withAccounts from './withAccounts';
|
||||
import withAccountsTableActions from './withAccountsTableActions';
|
||||
@@ -37,7 +43,6 @@ import withSettings from '@/containers/Settings/withSettings';
|
||||
import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* Accounts actions bar.
|
||||
@@ -57,22 +62,18 @@ function AccountsActionsBar({
|
||||
// #withAccountsTableActions
|
||||
setAccountsTableState,
|
||||
|
||||
// #ownProps
|
||||
onFilterChanged,
|
||||
|
||||
// #withSettings
|
||||
accountsTableSize,
|
||||
|
||||
// #withSettingsActions
|
||||
addSetting,
|
||||
}) {
|
||||
const { resourceViews, fields } = useAccountsChartContext();
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const onClickNewAccount = () => {
|
||||
openDialog(DialogsName.AccountForm, {});
|
||||
};
|
||||
const { resourceViews, fields } = useAccountsChartContext();
|
||||
|
||||
// Exports pdf document.
|
||||
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
|
||||
|
||||
// Accounts refresh action.
|
||||
const { refresh } = useRefreshAccounts();
|
||||
@@ -81,35 +82,29 @@ function AccountsActionsBar({
|
||||
const handleBulkDelete = () => {
|
||||
openAlert('accounts-bulk-delete', { accountsIds: accountsSelectedRows });
|
||||
};
|
||||
|
||||
// Handle bulk accounts activate.
|
||||
const handelBulkActivate = () => {
|
||||
openAlert('accounts-bulk-activate', { accountsIds: accountsSelectedRows });
|
||||
};
|
||||
|
||||
// Handle bulk accounts inactivate.
|
||||
const handelBulkInactive = () => {
|
||||
openAlert('accounts-bulk-inactivate', {
|
||||
accountsIds: accountsSelectedRows,
|
||||
});
|
||||
};
|
||||
|
||||
// Handle tab changing.
|
||||
const handleTabChange = (view) => {
|
||||
setAccountsTableState({ viewSlug: view ? view.slug : null });
|
||||
};
|
||||
|
||||
// Handle inactive switch changing.
|
||||
const handleInactiveSwitchChange = (event) => {
|
||||
const checked = event.target.checked;
|
||||
setAccountsTableState({ inactiveMode: checked });
|
||||
};
|
||||
|
||||
// Handle click a refresh accounts
|
||||
const handleRefreshBtnClick = () => {
|
||||
refresh();
|
||||
};
|
||||
|
||||
// Handle table row size change.
|
||||
const handleTableRowSizeChange = (size) => {
|
||||
addSetting('accounts', 'tableSize', size);
|
||||
@@ -122,6 +117,14 @@ function AccountsActionsBar({
|
||||
const handleExportBtnClick = () => {
|
||||
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 (
|
||||
<DashboardActionsBar>
|
||||
@@ -185,6 +188,7 @@ function AccountsActionsBar({
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="print-16" iconSize={16} />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePrintBtnClick}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
FeatureCan,
|
||||
} from '@/components';
|
||||
import { useRefreshCashflowAccounts } from '@/hooks/query';
|
||||
import { useOpenPlaidConnect } from '@/hooks/utils/useOpenPlaidConnect';
|
||||
import { CashflowAction, AbilitySubject } from '@/constants/abilityOption';
|
||||
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
@@ -39,6 +40,9 @@ function CashFlowAccountsActionsBar({
|
||||
}) {
|
||||
const { refresh } = useRefreshCashflowAccounts();
|
||||
|
||||
// Opens the Plaid popup.
|
||||
const { openPlaidAsync, isPlaidLoading } = useOpenPlaidConnect();
|
||||
|
||||
// Handle refresh button click.
|
||||
const handleRefreshBtnClick = () => {
|
||||
refresh();
|
||||
@@ -64,7 +68,7 @@ function CashFlowAccountsActionsBar({
|
||||
};
|
||||
// Handle connect button click.
|
||||
const handleConnectToBank = () => {
|
||||
openDialog(DialogsName.ConnectBankCreditCard);
|
||||
openPlaidAsync();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -116,6 +120,7 @@ function CashFlowAccountsActionsBar({
|
||||
className={Classes.MINIMAL}
|
||||
text={'Connect to Bank / Credit Card'}
|
||||
onClick={handleConnectToBank}
|
||||
disabled={isPlaidLoading}
|
||||
/>
|
||||
<NavbarDivider />
|
||||
</FeatureCan>
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './ConnectBankDialog';
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
|
||||
import { useCustomersListContext } from './CustomersListProvider';
|
||||
import { useRefreshCustomers } from '@/hooks/query/customers';
|
||||
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
|
||||
|
||||
import withCustomers from './withCustomers';
|
||||
import withCustomersActions from './withCustomersActions';
|
||||
@@ -70,6 +71,9 @@ function CustomerActionsBar({
|
||||
// Customers refresh action.
|
||||
const { refresh } = useRefreshCustomers();
|
||||
|
||||
// Exports pdf document.
|
||||
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
|
||||
|
||||
const onClickNewCustomer = () => {
|
||||
history.push('/customers/new');
|
||||
};
|
||||
@@ -109,6 +113,10 @@ function CustomerActionsBar({
|
||||
const handleExportBtnClick = () => {
|
||||
openDialog(DialogsName.Export, { resource: 'customer' });
|
||||
};
|
||||
// Handle the print button click.
|
||||
const handlePrintBtnClick = () => {
|
||||
downloadExportPdf({ resource: 'Customer' });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -154,6 +162,13 @@ function CustomerActionsBar({
|
||||
onClick={handleBulkDelete}
|
||||
/>
|
||||
</If>
|
||||
<NavbarDivider />
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="print-16" iconSize={16} />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePrintBtnClick}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="file-import-16" iconSize={16} />}
|
||||
|
||||
@@ -23,8 +23,11 @@ import {
|
||||
} from '@/components';
|
||||
|
||||
import { ExpenseAction, AbilitySubject } from '@/constants/abilityOption';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
import { useRefreshExpenses } from '@/hooks/query/expenses';
|
||||
import { useExpensesListContext } from './ExpensesListProvider';
|
||||
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
|
||||
|
||||
import withExpenses from './withExpenses';
|
||||
import withExpensesActions from './withExpensesActions';
|
||||
@@ -33,7 +36,6 @@ import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import withSettings from '@/containers/Settings/withSettings';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
/**
|
||||
* Expenses actions bar.
|
||||
@@ -60,6 +62,9 @@ function ExpensesActionsBar({
|
||||
// Expenses list context.
|
||||
const { expensesViews, fields } = useExpensesListContext();
|
||||
|
||||
// Exports pdf document.
|
||||
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
|
||||
|
||||
// Expenses refresh action.
|
||||
const { refresh } = useRefreshExpenses();
|
||||
|
||||
@@ -92,6 +97,10 @@ function ExpensesActionsBar({
|
||||
const handleExportBtnClick = () => {
|
||||
openDialog(DialogsName.Export, { resource: 'expense' });
|
||||
};
|
||||
// Handles the print button click.
|
||||
const handlePrintBtnClick = () => {
|
||||
downloadExportPdf({ resource: 'Expense' });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -135,11 +144,12 @@ function ExpensesActionsBar({
|
||||
onClick={handleBulkDelete}
|
||||
/>
|
||||
</If>
|
||||
|
||||
<NavbarDivider />
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="print-16" iconSize={16} />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePrintBtnClick}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
import { ItemAction, AbilitySubject } from '@/constants/abilityOption';
|
||||
import { useItemsListContext } from './ItemsListProvider';
|
||||
import { useRefreshItems } from '@/hooks/query/items';
|
||||
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
|
||||
|
||||
import withItems from './withItems';
|
||||
import withItemsActions from './withItemsActions';
|
||||
@@ -60,11 +61,14 @@ function ItemsActionsBar({
|
||||
addSetting,
|
||||
|
||||
// #withDialogActions
|
||||
openDialog
|
||||
openDialog,
|
||||
}) {
|
||||
// Items list context.
|
||||
const { itemsViews, fields } = useItemsListContext();
|
||||
|
||||
// Exports pdf document.
|
||||
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
|
||||
|
||||
// Items refresh action.
|
||||
const { refresh } = useRefreshItems();
|
||||
|
||||
@@ -107,7 +111,12 @@ function ItemsActionsBar({
|
||||
// Handle the export button click.
|
||||
const handleExportBtnClick = () => {
|
||||
openDialog(DialogsName.Export, { resource: 'item' });
|
||||
}
|
||||
};
|
||||
|
||||
// Handle the print button click.
|
||||
const handlePrintBtnClick = () => {
|
||||
downloadExportPdf({ resource: 'Item' });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -153,7 +162,12 @@ function ItemsActionsBar({
|
||||
onClick={handleBulkDelete}
|
||||
/>
|
||||
</If>
|
||||
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'print-16'} iconSize={'16'} />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePrintBtnClick}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="file-import-16" iconSize={16} />}
|
||||
@@ -204,5 +218,5 @@ export default compose(
|
||||
})),
|
||||
withItemsActions,
|
||||
withAlertActions,
|
||||
withDialogActions
|
||||
withDialogActions,
|
||||
)(ItemsActionsBar);
|
||||
|
||||
@@ -28,11 +28,13 @@ import withBills from './withBills';
|
||||
import withBillsActions from './withBillsActions';
|
||||
import withSettings from '@/containers/Settings/withSettings';
|
||||
import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
|
||||
import { useBillsListContext } from './BillsListProvider';
|
||||
import { useRefreshBills } from '@/hooks/query/bills';
|
||||
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
/**
|
||||
@@ -62,11 +64,13 @@ function BillActionsBar({
|
||||
// Bills list context.
|
||||
const { billsViews, fields } = useBillsListContext();
|
||||
|
||||
// Exports pdf document.
|
||||
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
|
||||
|
||||
// Handle click a new bill.
|
||||
const handleClickNewBill = () => {
|
||||
history.push('/bills/new');
|
||||
};
|
||||
|
||||
// Handle tab change.
|
||||
const handleTabChange = (view) => {
|
||||
setBillsTableState({
|
||||
@@ -77,21 +81,22 @@ function BillActionsBar({
|
||||
const handleRefreshBtnClick = () => {
|
||||
refresh();
|
||||
};
|
||||
|
||||
// Handle table row size change.
|
||||
const handleTableRowSizeChange = (size) => {
|
||||
addSetting('bills', 'tableSize', size);
|
||||
};
|
||||
|
||||
// Handle the import button click.
|
||||
const handleImportBtnClick = () => {
|
||||
history.push('/bills/import');
|
||||
};
|
||||
|
||||
// Handle the export button click.
|
||||
const handleExportBtnClick = () => {
|
||||
openDialog(DialogsName.Export, { resource: 'bill' });
|
||||
};
|
||||
// Handle the print button click.
|
||||
const handlePrintBtnClick = () => {
|
||||
downloadExportPdf({ resource: 'Bill' });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -133,13 +138,14 @@ function BillActionsBar({
|
||||
icon={<Icon icon={'trash-16'} iconSize={16} />}
|
||||
text={<T id={'delete'} />}
|
||||
intent={Intent.DANGER}
|
||||
// onClick={handleBulkDelete}
|
||||
/>
|
||||
</If>
|
||||
<NavbarDivider />
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'print-16'} iconSize={'16'} />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePrintBtnClick}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
@@ -153,7 +159,6 @@ function BillActionsBar({
|
||||
text={<T id={'export'} />}
|
||||
onClick={handleExportBtnClick}
|
||||
/>
|
||||
|
||||
<NavbarDivider />
|
||||
<DashboardRowsHeightButton
|
||||
initialValue={billsTableSize}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from '@/components';
|
||||
|
||||
import { useVendorsCreditNoteListContext } from './VendorsCreditNoteListProvider';
|
||||
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
|
||||
import { VendorCreditAction, AbilitySubject } from '@/constants/abilityOption';
|
||||
|
||||
import withVendorsCreditNotesActions from './withVendorsCreditNotesActions';
|
||||
@@ -60,35 +61,37 @@ function VendorsCreditNoteActionsBar({
|
||||
const { VendorCreditsViews, fields, refresh } =
|
||||
useVendorsCreditNoteListContext();
|
||||
|
||||
// Exports pdf document.
|
||||
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
|
||||
|
||||
// Handle click a new Vendor.
|
||||
const handleClickNewVendorCredit = () => {
|
||||
history.push('/vendor-credits/new');
|
||||
};
|
||||
|
||||
// Handle view tab change.
|
||||
const handleTabChange = (view) => {
|
||||
setVendorCreditsTableState({ viewSlug: view ? view.slug : null });
|
||||
};
|
||||
|
||||
// Handle click a refresh credit note.
|
||||
const handleRefreshBtnClick = () => {
|
||||
refresh();
|
||||
};
|
||||
|
||||
// Handle table row size change.
|
||||
const handleTableRowSizeChange = (size) => {
|
||||
addSetting('vendorCredit', 'tableSize', size);
|
||||
};
|
||||
|
||||
// Handle import button click.
|
||||
const handleImportBtnClick = () => {
|
||||
history.push('/vendor-credits/import');
|
||||
};
|
||||
|
||||
// Handle the export button click.
|
||||
const handleExportBtnClick = () => {
|
||||
openDialog(DialogsName.Export, { resource: 'vendor_credit' });
|
||||
};
|
||||
// Handle the print button click.
|
||||
const handlePrintBtnClick = () => {
|
||||
downloadExportPdf({ resource: 'VendorCredit' });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -127,6 +130,7 @@ function VendorsCreditNoteActionsBar({
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'print-16'} iconSize={'16'} />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePrintBtnClick}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Classes,
|
||||
@@ -22,19 +21,20 @@ import {
|
||||
DashboardRowsHeightButton,
|
||||
DashboardActionsBar,
|
||||
} from '@/components';
|
||||
import { PaymentMadeAction, AbilitySubject } from '@/constants/abilityOption';
|
||||
|
||||
import withPaymentMade from './withPaymentMade';
|
||||
import withPaymentMadeActions from './withPaymentMadeActions';
|
||||
import withSettings from '@/containers/Settings/withSettings';
|
||||
import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
|
||||
import { usePaymentMadesListContext } from './PaymentMadesListProvider';
|
||||
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 { compose } from '@/utils';
|
||||
|
||||
/**
|
||||
* Payment made actions bar.
|
||||
@@ -57,6 +57,9 @@ function PaymentMadeActionsBar({
|
||||
}) {
|
||||
const history = useHistory();
|
||||
|
||||
// Exports pdf document.
|
||||
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
|
||||
|
||||
// Payment receives list context.
|
||||
const { paymentMadesViews, fields } = usePaymentMadesListContext();
|
||||
|
||||
@@ -67,31 +70,30 @@ function PaymentMadeActionsBar({
|
||||
const handleClickNewPaymentMade = () => {
|
||||
history.push('/payment-mades/new');
|
||||
};
|
||||
|
||||
// Handle tab changing.
|
||||
const handleTabChange = (viewSlug) => {
|
||||
setPaymentMadesTableState({ viewSlug });
|
||||
};
|
||||
|
||||
// Handle click a refresh payment receives.
|
||||
const handleRefreshBtnClick = () => {
|
||||
refresh();
|
||||
};
|
||||
|
||||
// Handle table row size change.
|
||||
const handleTableRowSizeChange = (size) => {
|
||||
addSetting('billPayments', 'tableSize', size);
|
||||
};
|
||||
|
||||
// Handle the import button click.
|
||||
const handleImportBtnClick = () => {
|
||||
history.push('/payment-mades/import');
|
||||
};
|
||||
|
||||
// Handle the export button click.
|
||||
const handleExportBtnClick = () => {
|
||||
openDialog(DialogsName.Export, { resource: 'bill_payment' });
|
||||
};
|
||||
// Handle the print button click.
|
||||
const handlePrintBtnClick = () => {
|
||||
downloadExportPdf({ resource: 'BillPayment' });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -138,6 +140,7 @@ function PaymentMadeActionsBar({
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'print-16'} iconSize={'16'} />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePrintBtnClick}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
} from '@/components';
|
||||
|
||||
import { useCreditNoteListContext } from './CreditNotesListProvider';
|
||||
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
|
||||
|
||||
import { CreditNoteAction, AbilitySubject } from '@/constants/abilityOption';
|
||||
import withCreditNotes from './withCreditNotes';
|
||||
import withCreditNotesActions from './withCreditNotesActions';
|
||||
@@ -27,8 +29,8 @@ import withSettings from '@/containers/Settings/withSettings';
|
||||
import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import { compose } from '@/utils';
|
||||
|
||||
/**
|
||||
* Credit note table actions bar.
|
||||
@@ -54,6 +56,9 @@ function CreditNotesActionsBar({
|
||||
// credit note list context.
|
||||
const { CreditNotesView, fields, refresh } = useCreditNoteListContext();
|
||||
|
||||
// Exports pdf document.
|
||||
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
|
||||
|
||||
// Handle view tab change.
|
||||
const handleTabChange = (view) => {
|
||||
setCreditNotesTableState({ viewSlug: view ? view.slug : null });
|
||||
@@ -68,21 +73,22 @@ function CreditNotesActionsBar({
|
||||
const handleRefreshBtnClick = () => {
|
||||
refresh();
|
||||
};
|
||||
|
||||
// Handle table row size change.
|
||||
const handleTableRowSizeChange = (size) => {
|
||||
addSetting('creditNote', 'tableSize', size);
|
||||
};
|
||||
|
||||
// Handle import button click.
|
||||
const handleImportBtnClick = () => {
|
||||
history.push('/credit-notes/import');
|
||||
};
|
||||
|
||||
// Handle the export button click.
|
||||
const handleExportBtnClick = () => {
|
||||
openDialog(DialogsName.Export, { resource: 'credit_note' });
|
||||
};
|
||||
// Handle print button click.
|
||||
const handlePrintBtnClick = () => {
|
||||
downloadExportPdf({ resource: 'CreditNote' });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -121,6 +127,7 @@ function CreditNotesActionsBar({
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'print-16'} iconSize={'16'} />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePrintBtnClick}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
|
||||
@@ -26,12 +26,14 @@ import withEstimates from './withEstimates';
|
||||
import withEstimatesActions from './withEstimatesActions';
|
||||
import withSettings from '@/containers/Settings/withSettings';
|
||||
import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
|
||||
import { useEstimatesListContext } from './EstimatesListProvider';
|
||||
import { useRefreshEstimates } from '@/hooks/query/estimates';
|
||||
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
|
||||
|
||||
import { SaleEstimateAction, AbilitySubject } from '@/constants/abilityOption';
|
||||
import { compose } from '@/utils';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
/**
|
||||
@@ -58,6 +60,9 @@ function EstimateActionsBar({
|
||||
// Estimates list context.
|
||||
const { estimatesViews, fields } = useEstimatesListContext();
|
||||
|
||||
// Exports pdf document.
|
||||
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
|
||||
|
||||
// Handle click a new sale estimate.
|
||||
const onClickNewEstimate = () => {
|
||||
history.push('/estimates/new');
|
||||
@@ -71,17 +76,14 @@ function EstimateActionsBar({
|
||||
viewSlug: view ? view.slug : null,
|
||||
});
|
||||
};
|
||||
|
||||
// Handle click a refresh sale estimates
|
||||
const handleRefreshBtnClick = () => {
|
||||
refresh();
|
||||
};
|
||||
|
||||
// Handle table row size change.
|
||||
const handleTableRowSizeChange = (size) => {
|
||||
addSetting('salesEstimates', 'tableSize', size);
|
||||
};
|
||||
|
||||
// Handle the import button click.
|
||||
const handleImportBtnClick = () => {
|
||||
history.push('/estimates/import');
|
||||
@@ -90,6 +92,10 @@ function EstimateActionsBar({
|
||||
const handleExportBtnClick = () => {
|
||||
openDialog(DialogsName.Export, { resource: 'sale_estimate' });
|
||||
};
|
||||
// Handles the print button click.
|
||||
const handlePrintBtnClick = () => {
|
||||
downloadExportPdf({ resource: 'SaleEstimate' });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -138,8 +144,8 @@ function EstimateActionsBar({
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'print-16'} iconSize={'16'} />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePrintBtnClick}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'file-import-16'} />}
|
||||
@@ -180,5 +186,5 @@ export default compose(
|
||||
withSettings(({ estimatesSettings }) => ({
|
||||
estimatesTableSize: estimatesSettings?.tableSize,
|
||||
})),
|
||||
withDialogActions
|
||||
withDialogActions,
|
||||
)(EstimateActionsBar);
|
||||
|
||||
@@ -23,6 +23,7 @@ import { SaleInvoiceAction, AbilitySubject } from '@/constants/abilityOption';
|
||||
|
||||
import { useRefreshInvoices } from '@/hooks/query/invoices';
|
||||
import { useInvoicesListContext } from './InvoicesListProvider';
|
||||
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
|
||||
|
||||
import withInvoices from './withInvoices';
|
||||
import withInvoiceActions from './withInvoiceActions';
|
||||
@@ -49,13 +50,16 @@ function InvoiceActionsBar({
|
||||
addSetting,
|
||||
|
||||
// #withDialogsActions
|
||||
openDialog
|
||||
openDialog,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
|
||||
// Sale invoices list context.
|
||||
const { invoicesViews, invoicesFields } = useInvoicesListContext();
|
||||
|
||||
// Exports pdf document.
|
||||
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
|
||||
|
||||
// Handle new invoice button click.
|
||||
const handleClickNewInvoice = () => {
|
||||
history.push('/invoices/new');
|
||||
@@ -88,6 +92,10 @@ function InvoiceActionsBar({
|
||||
const handleExportBtnClick = () => {
|
||||
openDialog(DialogsName.Export, { resource: 'sale_invoice' });
|
||||
};
|
||||
// Handles the print button click.
|
||||
const handlePrintBtnClick = () => {
|
||||
downloadExportPdf({ resource: 'SaleInvoice' });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -134,6 +142,7 @@ function InvoiceActionsBar({
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'print-16'} iconSize={'16'} />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePrintBtnClick}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
|
||||
@@ -31,8 +31,11 @@ import {
|
||||
PaymentReceiveAction,
|
||||
AbilitySubject,
|
||||
} from '@/constants/abilityOption';
|
||||
|
||||
import { usePaymentReceivesListContext } from './PaymentReceiptsListProvider';
|
||||
import { useRefreshPaymentReceive } from '@/hooks/query/paymentReceives';
|
||||
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
@@ -61,6 +64,9 @@ function PaymentReceiveActionsBar({
|
||||
// Payment receives list context.
|
||||
const { paymentReceivesViews, fields } = usePaymentReceivesListContext();
|
||||
|
||||
// Exports pdf document.
|
||||
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
|
||||
|
||||
// Handle new payment button click.
|
||||
const handleClickNewPaymentReceive = () => {
|
||||
history.push('/payment-receives/new');
|
||||
@@ -91,6 +97,10 @@ function PaymentReceiveActionsBar({
|
||||
const handleExportBtnClick = () => {
|
||||
openDialog(DialogsName.Export, { resource: 'payment_receive' });
|
||||
};
|
||||
// Handles the print button click.
|
||||
const handlePrintBtnClick = () => {
|
||||
downloadExportPdf({ resource: 'PaymentReceive' });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -137,6 +147,7 @@ function PaymentReceiveActionsBar({
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'print-16'} iconSize={'16'} />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePrintBtnClick}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
|
||||
@@ -29,14 +29,15 @@ import withReceipts from './withReceipts';
|
||||
import withReceiptsActions from './withReceiptsActions';
|
||||
import withSettings from '@/containers/Settings/withSettings';
|
||||
import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
|
||||
import { useReceiptsListContext } from './ReceiptsListProvider';
|
||||
import { useRefreshReceipts } from '@/hooks/query/receipts';
|
||||
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
|
||||
import { SaleReceiptAction, AbilitySubject } from '@/constants/abilityOption';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
import { compose } from '@/utils';
|
||||
|
||||
/**
|
||||
* Receipts actions bar.
|
||||
@@ -62,6 +63,9 @@ function ReceiptActionsBar({
|
||||
// Sale receipts list context.
|
||||
const { receiptsViews, fields } = useReceiptsListContext();
|
||||
|
||||
// Exports pdf document.
|
||||
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
|
||||
|
||||
// Handle new receipt button click.
|
||||
const onClickNewReceipt = () => {
|
||||
history.push('/receipts/new');
|
||||
@@ -95,6 +99,10 @@ function ReceiptActionsBar({
|
||||
const handleExportBtnClick = () => {
|
||||
openDialog(DialogsName.Export, { resource: 'sale_receipt' });
|
||||
};
|
||||
// Handle print button click.
|
||||
const handlePrintButtonClick = () => {
|
||||
downloadExportPdf({ resource: 'SaleReceipt' });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -143,8 +151,8 @@ function ReceiptActionsBar({
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'print-16'} iconSize={'16'} />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePrintButtonClick}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'file-import-16'} />}
|
||||
|
||||
@@ -22,10 +22,12 @@ import {
|
||||
AdvancedFilterPopover,
|
||||
} from '@/components';
|
||||
|
||||
import { useRefreshVendors } from '@/hooks/query/vendors';
|
||||
import { VendorAction, AbilitySubject } from '@/constants/abilityOption';
|
||||
|
||||
import { useRefreshVendors } from '@/hooks/query/vendors';
|
||||
import { useVendorsListContext } from './VendorsListProvider';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useDownloadExportPdf } from '@/hooks/query/FinancialReports/use-export-pdf';
|
||||
|
||||
import withVendors from './withVendors';
|
||||
import withVendorsActions from './withVendorsActions';
|
||||
@@ -61,11 +63,13 @@ function VendorActionsBar({
|
||||
// Vendors list context.
|
||||
const { vendorsViews, fields } = useVendorsListContext();
|
||||
|
||||
// Exports pdf document.
|
||||
const { downloadAsync: downloadExportPdf } = useDownloadExportPdf();
|
||||
|
||||
// Handles new vendor button click.
|
||||
const onClickNewVendor = () => {
|
||||
history.push('/vendors/new');
|
||||
};
|
||||
|
||||
// Vendors refresh action.
|
||||
const { refresh } = useRefreshVendors();
|
||||
|
||||
@@ -73,31 +77,30 @@ function VendorActionsBar({
|
||||
const handleTabChange = (viewSlug) => {
|
||||
setVendorsTableState({ viewSlug });
|
||||
};
|
||||
|
||||
// Handle inactive switch changing.
|
||||
const handleInactiveSwitchChange = (event) => {
|
||||
const checked = event.target.checked;
|
||||
setVendorsTableState({ inactiveMode: checked });
|
||||
};
|
||||
|
||||
// Handle click a refresh sale estimates
|
||||
const handleRefreshBtnClick = () => {
|
||||
refresh();
|
||||
};
|
||||
|
||||
const handleTableRowSizeChange = (size) => {
|
||||
addSetting('vendors', 'tableSize', size);
|
||||
};
|
||||
|
||||
// Handle import button success.
|
||||
const handleImportBtnSuccess = () => {
|
||||
history.push('/vendors/import');
|
||||
};
|
||||
|
||||
// Handle the export button click.
|
||||
const handleExportBtnClick = () => {
|
||||
openDialog(DialogsName.Export, { resource: 'vendor' });
|
||||
};
|
||||
// Handle the print button click.
|
||||
const handlePrintBtnClick = () => {
|
||||
downloadExportPdf({ resource: 'Vendor' });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -140,6 +143,13 @@ function VendorActionsBar({
|
||||
intent={Intent.DANGER}
|
||||
/>
|
||||
</If>
|
||||
<NavbarDivider />
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="print-16" iconSize={16} />}
|
||||
text={<T id={'print'} />}
|
||||
onClick={handlePrintBtnClick}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="file-import-16" iconSize={16} />}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -134,7 +134,6 @@ const invalidateResourcesOnImport = (
|
||||
queryClient: QueryClient,
|
||||
resource: string,
|
||||
) => {
|
||||
debugger;
|
||||
switch (resource) {
|
||||
case 'Item':
|
||||
queryClient.invalidateQueries(T.ITEMS);
|
||||
|
||||
25
packages/webapp/src/hooks/utils/useOpenPlaidConnect.ts
Normal file
25
packages/webapp/src/hooks/utils/useOpenPlaidConnect.ts
Normal 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 };
|
||||
};
|
||||
@@ -190,7 +190,10 @@ $dashboard-views-bar-height: 44px;
|
||||
background: #a7b6c21f;
|
||||
color: #32304a;
|
||||
}
|
||||
|
||||
&.bp4-disabled{
|
||||
background: transparent;
|
||||
color: rgba(50, 48, 74, 0.4);
|
||||
}
|
||||
&.has-active-filters {
|
||||
|
||||
&,
|
||||
|
||||
76
packages/webapp/src/utils/async-toast-progress.tsx
Normal file
76
packages/webapp/src/utils/async-toast-progress.tsx
Normal 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user