Compare commits

...

39 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
2ada57a2b4 fix: auto-increment setting parsing. (#453) 2024-05-17 12:49:04 +02:00
Ahmed Bouhuolia
e380c598d3 fix: Showing the real mail address on email confirmation view (#445)
* fix: Showing the real mail address on email confirmation view

* chore: remove the unused hook
2024-05-15 14:03:58 +02:00
allcontributors[bot]
370a8a4b91 docs: add ccantrell72 as a contributor for bug (#441)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-05-13 08:42:27 +02:00
Chris Cantrell
c9ba9500cc fix: typo in setup wizard (#440)
Fixed spelling error
2024-05-13 08:39:51 +02:00
Ahmed Bouhuolia
23d27cafc1 fix: Update Dockerfile 2024-05-10 11:36:01 +02:00
Ahmed Bouhuolia
92e3d31360 fix: use server container from Docker registry instead 2024-05-09 20:10:49 +02:00
Ahmed Bouhuolia
8aefa7709c feat: Combine arm64 and amd64 in one Github action runner (#437) 2024-05-09 19:25:51 +02:00
Ahmed Bouhuolia
3020295841 feat: Create a manifest list for webapp Docker image and push it to DockerHub. (#436) 2024-05-09 15:37:07 +02:00
Ahmed Bouhuolia
7f31a48755 chore: dump the CHANGELOG.md file 2024-05-06 23:38:11 +02:00
Ahmed Bouhuolia
8c0ef61038 hotfix: add the pnpm-workspace.yaml file to Dockerfile 2024-05-06 22:28:13 +02:00
Ahmed Bouhuolia
76bb82f2b4 chore: update the pnpm-lock file 2024-05-06 22:25:45 +02:00
Ahmed Bouhuolia
4c0dc276dd hotfix: rollback the pnpm-workspace.yaml file 2024-05-06 22:04:15 +02:00
Ahmed Bouhuolia
a69c4b4067 Merge remote-tracking branch 'refs/remotes/origin/develop' into develop 2024-05-06 21:13:06 +02:00
Ahmed Bouhuolia
d81e544e82 chore: update pnpm-lock.yaml version 2024-05-06 21:11:39 +02:00
Ahmed Bouhuolia
6eeda23559 Update README.md 2024-05-06 20:04:21 +02:00
Ahmed Bouhuolia
cd046cbe27 hotbug: remove the pnpm-workspace.yaml from Dockerfile 2024-05-06 17:55:26 +02:00
Ahmed Bouhuolia
9b7bc1e5b9 Merge branch 'main' into develop 2024-05-06 17:51:24 +02:00
Ahmed Bouhuolia
987341ed29 Merge branch 'main' into develop 2024-05-06 17:50:02 +02:00
Ahmed Bouhuolia
d7cad17f1b Merge pull request #426 from bigcapitalhq/big-163-user-email-verification-after-signing-up
feat: User email verification after signing-up.
2024-05-06 17:46:26 +02:00
Ahmed Bouhuolia
dd02ae471e feat: redirect to the setup page if user is verified 2024-05-06 17:45:32 +02:00
Ahmed Bouhuolia
a5bfb0b02b fix: the email confirmation link on mail message 2024-05-06 17:36:36 +02:00
Ahmed Bouhuolia
f4440c9a03 feat: ability to enable/disable email confirmation from env variables 2024-05-03 18:30:19 +02:00
Ahmed Bouhuolia
cb88c234d1 feat: sync the isVerified state of authed user 2024-05-03 16:00:31 +02:00
Ahmed Bouhuolia
f6a0476fb4 feat: bump CHANGELOG 2024-05-03 11:28:11 +02:00
Ahmed Bouhuolia
63cdc45295 Merge pull request #430 from bigcapitalhq/export-resources
feat: Export resource data to csv, xlsx
2024-05-02 16:20:20 +02:00
Ahmed Bouhuolia
83a5010dc5 feat: flatten the nested columns of exported data 2024-05-02 15:38:57 +02:00
Ahmed Bouhuolia
55aab76c9b feat: configure columns of resources export 2024-05-01 20:15:35 +02:00
Ahmed Bouhuolia
495941f43a feat: style the export dialog form 2024-05-01 19:09:53 +02:00
Ahmed Bouhuolia
00a1e070c6 feat: export dialog on resource table 2024-05-01 16:26:10 +02:00
Ahmed Bouhuolia
fab71d2b65 feat: wip configure resources to be exportable 2024-05-01 12:45:24 +02:00
Ahmed Bouhuolia
9504bb5ccd fix: Running migration Docker container on Windows (#432) 2024-05-01 00:52:23 +02:00
Ahmed Bouhuolia
7e89966f20 feat: wip export resource data 2024-05-01 00:20:13 +02:00
Ahmed Bouhuolia
8a96c41258 feat: export resource data to csv, xlsx 2024-04-29 23:45:11 +02:00
Vederis Leunardus
4a713980bf feat: Pushing docker containers by version tag (#421)
* feat: build docker image based on tag version

* refactor: tabbing

* refactor: revert conflict
2024-04-29 11:39:47 +02:00
Ahmed Bouhuolia
2a1cbf6ced Merge branch 'main' into develop 2024-04-28 18:18:31 +02:00
Ahmed Bouhuolia
9103b60653 feat: New Relic tracking (#429) 2024-04-28 18:12:59 +02:00
Ahmed Bouhuolia
b9fc0cdd9e feat: wip email confirmation 2024-04-28 17:51:11 +02:00
Ahmed Bouhuolia
4368c18479 feat: User email verification after signing-up. 2024-04-26 12:21:40 +02:00
Ahmed Bouhuolia
7e5c6b6487 hotfix: parse the mail secure env variable (#424) 2024-04-24 21:09:56 +02:00
136 changed files with 4678 additions and 287 deletions

View File

@@ -123,6 +123,15 @@
"contributions": [
"code"
]
},
{
"login": "ccantrell72",
"name": "Chris Cantrell",
"avatar_url": "https://avatars.githubusercontent.com/u/104120598?v=4",
"profile": "http://www.pivoten.com",
"contributions": [
"bug"
]
}
],
"contributorsPerLine": 7,

View File

@@ -48,6 +48,9 @@ SIGNUP_DISABLED=false
SIGNUP_ALLOWED_DOMAINS=
SIGNUP_ALLOWED_EMAILS=
# Sign-up Email Confirmation
SIGNUP_EMAIL_CONFIRMATION=false
# API rate limit (points,duration,block duration).
API_RATE_LIMIT=120,60,600

View File

@@ -6,18 +6,13 @@ on:
workflow_dispatch:
env:
REGISTRY: ghcr.io
WEBAPP_IMAGE_NAME: bigcapital/bigcapital-webapp
SERVER_IMAGE_NAME: bigcapital/bigcapital-server
WEBAPP_IMAGE_NAME: bigcapitalhq/webapp
SERVER_IMAGE_NAME: bigcapitalhq/server
jobs:
build-publish-webapp:
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
name: Build and deploy webapp container
runs-on: ubuntu-latest
environment: production
@@ -30,9 +25,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -40,27 +32,26 @@ jobs:
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GH_TOKEN }}
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.REGISTRY }}/${{ env.WEBAPP_IMAGE_NAME }}
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: .
context: ./
file: ./packages/webapp/Dockerfile
platforms: ${{ matrix.platform }}
platforms: linux/amd64,linux/arm64
push: true
tags: ghcr.io/bigcapitalhq/webapp:latest
labels: ${{ steps.meta.outputs.labels }}
tags: bigcapitalhq/webapp:latest, bigcapitalhq/webapp:${{github.ref_name}}
- name: Export digest
run: |
@@ -71,7 +62,7 @@ jobs:
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-main-${{ env.PLATFORM_PAIR }}
name: digests-webapp
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
@@ -93,9 +84,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -103,9 +91,8 @@ jobs:
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GH_TOKEN }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# Builds and push the Docker image.
- name: Build and push Docker image
@@ -114,9 +101,9 @@ jobs:
with:
context: ./
file: ./packages/server/Dockerfile
platforms: ${{ matrix.platform }}
platforms: linux/amd64,linux/arm64
push: true
tags: ghcr.io/bigcapitalhq/server:latest
tags: bigcapitalhq/server:latest, bigcapitalhq/server:${{github.ref_name}}
labels: ${{ steps.meta.outputs.labels }}
- name: Export digest
@@ -128,13 +115,13 @@ jobs:
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-main-${{ env.PLATFORM_PAIR }}
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 }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

View File

@@ -2,6 +2,76 @@
All notable changes to Bigcapital server-side will be in this file.
## [0.16.11] - 06-05-2024
### improvements
* feat: Export resource data to csv, xlsx by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/430
* feat: User email verification after signing-up. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/426
### Fixes
* feat(repo): upgrade to latest lerna v8 and pnpm v9 by @benpsnyder in https://github.com/bigcapitalhq/bigcapital/pull/414
* feat: Update Docker Build-Push Action and Add ARM64 Support by @cloudsbird in https://github.com/bigcapitalhq/bigcapital/pull/412
* feat: Pushing docker containers by version tag by @cloudsbird in https://github.com/bigcapitalhq/bigcapital/pull/421
## [0.16.10]
* fix: Running migration Docker container on Windows by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/432
## [0.16.9]
* feat: New Relic for tracking by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/429
## [0.16.8]
* feat: Ability to enable/disable the bank connect feature by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/423
## [0.16.6]
* hotfix: fix the subscription plan when subscribe on cloud by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/422
## [0.16.5]
IMPORTANT: If you upgraded to the v0.16 recently you should upgrade to v0.16.4 as soon as possible, because there're some breaking changes affected the sign-in and some users reported couldn't sign-in.
* feat: Seed free subscription to tenants that have no subscription. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/410
## [0.16.3]
* feat: Integrate Lemon Squeezy payment by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/402
* feat: optimize the onboarding subscription experience. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/404
* feat: subscription page content by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/405
* feat: auto subscribe to free plan once signup on community version. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/406
* chore: add default value to env variable by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/407
* fix: absolute storage imports path. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/408
## [0.16.0]
* feat: add convert to invoice button on estimate drawer toolbar by @ANasouf in https://github.com/bigcapitalhq/bigcapital/pull/361
* feat(webapp): add mark as delivered to action bar of invoice details … by @ANasouf in https://github.com/bigcapitalhq/bigcapital/pull/360
* feat(webapp): Dialog to choose the bank service provider by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/378
* feat: Categorize the bank synced transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/377
* feat: uncategorize the cashflow transaction by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/381
* Import resources from csv/xlsx by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/382
* feat(webapp): import resource UI by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/386
* fix: import resources improvements by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/388
* feat: add sample sheet to accounts and bank transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/389
* fix: show the unique row value in the import preview by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/392
* feat: advanced parser for numeric and boolean import values by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/394
* feat: validate the given imported sheet whether is empty by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/395
* feat: linking relation with id in importing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/393
* feat: Aggregate rows import by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/396
* feat: clean up the imported temp files by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/400
* feat: add hints to import fields by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/401
## [0.15.0]
* feat: Printing financial reports by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/363
* feat: Convert invoice status after sending mail notification by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/332
* feat: Bigcapital <> Plaid Integration by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/346
* fix: Broken transactions by vendor report by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/369
* fix: Optimize the print style some financial reports by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/370
## [0.14.0] - 30-01-2024
* feat: purchases by items exporting by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/327

View File

@@ -1,6 +1,6 @@
<p align="center">
<p align="center">
<a href="https://bigcapital.ly" target="_blank">
<a href="https://bigcapital.app" target="_blank">
<img src="https://raw.githubusercontent.com/abouolia/blog/main/public/bigcapital.svg" alt="Bigcapital" width="280" height="75">
</a>
</p>
@@ -27,7 +27,7 @@
</p>
<p align="center">
<a href="https://app.bigcapital.ly">Bigcapital Cloud</a>
<a href="https://my.bigcapital.app">Bigcapital Cloud</a>
</p>
</p>
@@ -51,7 +51,7 @@ Bigcapital is available open-source under AGPL license. You can host it on your
### Docker
To get started with self-hosted with Docker and Docker Compose, take a look at the [Docker guide](https://docs.bigcapital.ly/deployment/docker).
To get started with self-hosted with Docker and Docker Compose, take a look at the [Docker guide](https://docs.bigcapital.app/deployment/docker).
## Development
@@ -74,7 +74,7 @@ You can integrate Bigcapital API with your system to organize your transactions
# Resources
- [Documentation](https://docs.bigcapital.ly/) - Learn how to use.
- [Documentation](https://docs.bigcapital.app/) - Learn how to use.
- [Contribution](https://github.com/bigcapitalhq/bigcapital/blob/develop/CONTRIBUTING.md) - Welcome to any contributions.
- [Discord](https://discord.com/invite/c8nPBJafeb) - Ask for help.
- [Bug Tracker](https://github.com/bigcapitalhq/bigcapital/issues) - Notify us new bugs.
@@ -124,6 +124,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asenawritescode"><img src="https://avatars.githubusercontent.com/u/67445192?v=4?s=100" width="100px;" alt="Asena"/><br /><sub><b>Asena</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Aasenawritescode" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://snyder.tech"><img src="https://avatars.githubusercontent.com/u/707567?v=4?s=100" width="100px;" alt="Ben Snyder"/><br /><sub><b>Ben Snyder</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=benpsnyder" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://vederis.id"><img src="https://avatars.githubusercontent.com/u/13505006?v=4?s=100" width="100px;" alt="Vederis Leunardus"/><br /><sub><b>Vederis Leunardus</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=cloudsbird" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.pivoten.com"><img src="https://avatars.githubusercontent.com/u/104120598?v=4?s=100" width="100px;" alt="Chris Cantrell"/><br /><sub><b>Chris Cantrell</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Accantrell72" title="Bug reports">🐛</a></td>
</tr>
</tbody>
</table>

View File

@@ -25,12 +25,12 @@ services:
webapp:
container_name: bigcapital-webapp
image: ghcr.io/bigcapitalhq/webapp:latest
image: bigcapitalhq/webapp:latest
restart: on-failure
server:
container_name: bigcapital-server
image: ghcr.io/bigcapitalhq/server:latest
image: bigcapitalhq/server:latest
expose:
- '3000'
links:
@@ -102,6 +102,14 @@ services:
- LEMONSQUEEZY_WEBHOOK_SECRET=${LEMONSQUEEZY_WEBHOOK_SECRET}
- HOSTED_ON_BIGCAPITAL_CLOUD=${HOSTED_ON_BIGCAPITAL_CLOUD}
# New Relic matrics tracking.
- NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=${NEW_RELIC_DISTRIBUTED_TRACING_ENABLED}
- NEW_RELIC_LOG=${NEW_RELIC_LOG}
- NEW_RELIC_AI_MONITORING_ENABLED=${NEW_RELIC_AI_MONITORING_ENABLED}
- NEW_RELIC_CUSTOM_INSIGHTS_EVENTS_MAX_SAMPLES_STORED=${NEW_RELIC_CUSTOM_INSIGHTS_EVENTS_MAX_SAMPLES_STORED}
- NEW_RELIC_SPAN_EVENTS_MAX_SAMPLES_STORED=${NEW_RELIC_SPAN_EVENTS_MAX_SAMPLES_STORED}
database_migration:
container_name: bigcapital-database-migration
build:

View File

@@ -1,4 +1,4 @@
FROM ghcr.io/bigcapitalhq/server:latest as build
FROM bigcapitalhq/server:latest as build
ARG DB_HOST= \
DB_USER= \
@@ -34,7 +34,5 @@ WORKDIR /app/packages/server
RUN git clone https://github.com/vishnubob/wait-for-it.git
ADD docker/migration/start.sh /
RUN chmod +x /start.sh
CMD ["/start.sh"]
# Once we listen the mysql port run the migration task.
CMD ./wait-for-it/wait-for-it.sh mysql:3306 -- sh -c "node ./build/commands.js system:migrate:latest && node ./build/commands.js tenants:migrate:latest"

View File

@@ -1,5 +0,0 @@
# Migrate the master system database.
./wait-for-it/wait-for-it.sh mysql:3306 -- node ./build/commands.js system:migrate:latest
# Migrate all tenants.
./wait-for-it/wait-for-it.sh mysql:3306 -- node ./build/commands.js tenants:migrate:latest

View File

@@ -78,6 +78,9 @@ ENV MAIL_HOST=$MAIL_HOST \
SIGNUP_ALLOWED_DOMAINS=$SIGNUP_ALLOWED_DOMAINS \
SIGNUP_ALLOWED_EMAILS=$SIGNUP_ALLOWED_EMAILS
# New Relic config file.
ENV NEW_RELIC_NO_CONFIG_FILE=true
# Create app directory.
WORKDIR /app
@@ -89,8 +92,8 @@ RUN npm install -g pnpm
# Copy application dependency manifests to the container image.
COPY ./package*.json ./
COPY ./pnpm-lock.yaml ./pnpm-lock.yaml
COPY ./pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY ./lerna.json ./lerna.json
COPY ./pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY ./packages/server/package*.json ./packages/server/
# Install application dependencies

View File

@@ -82,6 +82,7 @@
"mustache": "^3.0.3",
"mysql": "^2.17.1",
"mysql2": "^1.6.5",
"newrelic": "^11.15.0",
"node-cache": "^4.2.1",
"nodemailer": "^6.3.0",
"nodemon": "^1.19.1",

View File

@@ -244,6 +244,7 @@
"account.field.active": "Active",
"account.field.currency": "Currency",
"account.field.balance": "Balance",
"account.field.bank_balance": "Bank Balance",
"account.field.parent_account": "Parent Account",
"account.field.created_at": "Created at",
"item.field.type": "Item Type",
@@ -331,7 +332,7 @@
"bill_payment.field.reference_no": "Reference No.",
"bill_payment.field.description": "Description",
"bill_payment.field.exchange_rate": "Exchange Rate",
"bill_payment.field.statement": "Statement",
"bill_payment.field.note": "Note",
"bill_payment.field.entries.bill": "Bill No.",
"bill_payment.field.entries.payment_amount": "Payment Amount",
"bill_payment.field.reference": "Reference No.",
@@ -431,6 +432,7 @@
"vendor.field.created_at": "Created at",
"vendor.field.balance": "Balance",
"vendor.field.status": "Status",
"vendor.field.note": "Note",
"vendor.field.currency": "Currency",
"vendor.field.status.active": "Active",
"vendor.field.status.inactive": "Inactive",

View File

@@ -9,6 +9,8 @@ import { DATATYPES_LENGTH } from '@/data/DataTypes';
import LoginThrottlerMiddleware from '@/api/middleware/LoginThrottlerMiddleware';
import AuthenticationApplication from '@/services/Authentication/AuthApplication';
import JWTAuth from '@/api/middleware/jwtAuth';
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
@Service()
export default class AuthenticationController extends BaseController {
@Inject()
@@ -28,6 +30,20 @@ export default class AuthenticationController extends BaseController {
asyncMiddleware(this.login.bind(this)),
this.handlerErrors
);
router.use('/register/verify/resend', JWTAuth);
router.use('/register/verify/resend', AttachCurrentTenantUser);
router.post(
'/register/verify/resend',
asyncMiddleware(this.registerVerifyResendMail.bind(this)),
this.handlerErrors
);
router.post(
'/register/verify',
this.signupVerifySchema,
this.validationResult,
asyncMiddleware(this.registerVerify.bind(this)),
this.handlerErrors
);
router.post(
'/register',
this.registerSchema,
@@ -99,6 +115,17 @@ export default class AuthenticationController extends BaseController {
];
}
private get signupVerifySchema(): ValidationChain[] {
return [
check('email')
.exists()
.isString()
.isEmail()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('token').exists().isString(),
];
}
/**
* Reset password schema.
* @returns {ValidationChain[]}
@@ -166,6 +193,58 @@ export default class AuthenticationController extends BaseController {
}
}
/**
* Verifies the provider user's email after signin-up.
* @param {Request} req
* @param {Response}| res
* @param {Function} next
* @returns {Response|void}
*/
private async registerVerify(req: Request, res: Response, next: Function) {
const signUpVerifyDTO: { email: string; token: string } =
this.matchedBodyData(req);
try {
const user = await this.authApplication.signUpConfirm(
signUpVerifyDTO.email,
signUpVerifyDTO.token
);
return res.status(200).send({
type: 'success',
message: 'The given user has verified successfully',
user,
});
} catch (error) {
next(error);
}
}
/**
* Resends the confirmation email to the user.
* @param {Request} req
* @param {Response}| res
* @param {Function} next
*/
private async registerVerifyResendMail(
req: Request,
res: Response,
next: Function
) {
const { user } = req;
try {
const data = await this.authApplication.signUpConfirmResend(user.id);
return res.status(200).send({
type: 'success',
message: 'The given user has verified successfully',
data,
});
} catch (error) {
next(error);
}
}
/**
* Send reset password handler
* @param {Request} req

View File

@@ -0,0 +1,99 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { query } from 'express-validator';
import BaseController from '@/api/controllers/BaseController';
import { ServiceError } from '@/exceptions';
import { ExportApplication } from '@/services/Export/ExportApplication';
import { ACCEPT_TYPE } from '@/interfaces/Http';
@Service()
export class ExportController extends BaseController {
@Inject()
private exportResourceApp: ExportApplication;
/**
* Router constructor method.
*/
router() {
const router = Router();
router.get(
'/',
[
query('resource').exists(),
query('format').isIn(['csv', 'xlsx']).optional(),
],
this.validationResult,
this.export.bind(this),
this.catchServiceErrors
);
return router;
}
/**
* Imports xlsx/csv to the given resource type.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
private async export(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const query = this.matchedQueryData(req);
try {
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_XLSX,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_PDF,
]);
const data = await this.exportResourceApp.export(
tenantId,
query.resource,
acceptType === ACCEPT_TYPE.APPLICATION_XLSX ? 'xlsx' : 'csv'
);
if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(data);
// Retrieves the xlsx format.
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
res.setHeader(
'Content-Disposition',
'attachment; filename=output.xlsx'
);
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return 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);
}
}

View File

@@ -61,6 +61,7 @@ import { TaxRatesController } from './controllers/TaxRates/TaxRates';
import { ImportController } from './controllers/Import/ImportController';
import { BankingController } from './controllers/Banking/BankingController';
import { Webhooks } from './controllers/Webhooks/Webhooks';
import { ExportController } from './controllers/Export/ExportController';
export default () => {
const app = Router();
@@ -141,6 +142,7 @@ export default () => {
dashboard.use('/projects', Container.get(ProjectsController).router());
dashboard.use('/tax-rates', Container.get(TaxRatesController).router());
dashboard.use('/import', Container.get(ImportController).router());
dashboard.use('/export', Container.get(ExportController).router())
dashboard.use('/', Container.get(ProjectTasksController).router());
dashboard.use('/', Container.get(ProjectTimesController).router());

View File

@@ -55,7 +55,7 @@ module.exports = {
mail: {
host: process.env.MAIL_HOST,
port: process.env.MAIL_PORT,
secure: !!parseInt(process.env.MAIL_SECURE, 10),
secure: parseBoolean(defaultTo(process.env.MAIL_SECURE, false), false),
username: process.env.MAIL_USERNAME,
password: process.env.MAIL_PASSWORD,
from: process.env.MAIL_FROM_ADDRESS,
@@ -153,6 +153,13 @@ module.exports = {
),
},
/**
* Sign-up email confirmation
*/
signupConfirmation: {
enabled: parseBoolean<boolean>(process.env.SIGNUP_EMAIL_CONFIRMATION, false),
},
/**
* Puppeteer remote browserless connection.
*/

View File

@@ -66,16 +66,27 @@ export interface IAuthResetedPasswordEventPayload {
password: string;
}
export interface IAuthSendingResetPassword {
user: ISystemUser,
user: ISystemUser;
token: string;
}
export interface IAuthSendedResetPassword {
user: ISystemUser,
user: ISystemUser;
token: string;
}
export interface IAuthGetMetaPOJO {
export interface IAuthGetMetaPOJO {
signupDisabled: boolean;
}
}
export interface IAuthSignUpVerifingEventPayload {
email: string;
verifyToken: string;
userId: number;
}
export interface IAuthSignUpVerifiedEventPayload {
email: string;
verifyToken: string;
userId: number;
}

View File

@@ -126,13 +126,16 @@ export interface IModelMeta {
defaultFilterField: string;
defaultSort: IModelMetaDefaultSort;
importable?: boolean;
exportable?: boolean;
exportFlattenOn?: string;
importable?: boolean;
importAggregator?: string;
importAggregateOn?: string;
importAggregateBy?: string;
fields: { [key: string]: IModelMetaField };
columns: { [key: string]: IModelMetaColumn };
}
// ----
@@ -161,3 +164,22 @@ export type IModelMetaField2 = IModelMetaFieldCommon2 &
| IModelMetaRelationField2
| IModelMetaCollectionField
);
export interface ImodelMetaColumnMeta {
name: string;
accessor?: string;
exportable?: boolean;
}
interface IModelMetaColumnText {
type: 'text;';
}
interface IModelMetaColumnCollection {
type: 'collection';
collectionOf: 'object';
columns: { [key: string]: ImodelMetaColumnMeta & IModelMetaColumnText };
}
export type IModelMetaColumn = ImodelMetaColumnMeta &
(IModelMetaColumnText | IModelMetaColumnCollection);

View File

@@ -91,6 +91,7 @@ import { SaleEstimateMarkApprovedOnMailSent } from '@/services/Sales/Estimates/s
import { DeleteCashflowTransactionOnUncategorize } from '@/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize';
import { PreventDeleteTransactionOnDelete } from '@/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete';
import { SubscribeFreeOnSignupCommunity } from '@/services/Subscription/events/SubscribeFreeOnSignupCommunity';
import { SendVerfiyMailOnSignUp } from '@/services/Authentication/events/SendVerfiyMailOnSignUp';
export default () => {
@@ -222,6 +223,7 @@ export const susbcribers = () => {
DeleteCashflowTransactionOnUncategorize,
PreventDeleteTransactionOnDelete,
SubscribeFreeOnSignupCommunity
SubscribeFreeOnSignupCommunity,
SendVerfiyMailOnSignUp
];
};

View File

@@ -12,6 +12,7 @@ import { SaleReceiptMailNotificationJob } from '@/services/Sales/Receipts/SaleRe
import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob';
import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob';
import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDeleteExpiredFilesJob';
import { SendVerifyMailJob } from '@/services/Authentication/jobs/SendVerifyMailJob';
export default ({ agenda }: { agenda: Agenda }) => {
new ResetPasswordMailJob(agenda);
@@ -27,6 +28,7 @@ export default ({ agenda }: { agenda: Agenda }) => {
new PaymentReceiveMailNotificationJob(agenda);
new PlaidFetchTransactionsJob(agenda);
new ImportDeleteExpiredFilesJobs(agenda);
new SendVerifyMailJob(agenda);
agenda.start().then(() => {
agenda.every('1 hours', 'delete-expired-imported-files', {});

View File

@@ -7,6 +7,7 @@ export default {
sortField: 'name',
},
importable: true,
exportable: true,
fields: {
name: {
name: 'account.field.name',
@@ -85,6 +86,55 @@ export default {
fieldType: 'date',
},
},
columns: {
name: {
name: 'account.field.name',
type: 'text',
},
code: {
name: 'account.field.code',
type: 'text',
},
rootType: {
name: 'account.field.root_type',
type: 'text',
accessor: 'accountRootType',
},
accountType: {
name: 'account.field.type',
accessor: 'accountTypeLabel',
type: 'text',
},
accountNormal: {
name: 'account.field.normal',
accessor: 'accountNormalFormatted',
},
currencyCode: {
name: 'account.field.currency',
type: 'text',
},
bankBalance: {
name: 'account.field.bank_balance',
accessor: 'bankBalanceFormatted',
type: 'text',
exportable: true,
},
balance: {
name: 'account.field.balance',
accessor: 'amount',
},
description: {
name: 'account.field.description',
type: 'text',
},
active: {
name: 'account.field.active',
type: 'boolean',
},
createdAt: {
name: 'account.field.created_at',
},
},
fields2: {
name: {
name: 'account.field.name',

View File

@@ -5,6 +5,8 @@ export default {
sortField: 'bill_date',
},
importable: true,
exportFlattenOn: 'entries',
exportable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'billNumber',
@@ -80,6 +82,84 @@ export default {
fieldType: 'date',
},
},
columns: {
billNumber: {
name: 'Bill No.',
type: 'text',
},
referenceNo: {
name: 'Reference No.',
type: 'text',
},
billDate: {
name: 'Date',
type: 'date',
},
dueDate: {
name: 'Due Date',
type: 'date',
},
vendorId: {
name: 'Vendor',
accessor: 'vendor.displayName',
type: 'text',
},
amount: {
name: 'Amount',
accessor: 'formattedAmount',
},
exchangeRate: {
name: 'Exchange Rate',
type: 'number',
},
currencyCode: {
name: 'Currency Code',
type: 'text',
},
dueAmount: {
name: 'Due Amount',
accessor: 'formattedDueAmount',
},
paidAmount: {
name: 'Paid Amount',
accessor: 'formattedPaymentAmount',
},
note: {
name: 'Note',
type: 'text',
},
open: {
name: 'Open',
type: 'boolean',
},
entries: {
name: 'Entries',
accessor: 'entries',
type: 'collection',
collectionOf: 'object',
columns: {
itemName: {
name: 'Item Name',
accessor: 'item.name',
},
rate: {
name: 'Item Rate',
accessor: 'rateFormatted',
},
quantity: {
name: 'Item Quantity',
accessor: 'quantityFormatted',
},
description: {
name: 'Item Description',
},
amount: {
name: 'Item Amount',
accessor: 'totalFormatted',
},
},
},
},
fields2: {
billNumber: {
name: 'Bill No.',
@@ -132,7 +212,7 @@ export default {
relationModel: 'Item',
relationImportMatch: ['name', 'code'],
required: true,
importHint: "Matches the item name or code."
importHint: 'Matches the item name or code.',
},
rate: {
name: 'Rate',

View File

@@ -4,6 +4,7 @@ export default {
sortOrder: 'DESC',
sortField: 'bill_date',
},
exportable: true,
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
@@ -67,6 +68,46 @@ export default {
fieldType: 'date',
},
},
columns: {
vendor: {
name: 'bill_payment.field.vendor',
type: 'relation',
accessor: 'vendor.displayName',
},
paymentDate: {
name: 'bill_payment.field.payment_date',
type: 'date',
},
paymentNumber: {
name: 'bill_payment.field.payment_number',
type: 'text',
},
paymentAccount: {
name: 'bill_payment.field.payment_account',
accessor: 'paymentAccount.name',
type: 'text',
},
amount: {
name: 'Amount',
accessor: 'formattedAmount',
},
currencyCode: {
name: 'Currency Code',
type: 'text',
},
exchangeRate: {
name: 'bill_payment.field.exchange_rate',
type: 'number',
},
statement: {
name: 'bill_payment.field.note',
type: 'text',
},
reference: {
name: 'bill_payment.field.reference',
type: 'text',
},
},
fields2: {
vendorId: {
name: 'bill_payment.field.vendor',
@@ -84,7 +125,7 @@ export default {
name: 'bill_payment.field.payment_number',
fieldType: 'text',
unique: true,
importHint: "The payment number should be unique."
importHint: 'The payment number should be unique.',
},
paymentAccountId: {
name: 'bill_payment.field.payment_account',
@@ -92,14 +133,14 @@ export default {
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
importHint: "Matches the account name or code."
importHint: 'Matches the account name or code.',
},
exchangeRate: {
name: 'bill_payment.field.exchange_rate',
fieldType: 'number',
},
statement: {
name: 'bill_payment.field.statement',
name: 'bill_payment.field.note',
fieldType: 'text',
},
reference: {
@@ -120,7 +161,7 @@ export default {
relationModel: 'Bill',
relationImportMatch: 'billNumber',
required: true,
importHint: "Matches the bill number."
importHint: 'Matches the bill number.',
},
paymentAmount: {
name: 'bill_payment.field.entries.payment_amount',

View File

@@ -12,10 +12,14 @@ export default {
sortOrder: 'DESC',
sortField: 'name',
},
exportable: true,
exportFlattenOn: 'entries',
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'creditNoteNumber',
fields: {
customer: {
name: 'credit_note.field.customer',
@@ -81,6 +85,67 @@ export default {
fieldType: 'date',
},
},
columns: {
customer: {
name: 'Customer',
type: 'relation',
accessor: 'customer.displayName',
},
exchangeRate: {
name: 'Exchange Rate',
type: 'number',
},
creditNoteDate: {
name: 'Credit Note Date',
type: 'date',
},
referenceNo: {
name: 'Reference No.',
type: 'text',
},
note: {
name: 'Note',
type: 'text',
},
termsConditions: {
name: 'Terms & Conditions',
type: 'text',
},
creditNoteNumber: {
name: 'Credit Note Number',
type: 'text',
},
open: {
name: 'Open',
type: 'boolean',
},
entries: {
name: 'Entries',
type: 'collection',
collectionOf: 'object',
columns: {
itemName: {
name: 'Item Name',
accessor: 'item.name',
},
rate: {
name: 'Item Rate',
accessor: 'rateFormatted',
},
quantity: {
name: 'Item Quantity',
accessor: 'quantityFormatted',
},
description: {
name: 'Item Description',
},
amount: {
name: 'Item Amount',
accessor: 'totalFormatted',
},
},
},
},
fields2: {
customerId: {
name: 'Customer',

View File

@@ -1,5 +1,6 @@
export default {
importable: true,
exportable: true,
defaultFilterField: 'displayName',
defaultSort: {
sortOrder: 'DESC',
@@ -90,6 +91,138 @@ export default {
},
},
},
columns: {
firstName: {
name: 'vendor.field.first_name',
type: 'text',
},
lastName: {
name: 'vendor.field.last_name',
type: 'text',
},
displayName: {
name: 'vendor.field.display_name',
type: 'text',
},
email: {
name: 'vendor.field.email',
type: 'text',
},
workPhone: {
name: 'vendor.field.work_phone',
type: 'text',
},
personalPhone: {
name: 'vendor.field.personal_phone',
type: 'text',
},
companyName: {
name: 'vendor.field.company_name',
type: 'text',
},
website: {
name: 'vendor.field.website',
type: 'text',
},
balance: {
name: 'vendor.field.balance',
type: 'number',
},
openingBalance: {
name: 'vendor.field.opening_balance',
type: 'number',
},
openingBalanceAt: {
name: 'vendor.field.opening_balance_at',
type: 'date',
},
currencyCode: {
name: 'vendor.field.currency',
type: 'text',
},
status: {
name: 'vendor.field.status',
},
note: {
name: 'vendor.field.note',
},
// Billing Address
billingAddress1: {
name: 'Billing Address 1',
column: 'billing_address1',
type: 'text',
},
billingAddress2: {
name: 'Billing Address 2',
column: 'billing_address2',
type: 'text',
},
billingAddressCity: {
name: 'Billing Address City',
column: 'billing_address_city',
type: 'text',
},
billingAddressCountry: {
name: 'Billing Address Country',
column: 'billing_address_country',
type: 'text',
},
billingAddressPostcode: {
name: 'Billing Address Postcode',
column: 'billing_address_postcode',
type: 'text',
},
billingAddressState: {
name: 'Billing Address State',
column: 'billing_address_state',
type: 'text',
},
billingAddressPhone: {
name: 'Billing Address Phone',
column: 'billing_address_phone',
type: 'text',
},
// Shipping Address
shippingAddress1: {
name: 'Shipping Address 1',
column: 'shipping_address1',
type: 'text',
},
shippingAddress2: {
name: 'Shipping Address 2',
column: 'shipping_address2',
type: 'text',
},
shippingAddressCity: {
name: 'Shipping Address City',
column: 'shipping_address_city',
type: 'text',
},
shippingAddressCountry: {
name: 'Shipping Address Country',
column: 'shipping_address_country',
type: 'text',
},
shippingAddressPostcode: {
name: 'Shipping Address Postcode',
column: 'shipping_address_postcode',
type: 'text',
},
shippingAddressPhone: {
name: 'Shipping Address Phone',
column: 'shipping_address_phone',
type: 'text',
},
shippingAddressState: {
name: 'Shipping Address State',
column: 'shipping_address_state',
type: 'text',
},
createdAt: {
name: 'vendor.field.created_at',
type: 'date',
},
},
fields2: {
customerType: {
name: 'Customer Type',

View File

@@ -8,6 +8,8 @@ export default {
sortField: 'name',
},
importable: true,
exportFlattenOn: 'categories',
exportable: true,
fields: {
payment_date: {
name: 'expense.field.payment_date',
@@ -61,6 +63,56 @@ export default {
fieldType: 'date',
},
},
columns: {
paymentReceive: {
name: 'expense.field.payment_account',
type: 'text',
accessor: 'paymentAccount.name'
},
referenceNo: {
name: 'expense.field.reference_no',
type: 'text',
},
paymentDate: {
name: 'expense.field.payment_date',
type: 'date',
},
currencyCode: {
name: 'expense.field.currency_code',
type: 'text',
},
exchangeRate: {
name: 'expense.field.exchange_rate',
type: 'number',
},
description: {
name: 'expense.field.description',
type: 'text',
},
categories: {
name: 'expense.field.categories',
type: 'collection',
collectionOf: 'object',
columns: {
expenseAccount: {
name: 'expense.field.expense_account',
accessor: 'expenseAccount.name',
},
amount: {
name: 'expense.field.amount',
accessor: 'amountFormatted',
},
description: {
name: 'expense.field.line_description',
type: 'text',
},
},
},
publish: {
name: 'expense.field.publish',
type: 'boolean',
},
},
fields2: {
paymentAccountId: {
name: 'expense.field.payment_account',
@@ -68,7 +120,7 @@ export default {
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
importHint: "Matches the account name or code."
importHint: 'Matches the account name or code.',
},
referenceNo: {
name: 'expense.field.reference_no',
@@ -102,7 +154,7 @@ export default {
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
importHint: "Matches the account name or code."
importHint: 'Matches the account name or code.',
},
amount: {
name: 'expense.field.amount',

View File

@@ -4,6 +4,54 @@ export default {
sortOrder: 'DESC',
sortField: 'date',
},
columns: {
date: {
name: 'inventory_adjustment.field.date',
column: 'date',
fieldType: 'date',
exportable: true,
},
type: {
name: 'inventory_adjustment.field.type',
column: 'type',
fieldType: 'enumeration',
options: [
{ key: 'increment', name: 'inventory_adjustment.field.type.increment' },
{ key: 'decrement', name: 'inventory_adjustment.field.type.decrement' },
],
exportable: true,
},
adjustmentAccount: {
name: 'inventory_adjustment.field.adjustment_account',
type: 'adjustment_account_id',
exportable: true,
},
reason: {
name: 'inventory_adjustment.field.reason',
type: 'text',
exportable: true,
},
referenceNo: {
name: 'inventory_adjustment.field.reference_no',
type: 'text',
exportable: true,
},
description: {
name: 'inventory_adjustment.field.description',
type: 'text',
exportable: true,
},
publishedAt: {
name: 'inventory_adjustment.field.published_at',
type: 'date',
exportable: true,
},
createdAt: {
name: 'inventory_adjustment.field.created_at',
type: 'date',
exportable: true,
},
},
fields: {
date: {
name: 'inventory_adjustment.field.date',

View File

@@ -1,5 +1,6 @@
export default {
importable: true,
exportable: true,
defaultFilterField: 'name',
defaultSort: {
sortField: 'name',
@@ -121,6 +122,97 @@ export default {
fieldType: 'date',
},
},
columns: {
type: {
name: 'item.field.type',
type: 'text',
exportable: true,
},
name: {
name: 'item.field.name',
type: 'text',
exportable: true,
},
code: {
name: 'item.field.code',
type: 'text',
exportable: true,
},
sellable: {
name: 'item.field.sellable',
type: 'boolean',
exportable: true,
},
purchasable: {
name: 'item.field.purchasable',
type: 'boolean',
exportable: true,
},
sellPrice: {
name: 'item.field.cost_price',
type: 'number',
exportable: true,
},
costPrice: {
name: 'item.field.cost_account',
type: 'number',
exportable: true,
},
costAccount: {
name: 'item.field.sell_account',
type: 'text',
accessor: 'costAccount.name',
exportable: true,
},
sellAccount: {
name: 'item.field.sell_description',
type: 'text',
accessor: 'sellAccount.name',
exportable: true,
},
inventoryAccount: {
name: 'item.field.inventory_account',
type: 'text',
accessor: 'inventoryAccount.name',
exportable: true,
},
sellDescription: {
name: 'Sell description',
type: 'text',
exportable: true,
},
purchaseDescription: {
name: 'Purchase description',
type: 'text',
exportable: true,
},
quantityOnHand: {
name: 'item.field.quantity_on_hand',
type: 'number',
exportable: true,
},
note: {
name: 'item.field.note',
type: 'text',
exportable: true,
},
category: {
name: 'item.field.category',
type: 'text',
accessor: 'category.name',
exportable: true,
},
active: {
name: 'item.field.active',
fieldType: 'boolean',
exportable: true,
},
createdAt: {
name: 'item.field.created_at',
type: 'date',
exportable: true,
},
},
fields2: {
type: {
name: 'item.field.type',
@@ -195,7 +287,7 @@ export default {
fieldType: 'relation',
relationModel: 'ItemCategory',
relationImportMatch: ['name'],
importHint: "Matches the category name."
importHint: 'Matches the category name.',
},
active: {
name: 'item.field.active',

View File

@@ -5,6 +5,7 @@ export default {
sortOrder: 'DESC',
},
importable: true,
exportable: true,
fields: {
name: {
name: 'item_category.field.name',
@@ -28,6 +29,24 @@ export default {
columnType: 'date',
},
},
columns: {
name: {
name: 'item_category.field.name',
type: 'text',
},
description: {
name: 'item_category.field.description',
type: 'text',
},
count: {
name: 'item_category.field.count',
type: 'text',
},
createdAt: {
name: 'item_category.field.created_at',
type: 'text',
},
},
fields2: {
name: {
name: 'item_category.field.name',

View File

@@ -5,6 +5,9 @@ export default {
sortField: 'name',
},
importable: true,
exportFlattenOn: 'entries',
exportable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'journalNumber',
@@ -56,6 +59,76 @@ export default {
fieldType: 'date',
},
},
columns: {
date: {
name: 'manual_journal.field.date',
type: 'date',
},
journalNumber: {
name: 'manual_journal.field.journal_number',
type: 'text',
},
reference: {
name: 'manual_journal.field.reference',
type: 'text',
},
journalType: {
name: 'manual_journal.field.journal_type',
type: 'text',
},
amount: {
name: 'Amount',
accessor: 'formattedAmount',
},
currencyCode: {
name: 'manual_journal.field.currency',
type: 'text',
},
exchangeRate: {
name: 'manual_journal.field.exchange_rate',
type: 'number',
},
description: {
name: 'manual_journal.field.description',
type: 'text',
},
entries: {
name: 'Entries',
type: 'collection',
collectionOf: 'object',
columns: {
credit: {
name: 'Credit',
type: 'text',
},
debit: {
name: 'Debit',
type: 'text',
},
account: {
name: 'Account',
accessor: 'account.name',
},
contact: {
name: 'Contact',
accessor: 'contact.displayName',
},
note: {
name: 'Note',
},
},
publish: {
name: 'Publish',
type: 'boolean',
},
publishedAt: {
name: 'Published At',
},
},
createdAt: {
name: 'Created At',
},
},
fields2: {
date: {
name: 'manual_journal.field.date',

View File

@@ -1,5 +1,6 @@
export default {
importable: true,
exportable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'paymentReceiveNo',
@@ -57,6 +58,42 @@ export default {
fieldDate: 'date',
},
},
columns: {
customer: {
name: 'payment_receive.field.customer',
accessor: 'customer.displayName',
type: 'text',
},
paymentDate: {
name: 'payment_receive.field.payment_date',
type: 'date',
},
amount: {
name: 'payment_receive.field.amount',
type: 'number',
},
referenceNo: {
name: 'payment_receive.field.reference_no',
type: 'text',
},
depositAccount: {
name: 'payment_receive.field.deposit_account',
accessor: 'depositAccount.name',
type: 'text',
},
paymentReceiveNo: {
name: 'payment_receive.field.payment_receive_no',
type: 'text',
},
statement: {
name: 'payment_receive.field.statement',
type: 'text',
},
created_at: {
name: 'payment_receive.field.created_at',
type: 'date',
},
},
fields2: {
customerId: {
name: 'payment_receive.field.customer',
@@ -84,12 +121,12 @@ export default {
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
importHint: "Matches the account name or code."
importHint: 'Matches the account name or code.',
},
paymentReceiveNo: {
name: 'payment_receive.field.payment_receive_no',
fieldType: 'text',
importHint: "The payment number should be unique."
importHint: 'The payment number should be unique.',
},
statement: {
name: 'payment_receive.field.statement',
@@ -108,7 +145,7 @@ export default {
relationModel: 'SaleInvoice',
relationImportMatch: 'invoiceNo',
required: true,
importHint: "Matches the invoice number."
importHint: 'Matches the invoice number.',
},
paymentAmount: {
name: 'payment_receive.field.entries.payment_amount',

View File

@@ -4,6 +4,9 @@ export default {
sortOrder: 'DESC',
sortField: 'estimate_date',
},
exportable: true,
exportFlattenOn: 'entries',
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
@@ -73,6 +76,91 @@ export default {
columnType: 'date',
},
},
columns: {
customer: {
name: 'Customer',
type: 'text',
accessor: 'customer.displayName',
exportable: true,
},
estimateDate: {
name: 'Estimate Date',
type: 'date',
exportable: true,
},
expirationDate: {
name: 'Expiration Date',
type: 'date',
exportable: true,
},
estimateNumber: {
name: 'Estimate No.',
type: 'text',
exportable: true,
},
reference: {
name: 'Reference No.',
type: 'text',
exportable: true,
},
amount: {
name: 'Amount',
accessor: 'formattedAmount',
type: 'text',
},
exchangeRate: {
name: 'Exchange Rate',
type: 'number',
exportable: true,
},
currencyCode: {
name: 'Currency',
type: 'text',
exportable: true,
},
note: {
name: 'Note',
type: 'text',
exportable: true,
},
termsConditions: {
name: 'Terms & Conditions',
type: 'text',
exportable: true,
},
delivered: {
name: 'Delivered',
type: 'boolean',
exportable: true,
},
entries: {
name: 'Entries',
accessor: 'entries',
type: 'collection',
collectionOf: 'object',
columns: {
itemName: {
name: 'Item Name',
accessor: 'item.name',
},
rate: {
name: 'Item Rate',
accessor: 'rateFormatted',
},
quantity: {
name: 'Item Quantity',
accessor: 'quantityFormatted',
},
description: {
name: 'Item Description',
},
amount: {
name: 'Item Amount',
accessor: 'totalFormatted',
},
},
},
},
fields2: {
customerId: {
name: 'Customer',
@@ -132,7 +220,7 @@ export default {
relationModel: 'Item',
relationImportMatch: ['name', 'code'],
required: true,
importHint: "Matches the item name or code."
importHint: 'Matches the item name or code.',
},
rate: {
name: 'invoice.field.rate',

View File

@@ -4,6 +4,9 @@ export default {
sortOrder: 'DESC',
sortField: 'created_at',
},
exportable: true,
exportFlattenOn: 'entries',
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
@@ -87,6 +90,89 @@ export default {
fieldType: 'date',
},
},
columns: {
invoiceDate: {
name: 'invoice.field.invoice_date',
type: 'date',
},
dueDate: {
name: 'invoice.field.due_date',
type: 'date',
},
referenceNo: {
name: 'invoice.field.reference_no',
type: 'text',
},
invoiceNo: {
name: 'invoice.field.invoice_no',
type: 'text',
},
customer: {
name: 'invoice.field.customer',
type: 'text',
accessor: 'customer.displayName',
},
amount: {
name: 'invoice.field.amount',
type: 'text',
accessor: 'balanceAmountFormatted',
},
exchangeRate: {
name: 'invoice.field.exchange_rate',
type: 'number',
},
currencyCode: {
name: 'invoice.field.currency',
type: 'text',
},
paidAmount: {
name: 'Paid Amount',
accessor: 'paymentAmountFormatted',
},
dueAmount: {
name: 'Due Amount',
accessor: 'dueAmountFormatted',
},
invoiceMessage: {
name: 'invoice.field.invoice_message',
type: 'text',
},
termsConditions: {
name: 'invoice.field.terms_conditions',
type: 'text',
},
delivered: {
name: 'invoice.field.delivered',
type: 'boolean',
},
entries: {
name: 'Entries',
accessor: 'entries',
type: 'collection',
collectionOf: 'object',
columns: {
itemName: {
name: 'Item Name',
accessor: 'item.name',
},
rate: {
name: 'Item Rate',
accessor: 'rateFormatted',
},
quantity: {
name: 'Item Quantity',
accessor: 'quantityFormatted',
},
description: {
name: 'Item Description',
},
amount: {
name: 'Item Amount',
accessor: 'totalFormatted',
},
},
},
},
fields2: {
invoiceDate: {
name: 'invoice.field.invoice_date',
@@ -142,7 +228,7 @@ export default {
relationModel: 'Item',
relationImportMatch: ['name', 'code'],
required: true,
importHint: "Matches the item name or code."
importHint: 'Matches the item name or code.',
},
rate: {
name: 'invoice.field.rate',

View File

@@ -4,6 +4,9 @@ export default {
sortOrder: 'DESC',
sortField: 'created_at',
},
exportable: true,
exportFlattenOn: 'entries',
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
@@ -77,6 +80,86 @@ export default {
sortCustomQuery: StatusFieldSortQuery,
},
},
columns: {
amount: {
name: 'receipt.field.amount',
column: 'amount',
type: 'number',
},
depositAccount: {
name: 'receipt.field.deposit_account',
type: 'text',
accessor: 'depositAccount.name',
},
customer: {
name: 'receipt.field.customer',
type: 'text',
accessor: 'customer.displayName',
},
receiptDate: {
name: 'receipt.field.receipt_date',
type: 'date',
},
receiptNumber: {
name: 'receipt.field.receipt_number',
type: 'text',
},
referenceNo: {
name: 'receipt.field.reference_no',
column: 'reference_no',
type: 'text',
exportable: true,
},
receiptMessage: {
name: 'receipt.field.receipt_message',
column: 'receipt_message',
type: 'text',
},
statement: {
name: 'receipt.field.statement',
type: 'text',
},
status: {
name: 'receipt.field.status',
type: 'enumeration',
options: [
{ key: 'draft', label: 'receipt.field.status.draft' },
{ key: 'closed', label: 'receipt.field.status.closed' },
],
exportable: true,
},
entries: {
name: 'Entries',
accessor: 'entries',
type: 'collection',
collectionOf: 'object',
columns: {
itemName: {
name: 'Item Name',
accessor: 'item.name',
},
rate: {
name: 'Item Rate',
accessor: 'rateFormatted',
},
quantity: {
name: 'Item Quantity',
accessor: 'quantityFormatted',
},
description: {
name: 'Item Description',
},
amount: {
name: 'Item Amount',
accessor: 'totalFormatted',
},
},
},
createdAt: {
name: 'receipt.field.created_at',
type: 'date',
},
},
fields2: {
receiptDate: {
name: 'Receipt Date',
@@ -126,7 +209,7 @@ export default {
relationModel: 'Item',
relationImportMatch: ['name', 'code'],
required: true,
importHint: "Matches the item name or code."
importHint: 'Matches the item name or code.',
},
rate: {
name: 'invoice.field.rate',

View File

@@ -5,6 +5,7 @@ export default {
sortField: 'created_at',
},
importable: true,
exportable: true,
fields: {
first_name: {
name: 'vendor.field.first_name',
@@ -32,7 +33,7 @@ export default {
fieldType: 'text',
},
personal_phone: {
name: 'vendor.field.personal_pone',
name: 'vendor.field.personal_phone',
column: 'personal_phone',
fieldType: 'text',
},
@@ -90,6 +91,154 @@ export default {
},
},
},
columns: {
firstName: {
name: 'vendor.field.first_name',
type: 'text',
},
lastName: {
name: 'vendor.field.last_name',
type: 'text',
},
displayName: {
name: 'vendor.field.display_name',
type: 'text',
},
email: {
name: 'vendor.field.email',
type: 'text',
},
workPhone: {
name: 'vendor.field.work_phone',
type: 'text',
},
personalPhone: {
name: 'vendor.field.personal_phone',
type: 'text',
},
companyName: {
name: 'vendor.field.company_name',
type: 'text',
},
website: {
name: 'vendor.field.website',
type: 'text',
},
balance: {
name: 'vendor.field.balance',
type: 'number',
},
openingBalance: {
name: 'vendor.field.opening_balance',
type: 'number',
},
openingBalanceAt: {
name: 'vendor.field.opening_balance_at',
type: 'date',
},
currencyCode: {
name: 'vendor.field.currency',
type: 'text',
},
status: {
name: 'vendor.field.status',
},
note: {
name: 'vendor.field.note',
type: 'text',
},
// Billing Address
billingAddress1: {
name: 'Billing Address 1',
column: 'billing_address1',
type: 'text',
exportable: true,
},
billingAddress2: {
name: 'Billing Address 2',
column: 'billing_address2',
type: 'text',
exportable: true,
},
billingAddressCity: {
name: 'Billing Address City',
column: 'billing_address_city',
type: 'text',
exportable: true,
},
billingAddressCountry: {
name: 'Billing Address Country',
column: 'billing_address_country',
type: 'text',
exportable: true,
},
billingAddressPostcode: {
name: 'Billing Address Postcode',
column: 'billing_address_postcode',
type: 'text',
exportable: true,
},
billingAddressState: {
name: 'Billing Address State',
column: 'billing_address_state',
type: 'text',
exportable: true,
},
billingAddressPhone: {
name: 'Billing Address Phone',
column: 'billing_address_phone',
type: 'text',
exportable: true,
},
// Shipping Address
shippingAddress1: {
name: 'Shipping Address 1',
column: 'shipping_address1',
type: 'text',
exportable: true,
},
shippingAddress2: {
name: 'Shipping Address 2',
column: 'shipping_address2',
type: 'text',
exportable: true,
},
shippingAddressCity: {
name: 'Shipping Address City',
column: 'shipping_address_city',
type: 'text',
exportable: true,
},
shippingAddressCountry: {
name: 'Shipping Address Country',
column: 'shipping_address_country',
type: 'text',
exportable: true,
},
shippingAddressPostcode: {
name: 'Shipping Address Postcode',
column: 'shipping_address_postcode',
type: 'text',
exportable: true,
},
shippingAddressState: {
name: 'Shipping Address State',
column: 'shipping_address_state',
type: 'text',
exportable: true,
},
shippingAddressPhone: {
name: 'Shipping Address Phone',
column: 'shipping_address_phone',
type: 'text',
exportable: true,
},
createdAt: {
name: 'vendor.field.created_at',
type: 'date',
exportable: true,
},
},
fields2: {
firstName: {
name: 'vendor.field.first_name',

View File

@@ -12,10 +12,14 @@ export default {
sortOrder: 'DESC',
sortField: 'name',
},
exportable: true,
exportFlattenOn: 'entries',
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'vendorCreditNumber',
fields: {
vendor: {
name: 'vendor_credit.field.vendor',
@@ -76,6 +80,79 @@ export default {
fieldType: 'date',
},
},
columns: {
vendorId: {
name: 'Vendor',
type: 'relation',
accessor: 'vendor.displayName',
},
exchangeRate: {
name: 'Echange Rate',
type: 'text',
},
vendorCreditNumber: {
name: 'Vendor Credit No.',
type: 'text',
},
referenceNo: {
name: 'Refernece No.',
type: 'text',
},
vendorCreditDate: {
name: 'Vendor Credit Date',
type: 'date',
},
amount: {
name: 'Amount',
accessor: 'formattedAmount',
},
creditRemaining: {
name: 'Credits Remaining',
accessor: 'formattedCreditsRemaining',
},
refundedAmount: {
name: 'Refunded Amount',
accessor: 'refundedAmount',
},
invoicedAmount: {
name: 'Invoiced Amount',
accessor: 'formattedInvoicedAmount',
},
note: {
name: 'Note',
type: 'text',
},
open: {
name: 'Open',
type: 'boolean',
},
entries: {
name: 'Entries',
type: 'collection',
collectionOf: 'object',
columns: {
itemName: {
name: 'Item Name',
accessor: 'item.name',
},
rate: {
name: 'Item Rate',
accessor: 'rateFormatted',
},
quantity: {
name: 'Item Quantity',
accessor: 'quantityFormatted',
},
description: {
name: 'Item Description',
},
amount: {
name: 'Item Amount',
accessor: 'totalFormatted',
},
},
},
},
fields2: {
vendorId: {
name: 'Vendor',
@@ -122,7 +199,7 @@ export default {
relationModel: 'Item',
relationImportMatch: ['name', 'code'],
required: true,
importHint: "Matches the item name or code."
importHint: 'Matches the item name or code.',
},
rate: {
name: 'Rate',

View File

@@ -8,6 +8,34 @@ export default {
sortField: 'name',
sortOrder: 'DESC',
},
columns: {
date: {
name: 'warehouse_transfer.field.date',
type: 'date',
exportable: true,
},
transaction_number: {
name: 'warehouse_transfer.field.transaction_number',
type: 'text',
exportable: true,
},
status: {
name: 'warehouse_transfer.field.status',
fieldType: 'enumeration',
options: [
{ key: 'draft', label: 'Draft' },
{ key: 'in-transit', label: 'In Transit' },
{ key: 'transferred', label: 'Transferred' },
],
sortable: false,
},
created_at: {
name: 'warehouse_transfer.field.created_at',
column: 'created_at',
columnType: 'date',
fieldType: 'date',
},
},
fields: {
date: {
name: 'warehouse_transfer.field.date',

View File

@@ -1,4 +1,5 @@
import 'reflect-metadata'; // We need this in order to use @Decorators
import 'newrelic';
import './before';
import '@/config';

View File

@@ -0,0 +1,31 @@
import { Inject, Service } from 'typedi';
import { AccountsApplication } from './AccountsApplication';
import { Exportable } from '../Export/Exportable';
import { IAccountsFilter, IAccountsStructureType } from '@/interfaces';
@Service()
export class AccountsExportable extends Exportable {
@Inject()
private accountsApplication: AccountsApplication;
/**
* Retrieves the accounts data to exportable sheet.
* @param {number} tenantId
* @returns
*/
public exportable(tenantId: number, query: IAccountsFilter) {
const parsedQuery = {
sortOrder: 'desc',
columnSortBy: 'created_at',
inactiveMode: false,
...query,
structure: IAccountsStructureType.Flat,
pageSize: 12000,
page: 1,
} as IAccountsFilter;
return this.accountsApplication
.getAccounts(tenantId, parsedQuery)
.then((output) => output.accounts);
}
}

View File

@@ -1,4 +1,4 @@
import { Service, Inject, Container } from 'typedi';
import { Service, Inject } from 'typedi';
import {
IRegisterDTO,
ISystemUser,
@@ -9,6 +9,9 @@ import { AuthSigninService } from './AuthSignin';
import { AuthSignupService } from './AuthSignup';
import { AuthSendResetPassword } from './AuthSendResetPassword';
import { GetAuthMeta } from './GetAuthMeta';
import { AuthSignupConfirmService } from './AuthSignupConfirm';
import { SystemUser } from '@/system/models';
import { AuthSignupConfirmResend } from './AuthSignupResend';
@Service()
export default class AuthenticationApplication {
@@ -18,6 +21,12 @@ export default class AuthenticationApplication {
@Inject()
private authSignupService: AuthSignupService;
@Inject()
private authSignupConfirmService: AuthSignupConfirmService;
@Inject()
private authSignUpConfirmResendService: AuthSignupConfirmResend;
@Inject()
private authResetPasswordService: AuthSendResetPassword;
@@ -44,6 +53,28 @@ export default class AuthenticationApplication {
return this.authSignupService.signUp(signupDTO);
}
/**
* Verfying the provided user's email after signin-up.
* @param {string} email
* @param {string} token
* @returns {Promise<SystemUser>}
*/
public async signUpConfirm(
email: string,
token: string
): Promise<SystemUser> {
return this.authSignupConfirmService.signUpConfirm(email, token);
}
/**
* Resends the confirmation email of the given system user.
* @param {number} userId - System user id.
* @returns {Promise<void>}
*/
public async signUpConfirmResend(userId: number) {
return this.authSignUpConfirmResendService.signUpConfirmResend(userId);
}
/**
* Generates and retrieve password reset token for the given user email.
* @param {string} email

View File

@@ -1,5 +1,6 @@
import { isEmpty, omit } from 'lodash';
import { defaultTo, isEmpty, omit } from 'lodash';
import moment from 'moment';
import crypto from 'crypto';
import { ServiceError } from '@/exceptions';
import {
IAuthSignedUpEventPayload,
@@ -42,6 +43,13 @@ export class AuthSignupService {
const hashedPassword = await hashPassword(signupDTO.password);
const verifyTokenCrypto = crypto.randomBytes(64).toString('hex');
const verifiedEnabed = defaultTo(config.signupConfirmation.enabled, false);
const verifyToken = verifiedEnabed ? verifyTokenCrypto : '';
const verified = !verifiedEnabed;
const inviteAcceptedAt = moment().format('YYYY-MM-DD');
// Triggers signin up event.
await this.eventPublisher.emitAsync(events.auth.signingUp, {
signupDTO,
@@ -50,10 +58,12 @@ export class AuthSignupService {
const tenant = await this.tenantsManager.createTenant();
const registeredUser = await systemUserRepository.create({
...omit(signupDTO, 'country'),
verifyToken,
verified,
active: true,
password: hashedPassword,
tenantId: tenant.id,
inviteAcceptedAt: moment().format('YYYY-MM-DD'),
inviteAcceptedAt,
});
// Triggers signed up event.
await this.eventPublisher.emitAsync(events.auth.signUp, {

View File

@@ -0,0 +1,57 @@
import { Inject, Service } from 'typedi';
import { ServiceError } from '@/exceptions';
import { SystemUser } from '@/system/models';
import { ERRORS } from './_constants';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import {
IAuthSignUpVerifiedEventPayload,
IAuthSignUpVerifingEventPayload,
} from '@/interfaces';
@Service()
export class AuthSignupConfirmService {
@Inject()
private eventPublisher: EventPublisher;
/**
* Verifies the provided user's email after signing-up.
* @throws {ServiceErrors}
* @param {IRegisterDTO} signupDTO
* @returns {Promise<ISystemUser>}
*/
public async signUpConfirm(
email: string,
verifyToken: string
): Promise<SystemUser> {
const foundUser = await SystemUser.query().findOne({ email, verifyToken });
if (!foundUser) {
throw new ServiceError(ERRORS.SIGNUP_CONFIRM_TOKEN_INVALID);
}
const userId = foundUser.id;
// Triggers `signUpConfirming` event.
await this.eventPublisher.emitAsync(events.auth.signUpConfirming, {
email,
verifyToken,
userId,
} as IAuthSignUpVerifingEventPayload);
const updatedUser = await SystemUser.query().patchAndFetchById(
foundUser.id,
{
verified: true,
verifyToken: '',
}
);
// Triggers `signUpConfirmed` event.
await this.eventPublisher.emitAsync(events.auth.signUpConfirmed, {
email,
verifyToken,
userId,
} as IAuthSignUpVerifiedEventPayload);
return updatedUser as SystemUser;
}
}

View File

@@ -0,0 +1,34 @@
import { Inject, Service } from 'typedi';
import { ServiceError } from '@/exceptions';
import { SystemUser } from '@/system/models';
import { ERRORS } from './_constants';
@Service()
export class AuthSignupConfirmResend {
@Inject('agenda')
private agenda: any;
/**
* Resends the email confirmation of the given user.
* @param {number} userId - User ID.
* @returns {Promise<void>}
*/
public async signUpConfirmResend(userId: number) {
const user = await SystemUser.query().findById(userId).throwIfNotFound();
// Throw error if the user is already verified.
if (user.verified) {
throw new ServiceError(ERRORS.USER_ALREADY_VERIFIED);
}
// Throw error if the verification token is not exist.
if (!user.verifyToken) {
throw new ServiceError(ERRORS.USER_ALREADY_VERIFIED);
}
const payload = {
email: user.email,
token: user.verifyToken,
fullName: user.firstName,
};
await this.agenda.now('send-signup-verify-mail', payload);
}
}

View File

@@ -33,4 +33,33 @@ export default class AuthenticationMailMesssages {
})
.send();
}
/**
* Sends signup verification mail.
* @param {string} email - Email address
* @param {string} fullName - User name.
* @param {string} token - Verification token.
* @returns {Promise<void>}
*/
public async sendSignupVerificationMail(
email: string,
fullName: string,
token: string
) {
const verifyUrl = `${config.baseURL}/auth/email_confirmation?token=${token}&email=${email}`;
await new Mail()
.setSubject('Bigcapital - Verify your email')
.setView('mail/SignupVerifyEmail.html')
.setTo(email)
.setAttachments([
{
filename: 'bigcapital.png',
path: `${global.__views_dir}/images/bigcapital.png`,
cid: 'bigcapital_logo',
},
])
.setData({ verifyUrl, fullName })
.send();
}
}

View File

@@ -9,4 +9,6 @@ export const ERRORS = {
EMAIL_EXISTS: 'EMAIL_EXISTS',
SIGNUP_RESTRICTED_NOT_ALLOWED: 'SIGNUP_RESTRICTED_NOT_ALLOWED',
SIGNUP_RESTRICTED: 'SIGNUP_RESTRICTED',
SIGNUP_CONFIRM_TOKEN_INVALID: 'SIGNUP_CONFIRM_TOKEN_INVALID',
USER_ALREADY_VERIFIED: 'USER_ALREADY_VERIFIED',
};

View File

@@ -0,0 +1,30 @@
import { IAuthSignedUpEventPayload } from '@/interfaces';
import events from '@/subscribers/events';
import { Inject } from 'typedi';
export class SendVerfiyMailOnSignUp {
@Inject('agenda')
private agenda: any;
/**
* Attaches events with handles.
*/
public attach(bus) {
bus.subscribe(events.auth.signUp, this.handleSendVerifyMailOnSignup);
}
/**
*
* @param {ITaxRateEditedPayload} payload -
*/
private handleSendVerifyMailOnSignup = async ({
user,
}: IAuthSignedUpEventPayload) => {
const payload = {
email: user.email,
token: user.verifyToken,
fullName: user.firstName,
};
await this.agenda.now('send-signup-verify-mail', payload);
};
}

View File

@@ -0,0 +1,35 @@
import { Container } from 'typedi';
import AuthenticationMailMesssages from '@/services/Authentication/AuthenticationMailMessages';
export class SendVerifyMailJob {
/**
* Constructor method.
* @param {Agenda} agenda
*/
constructor(agenda) {
agenda.define(
'send-signup-verify-mail',
{ priority: 'high' },
this.handler.bind(this)
);
}
/**
* Handle send welcome mail job.
* @param {Job} job
* @param {Function} done
*/
public async handler(job, done: Function): Promise<void> {
const { data } = job.attrs;
const { email, fullName, token } = data;
const authService = Container.get(AuthenticationMailMesssages);
try {
await authService.sendSignupVerificationMail(email, fullName, token);
done();
} catch (error) {
console.log(error);
done(error);
}
}
}

View File

@@ -0,0 +1,29 @@
import { Inject, Service } from 'typedi';
import { IItemsFilter } from '@/interfaces';
import { CustomersApplication } from './CustomersApplication';
import { Exportable } from '@/services/Export/Exportable';
@Service()
export class CustomersExportable extends Exportable {
@Inject()
private customersApplication: CustomersApplication;
/**
* Retrieves the accounts data to exportable sheet.
* @param {number} tenantId
* @returns
*/
public exportable(tenantId: number, query: IItemsFilter) {
const parsedQuery = {
sortOrder: 'DESC',
columnSortBy: 'created_at',
page: 1,
...query,
pageSize: 12,
} as IItemsFilter;
return this.customersApplication
.getCustomers(tenantId, parsedQuery)
.then((output) => output.customers);
}
}

View File

@@ -0,0 +1,29 @@
import { Inject, Service } from 'typedi';
import { IItemsFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable';
import { VendorsApplication } from './VendorsApplication';
@Service()
export class VendorsExportable extends Exportable {
@Inject()
private vendorsApplication: VendorsApplication;
/**
* Retrieves the accounts data to exportable sheet.
* @param {number} tenantId
* @returns
*/
public exportable(tenantId: number, query: IItemsFilter) {
const parsedQuery = {
sortOrder: 'DESC',
columnSortBy: 'created_at',
page: 1,
...query,
pageSize: 12,
} as IItemsFilter;
return this.vendorsApplication
.getVendors(tenantId, parsedQuery)
.then((output) => output.vendors);
}
}

View File

@@ -0,0 +1,30 @@
import { Inject, Service } from 'typedi';
import { ICreditNotesQueryDTO } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable';
import ListCreditNotes from './ListCreditNotes';
@Service()
export class CreditNotesExportable extends Exportable {
@Inject()
private getCreditNotes: ListCreditNotes;
/**
* Retrieves the accounts data to exportable sheet.
* @param {number} tenantId -
* @param {IVendorCreditsQueryDTO} query -
* @returns {}
*/
public exportable(tenantId: number, query: ICreditNotesQueryDTO) {
const parsedQuery = {
sortOrder: 'desc',
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: 12000,
} as ICreditNotesQueryDTO;
return this.getCreditNotes
.getCreditNotesList(tenantId, parsedQuery)
.then((output) => output.creditNotes);
}
}

View File

@@ -45,7 +45,7 @@ export default class ListCreditNotes extends BaseCreditNotes {
);
const { results, pagination } = await CreditNote.query()
.onBuild((builder) => {
builder.withGraphFetched('entries');
builder.withGraphFetched('entries.item');
builder.withGraphFetched('customer');
dynamicFilter.buildQuery()(builder);
})

View File

@@ -0,0 +1,29 @@
import { Inject, Service } from 'typedi';
import { Exportable } from '../Export/Exportable';
import { IExpensesFilter } from '@/interfaces';
import { ExpensesApplication } from './ExpensesApplication';
@Service()
export class ExpensesExportable extends Exportable {
@Inject()
private expensesApplication: ExpensesApplication;
/**
* Retrieves the accounts data to exportable sheet.
* @param {number} tenantId
* @returns
*/
public exportable(tenantId: number, query: IExpensesFilter) {
const parsedQuery = {
sortOrder: 'desc',
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: 12000,
} as IExpensesFilter;
return this.expensesApplication
.getExpenses(tenantId, parsedQuery)
.then((output) => output.expenses);
}
}

View File

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

View File

@@ -0,0 +1,49 @@
import { camelCase, upperFirst } from 'lodash';
import { Exportable } from './Exportable';
export class ExportableRegistry {
private static instance: ExportableRegistry;
private exportables: Record<string, Exportable>;
/**
* Constructor method.
*/
constructor() {
this.exportables = {};
}
/**
* Gets singleton instance of registry.
* @returns {ExportableRegistry}
*/
public static getInstance(): ExportableRegistry {
if (!ExportableRegistry.instance) {
ExportableRegistry.instance = new ExportableRegistry();
}
return ExportableRegistry.instance;
}
/**
* Registers the given importable service.
* @param {string} resource
* @param {Exportable} importable
*/
public registerExportable(resource: string, importable: Exportable): void {
const _resource = this.sanitizeResourceName(resource);
this.exportables[_resource] = importable;
}
/**
* Retrieves the importable service instance of the given resource name.
* @param {string} name
* @returns {Exportable}
*/
public getExportable(name: string): Exportable {
const _name = this.sanitizeResourceName(name);
return this.exportables[_name];
}
private sanitizeResourceName(resource: string) {
return upperFirst(camelCase(resource));
}
}

View File

@@ -0,0 +1,72 @@
import Container, { Service } from 'typedi';
import { AccountsExportable } from '../Accounts/AccountsExportable';
import { ExportableRegistry } from './ExportRegistery';
import { ItemsExportable } from '../Items/ItemsExportable';
import { CustomersExportable } from '../Contacts/Customers/CustomersExportable';
import { VendorsExportable } from '../Contacts/Vendors/VendorsExportable';
import { ExpensesExportable } from '../Expenses/ExpensesExportable';
import { SaleInvoicesExportable } from '../Sales/Invoices/SaleInvoicesExportable';
import { SaleEstimatesExportable } from '../Sales/Estimates/SaleEstimatesExportable';
import { SaleReceiptsExportable } from '../Sales/Receipts/SaleReceiptsExportable';
import { BillsExportable } from '../Purchases/Bills/BillsExportable';
import { PaymentsReceivedExportable } from '../Sales/PaymentReceives/PaymentsReceivedExportable';
import { BillPaymentExportable } from '../Purchases/BillPayments/BillPaymentExportable';
import { ManualJournalsExportable } from '../ManualJournals/ManualJournalExportable';
import { CreditNotesExportable } from '../CreditNotes/CreditNotesExportable';
import { VendorCreditsExportable } from '../Purchases/VendorCredits/VendorCreditsExportable';
import { ItemCategoriesExportable } from '../ItemCategories/ItemCategoriesExportable';
@Service()
export class ExportableResources {
private static registry: ExportableRegistry;
/**
* Consttuctor method.
*/
constructor() {
this.boot();
}
/**
* Importable instances.
*/
private importables = [
{ resource: 'Account', exportable: AccountsExportable },
{ resource: 'Item', exportable: ItemsExportable },
{ resource: 'ItemCategory', exportable: ItemCategoriesExportable },
{ resource: 'Customer', exportable: CustomersExportable },
{ resource: 'Vendor', exportable: VendorsExportable },
{ resource: 'Expense', exportable: ExpensesExportable },
{ resource: 'SaleInvoice', exportable: SaleInvoicesExportable },
{ resource: 'SaleEstimate', exportable: SaleEstimatesExportable },
{ resource: 'SaleReceipt', exportable: SaleReceiptsExportable },
{ resource: 'Bill', exportable: BillsExportable },
{ resource: 'PaymentReceive', exportable: PaymentsReceivedExportable },
{ resource: 'BillPayment', exportable: BillPaymentExportable },
{ resource: 'ManualJournal', exportable: ManualJournalsExportable },
{ resource: 'CreditNote', exportable: CreditNotesExportable },
{ resource: 'VendorCredit', exportable: VendorCreditsExportable },
];
/**
*
*/
public get registry() {
return ExportableResources.registry;
}
/**
* Boots all the registered importables.
*/
public boot() {
if (!ExportableResources.registry) {
const instance = ExportableRegistry.getInstance();
this.importables.forEach((importable) => {
const importableInstance = Container.get(importable.exportable);
instance.registerExportable(importable.resource, importableInstance);
});
ExportableResources.registry = instance;
}
}
}

View File

@@ -0,0 +1,161 @@
import { Inject, Service } from 'typedi';
import xlsx from 'xlsx';
import * as R from 'ramda';
import { get } from 'lodash';
import { sanitizeResourceName } from '../Import/_utils';
import ResourceService from '../Resource/ResourceService';
import { ExportableResources } from './ExportResources';
import { ServiceError } from '@/exceptions';
import { Errors } from './common';
import { IModelMeta, IModelMetaColumn } from '@/interfaces';
import { flatDataCollections, getDataAccessor } from './utils';
@Service()
export class ExportResourceService {
@Inject()
private resourceService: ResourceService;
@Inject()
private exportableResources: ExportableResources;
/**
* 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.
*/
public async export(tenantId: number, resourceName: string, format: string = 'csv') {
const resource = sanitizeResourceName(resourceName);
const resourceMeta = this.getResourceMeta(tenantId, resource);
this.validateResourceMeta(resourceMeta);
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);
}
/**
* Retrieves metadata for a specific resource.
* @param {number} tenantId - The tenant identifier.
* @param {string} resource - The name of the resource.
* @returns The metadata of the resource.
*/
private getResourceMeta(tenantId: number, resource: string) {
return this.resourceService.getResourceMeta(tenantId, resource);
}
/**
* Validates if the resource metadata is exportable.
* @param {any} resourceMeta - The metadata of the resource.
* @throws {ServiceError} If the resource is not exportable or lacks columns.
*/
private validateResourceMeta(resourceMeta: any) {
if (!resourceMeta.exportable || !resourceMeta.columns) {
throw new ServiceError(Errors.RESOURCE_NOT_EXPORTABLE);
}
}
/**
* Transforms the exported data based on the resource metadata.
* If the resource metadata specifies a flattening attribute (`exportFlattenOn`),
* the data will be flattened based on this attribute using the `flatDataCollections` utility function.
*
* @param {number} tenantId - The tenant identifier.
* @param {string} resource - The name of the resource.
* @param {Array<Record<string, any>>} data - The original data to be transformed.
* @returns {Array<Record<string, any>>} - The transformed data.
*/
private transformExportedData(
tenantId: number,
resource: string,
data: Array<Record<string, any>>
): Array<Record<string, any>> {
const resourceMeta = this.getResourceMeta(tenantId, resource);
return R.when<Array<Record<string, any>>, Array<Record<string, any>>>(
R.always(Boolean(resourceMeta.exportFlattenOn)),
(data) => flatDataCollections(data, resourceMeta.exportFlattenOn),
data
);
}
/**
* Fetches exportable data for a given resource.
* @param {number} tenantId - The tenant identifier.
* @param {string} resource - The name of the resource.
* @returns A promise that resolves to the exportable data.
*/
private async getExportableData(tenantId: number, resource: string) {
const exportable =
this.exportableResources.registry.getExportable(resource);
return exportable.exportable(tenantId, {});
}
/**
* Extracts columns that are marked as exportable from the resource metadata.
* @param {IModelMeta} resourceMeta - The metadata of the resource.
* @returns An array of exportable columns.
*/
private getExportableColumns(resourceMeta: IModelMeta) {
const processColumns = (
columns: { [key: string]: IModelMetaColumn },
parent = ''
) => {
return Object.entries(columns)
.filter(([_, value]) => value.exportable !== 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.
* @param {any[]} exportableColumns - The columns to be included in the workbook.
* @returns The created workbook.
*/
private createWorkbook(data: any[], exportableColumns: any[]) {
const workbook = xlsx.utils.book_new();
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);
xlsx.utils.book_append_sheet(workbook, worksheet, 'Exported Data');
return workbook;
}
/**
* Exports the workbook in the specified format.
* @param {any} workbook - The workbook to be exported.
* @param {string} format - The format to export the workbook in.
* @returns The exported workbook data.
*/
private exportWorkbook(workbook: any, format: string) {
if (format.toLowerCase() === 'csv') {
return xlsx.write(workbook, { type: 'buffer', bookType: 'csv' });
} else if (format.toLowerCase() === 'xlsx') {
return xlsx.write(workbook, { type: 'buffer', bookType: 'xlsx' });
}
}
}

View File

@@ -0,0 +1,22 @@
export class Exportable {
/**
*
* @param tenantId
* @returns
*/
public async exportable(
tenantId: number,
query: Record<string, any>
): Promise<Array<Record<string, any>>> {
return [];
}
/**
*
* @param data
* @returns
*/
public transform(data: Record<string, any>) {
return data;
}
}

View File

@@ -0,0 +1,3 @@
export enum Errors {
RESOURCE_NOT_EXPORTABLE = 'RESOURCE_NOT_EXPORTABLE',
}

View File

@@ -0,0 +1,27 @@
import { flatMap } from 'lodash';
/**
* Flattens the data based on a specified attribute.
* @param data - The data to be flattened.
* @param flattenAttr - The attribute to be flattened.
* @returns - The flattened data.
*/
export const flatDataCollections = (
data: Record<string, any>,
flattenAttr: string
): Record<string, any>[] => {
return flatMap(data, (item) =>
item[flattenAttr].map((entry) => ({
...item,
[flattenAttr]: entry,
}))
);
};
/**
* Gets the data accessor for a given column.
* @param col - The column to get the data accessor for.
* @returns - The data accessor.
*/
export const getDataAccessor = (col: any) => {
return col.group ? `${col.group}.${col.accessor}` : col.accessor;
};

View File

@@ -0,0 +1,29 @@
import { Inject, Service } from 'typedi';
import { Exportable } from '../Export/Exportable';
import { IAccountsFilter, IAccountsStructureType } from '@/interfaces';
import ItemCategoriesService from './ItemCategoriesService';
@Service()
export class ItemCategoriesExportable extends Exportable {
@Inject()
private itemCategoriesApplication: ItemCategoriesService;
/**
* Retrieves the accounts data to exportable sheet.
* @param {number} tenantId
* @returns
*/
public exportable(tenantId: number, query: IAccountsFilter) {
const parsedQuery = {
sortOrder: 'desc',
columnSortBy: 'created_at',
inactiveMode: false,
...query,
structure: IAccountsStructureType.Flat,
} as IAccountsFilter;
return this.itemCategoriesApplication
.getItemCategoriesList(tenantId, parsedQuery, {})
.then((output) => output.itemCategories);
}
}

View File

@@ -0,0 +1,29 @@
import { Inject, Service } from 'typedi';
import { Exportable } from '../Export/Exportable';
import { IItemsFilter } from '@/interfaces';
import { ItemsApplication } from './ItemsApplication';
@Service()
export class ItemsExportable extends Exportable {
@Inject()
private itemsApplication: ItemsApplication;
/**
* Retrieves the accounts data to exportable sheet.
* @param {number} tenantId
* @returns
*/
public exportable(tenantId: number, query: IItemsFilter) {
const parsedQuery = {
sortOrder: 'DESC',
columnSortBy: 'created_at',
page: 1,
...query,
pageSize: 12,
} as IItemsFilter;
return this.itemsApplication
.getItems(tenantId, parsedQuery)
.then((output) => output.items);
}
}

View File

@@ -1,4 +1,4 @@
import { difference } from 'lodash';
import { difference, isEmpty } from 'lodash';
import { Service, Inject } from 'typedi';
import { ServiceError } from '@/exceptions';
import {
@@ -244,16 +244,12 @@ export class CommandManualJournalValidators {
/**
* Validates the manual journal number require.
* @param {string} journalNumber
* @throws {ServiceError(ERRORS.MANUAL_JOURNAL_NO_REQUIRED)}
*/
public validateJournalNoRequireWhenAutoNotEnabled = (
tenantId: number,
journalNumber: string
) => {
// Retrieve the next manual journal number.
const autoIncrmenetEnabled =
this.autoIncrement.autoIncrementEnabled(tenantId);
if (!journalNumber || !autoIncrmenetEnabled) {
if (isEmpty(journalNumber)) {
throw new ServiceError(ERRORS.MANUAL_JOURNAL_NO_REQUIRED);
}
};

View File

@@ -99,7 +99,6 @@ export class CreateManualJournalService {
// Validate manual journal number require when auto-increment not enabled.
this.validator.validateJournalNoRequireWhenAutoNotEnabled(
tenantId,
manualJournalDTO.journalNumber
);
// Validate manual journal uniquiness on the storage.

View File

@@ -39,7 +39,7 @@ export class GetManualJournals {
tenantId: number,
filterDTO: IManualJournalsFilter
): Promise<{
manualJournals: IManualJournal;
manualJournals: IManualJournal[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> => {

View File

@@ -0,0 +1,29 @@
import { Inject, Service } from 'typedi';
import { IManualJournalsFilter } from '@/interfaces';
import { Exportable } from '../Export/Exportable';
import { ManualJournalsApplication } from './ManualJournalsApplication';
@Service()
export class ManualJournalsExportable extends Exportable {
@Inject()
private manualJournalsApplication: ManualJournalsApplication;
/**
* Retrieves the manual journals data to exportable sheet.
* @param {number} tenantId
* @returns
*/
public exportable(tenantId: number, query: IManualJournalsFilter) {
const parsedQuery = {
sortOrder: 'desc',
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: 12000,
} as IManualJournalsFilter;
return this.manualJournalsApplication
.getManualJournals(tenantId, parsedQuery)
.then((output) => output.manualJournals);
}
}

View File

@@ -0,0 +1,28 @@
import { Inject, Service } from 'typedi';
import { Exportable } from '@/services/Export/Exportable';
import { BillPaymentsApplication } from './BillPaymentsApplication';
@Service()
export class BillPaymentExportable extends Exportable {
@Inject()
private billPaymentsApplication: BillPaymentsApplication;
/**
* Retrieves the accounts data to exportable sheet.
* @param {number} tenantId
* @returns
*/
public exportable(tenantId: number, query: any) {
const parsedQuery = {
page: 1,
pageSize: 12,
...query,
sortOrder: 'desc',
columnSortBy: 'created_at',
} as any;
return this.billPaymentsApplication
.getBillPayments(tenantId, parsedQuery)
.then((output) => output.billPayments);
}
}

View File

@@ -31,7 +31,7 @@ export class GetBillPayments {
tenantId: number,
filterDTO: IBillPaymentsFilter
): Promise<{
billPayments: IBillPayment;
billPayments: IBillPayment[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {

View File

@@ -99,7 +99,7 @@ export class BillsApplication {
tenantId: number,
filterDTO: IBillsFilter
): Promise<{
bills: IBill;
bills: IBill[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {

View File

@@ -0,0 +1,29 @@
import { Inject, Service } from 'typedi';
import { IBillsFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable';
import { BillsApplication } from './BillsApplication';
@Service()
export class BillsExportable extends Exportable {
@Inject()
private billsApplication: BillsApplication;
/**
* Retrieves the accounts data to exportable sheet.
* @param {number} tenantId
* @returns
*/
public exportable(tenantId: number, query: IBillsFilter) {
const parsedQuery = {
sortOrder: 'desc',
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: 12000,
} as IBillsFilter;
return this.billsApplication
.getBills(tenantId, parsedQuery)
.then((output) => output.bills);
}
}

View File

@@ -49,6 +49,7 @@ export class GetBills {
const { results, pagination } = await Bill.query()
.onBuild((builder) => {
builder.withGraphFetched('vendor');
builder.withGraphFetched('entries.item');
dynamicFilter.buildQuery()(builder);
})
.pagination(filter.page - 1, filter.pageSize);

View File

@@ -14,6 +14,7 @@ export class VendorCreditTransformer extends Transformer {
'formattedSubtotal',
'formattedVendorCreditDate',
'formattedCreditsRemaining',
'formattedInvoicedAmount',
'entries',
];
};
@@ -58,6 +59,17 @@ export class VendorCreditTransformer extends Transformer {
});
};
/**
* Retrieves the formatted invoiced amount.
* @param credit
* @returns {string}
*/
protected formattedInvoicedAmount = (credit) => {
return formatNumber(credit.invoicedAmount, {
currencyCode: credit.currencyCode,
});
};
/**
* Retrieves the entries of the bill.
* @param {IVendorCredit} vendorCredit

View File

@@ -0,0 +1,30 @@
import { Inject, Service } from 'typedi';
import { IVendorCreditsQueryDTO } from '@/interfaces';
import ListVendorCredits from './ListVendorCredits';
import { Exportable } from '@/services/Export/Exportable';
@Service()
export class VendorCreditsExportable extends Exportable {
@Inject()
private getVendorCredits: ListVendorCredits;
/**
* Retrieves the vendor credits data to exportable sheet.
* @param {number} tenantId -
* @param {IVendorCreditsQueryDTO} query -
* @returns {}
*/
public exportable(tenantId: number, query: IVendorCreditsQueryDTO) {
const parsedQuery = {
sortOrder: 'desc',
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: 12000,
} as IVendorCreditsQueryDTO;
return this.getVendorCredits
.getVendorCredits(tenantId, parsedQuery)
.then((output) => output.vendorCredits);
}
}

View File

@@ -105,7 +105,11 @@ export default class ResourceService {
const $enumerationType = (field) =>
field.fieldType === 'enumeration' ? field : undefined;
const $hasFields = (field) => 'undefined' !== typeof field.fields ? field : undefined;
const $hasFields = (field) =>
'undefined' !== typeof field.fields ? field : undefined;
const $hasColumns = (column) =>
'undefined' !== typeof column.columns ? column : undefined;
const naviagations = [
['fields', qim.$each, 'name'],
@@ -113,6 +117,8 @@ export default class ResourceService {
['fields2', qim.$each, 'name'],
['fields2', qim.$each, $enumerationType, 'options', qim.$each, 'label'],
['fields2', qim.$each, $hasFields, 'fields', qim.$each, 'name'],
['columns', qim.$each, 'name'],
['columns', qim.$each, $hasColumns, 'columns', qim.$each, 'name'],
];
return this.i18nService.i18nApply(naviagations, meta, tenantId);
}

View File

@@ -15,10 +15,8 @@ export default class AutoIncrementOrdersService {
const group = settingsGroup;
// Settings service transaction number and prefix.
const autoIncrement = settings.get({ group, key: 'auto_increment' }, false);
return parseBoolean(autoIncrement, false);
}
return settings.get({ group, key: 'auto_increment' }, false);
};
/**
* Retrieve the next service transaction number.
@@ -27,17 +25,16 @@ export default class AutoIncrementOrdersService {
* @param {Function} getMaxTransactionNo
* @return {Promise<string>}
*/
getNextTransactionNumber(tenantId: number, settingsGroup: string): string {
getNextTransactionNumber(tenantId: number, group: string): string {
const settings = this.tenancy.settings(tenantId);
const group = settingsGroup;
// Settings service transaction number and prefix.
const autoIncrement = settings.get({ group, key: 'auto_increment' }, false);
const autoIncrement = this.autoIncrementEnabled(tenantId, group);
const settingNo = settings.get({ group, key: 'next_number' }, '');
const settingPrefix = settings.get({ group, key: 'number_prefix' }, '');
return parseBoolean(autoIncrement, false) ? `${settingPrefix}${settingNo}` : '';
return autoIncrement ? `${settingPrefix}${settingNo}` : '';
}
/**
@@ -53,7 +50,9 @@ export default class AutoIncrementOrdersService {
const autoIncrement = settings.get({ group, key: 'auto_increment' });
// Can't continue if the auto-increment of the service was disabled.
if (!autoIncrement) { return; }
if (!autoIncrement) {
return;
}
settings.set(
{ group, key: 'next_number' },

View File

@@ -51,6 +51,7 @@ export class GetSaleEstimates {
.onBuild((builder) => {
builder.withGraphFetched('customer');
builder.withGraphFetched('entries');
builder.withGraphFetched('entries.item');
dynamicFilter.buildQuery()(builder);
})
.pagination(filter.page - 1, filter.pageSize);

View File

@@ -0,0 +1,29 @@
import { Inject, Service } from 'typedi';
import { ISalesInvoicesFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable';
import { SaleEstimatesApplication } from './SaleEstimatesApplication';
@Service()
export class SaleEstimatesExportable extends Exportable {
@Inject()
private saleEstimatesApplication: SaleEstimatesApplication;
/**
* Retrieves the accounts data to exportable sheet.
* @param {number} tenantId
* @returns
*/
public exportable(tenantId: number, query: ISalesInvoicesFilter) {
const parsedQuery = {
sortOrder: 'desc',
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: 12000,
} as ISalesInvoicesFilter;
return this.saleEstimatesApplication
.getSaleEstimates(tenantId, parsedQuery)
.then((output) => output.salesEstimates);
}
}

View File

@@ -49,7 +49,7 @@ export class GetSaleInvoices {
);
const { results, pagination } = await SaleInvoice.query()
.onBuild((builder) => {
builder.withGraphFetched('entries');
builder.withGraphFetched('entries.item');
builder.withGraphFetched('customer');
dynamicFilter.buildQuery()(builder);
})

View File

@@ -0,0 +1,29 @@
import { Inject, Service } from 'typedi';
import { ISalesInvoicesFilter } from '@/interfaces';
import { SaleInvoiceApplication } from './SaleInvoicesApplication';
import { Exportable } from '@/services/Export/Exportable';
@Service()
export class SaleInvoicesExportable extends Exportable {
@Inject()
private saleInvoicesApplication: SaleInvoiceApplication;
/**
* Retrieves the accounts data to exportable sheet.
* @param {number} tenantId
* @returns
*/
public exportable(tenantId: number, query: ISalesInvoicesFilter) {
const parsedQuery = {
sortOrder: 'desc',
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: 120000,
} as ISalesInvoicesFilter;
return this.saleInvoicesApplication
.getSaleInvoices(tenantId, parsedQuery)
.then((output) => output.salesInvoices);
}
}

View File

@@ -0,0 +1,30 @@
import { Inject, Service } from 'typedi';
import { IAccountsStructureType, IPaymentReceivesFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable';
import { PaymentReceivesApplication } from './PaymentReceivesApplication';
@Service()
export class PaymentsReceivedExportable extends Exportable {
@Inject()
private paymentReceivedApp: PaymentReceivesApplication;
/**
* Retrieves the accounts data to exportable sheet.
* @param {number} tenantId
* @param {IPaymentReceivesFilter} query -
* @returns
*/
public exportable(tenantId: number, query: IPaymentReceivesFilter) {
const parsedQuery = {
sortOrder: 'desc',
columnSortBy: 'created_at',
inactiveMode: false,
...query,
structure: IAccountsStructureType.Flat,
} as IPaymentReceivesFilter;
return this.paymentReceivedApp
.getPaymentReceives(tenantId, parsedQuery)
.then((output) => output.paymentReceives);
}
}

View File

@@ -11,6 +11,9 @@ import { SaleReceiptTransformer } from './SaleReceiptTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
interface GetSaleReceiptsSettings {
fetchEntriesGraph?: boolean;
}
@Service()
export class GetSaleReceipts {
@Inject()
@@ -50,7 +53,7 @@ export class GetSaleReceipts {
.onBuild((builder) => {
builder.withGraphFetched('depositAccount');
builder.withGraphFetched('customer');
builder.withGraphFetched('entries');
builder.withGraphFetched('entries.item');
dynamicFilter.buildQuery()(builder);
})

View File

@@ -0,0 +1,29 @@
import { Inject, Service } from 'typedi';
import { ISalesReceiptsFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable';
import { SaleReceiptApplication } from './SaleReceiptApplication';
@Service()
export class SaleReceiptsExportable extends Exportable {
@Inject()
private saleReceiptsApp: SaleReceiptApplication;
/**
* Retrieves the accounts data to exportable sheet.
* @param {number} tenantId
* @returns
*/
public exportable(tenantId: number, query: ISalesReceiptsFilter) {
const parsedQuery = {
sortOrder: 'desc',
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: 12,
} as ISalesReceiptsFilter;
return this.saleReceiptsApp
.getSaleReceipts(tenantId, parsedQuery)
.then((output) => output.data);
}
}

View File

@@ -9,6 +9,9 @@ export default {
signUp: 'onSignUp',
signingUp: 'onSigningUp',
signUpConfirming: 'signUpConfirming',
signUpConfirmed: 'signUpConfirmed',
sendingResetPassword: 'onSendingResetPassword',
sendResetPassword: 'onSendResetPassword',

View File

@@ -0,0 +1,12 @@
exports.up = function (knex) {
return knex.schema
.table('users', (table) => {
table.string('verify_token');
table.boolean('verified').defaultTo(false);
})
.then(() => {
return knex('USERS').update({ verified: true });
});
};
exports.down = (knex) => {};

View File

@@ -4,6 +4,12 @@ import SystemModel from '@/system/models/SystemModel';
import SoftDeleteQueryBuilder from '@/collection/SoftDeleteQueryBuilder';
export default class SystemUser extends SystemModel {
firstName!: string;
lastName!: string;
verified!: boolean;
inviteAcceptedAt!: Date | null;
deletedAt!: Date | null;
/**
* Table name.
*/
@@ -29,23 +35,33 @@ export default class SystemUser extends SystemModel {
* Virtual attributes.
*/
static get virtualAttributes() {
return ['fullName', 'isDeleted', 'isInviteAccepted'];
return ['fullName', 'isDeleted', 'isInviteAccepted', 'isVerified'];
}
/**
*
* Detarmines whether the user is deleted.
* @returns {boolean}
*/
get isDeleted() {
return !!this.deletedAt;
}
/**
*
* Detarmines whether the sent invite is accepted.
* @returns {boolean}
*/
get isInviteAccepted() {
return !!this.inviteAcceptedAt;
}
/**
* Detarmines whether the user's email is verified.
* @returns {boolean}
*/
get isVerified() {
return !!this.verified;
}
/**
* Full name attribute.
*/

View File

@@ -1,6 +1,6 @@
import bcrypt from 'bcryptjs';
import moment from 'moment';
import _ from 'lodash';
import _, { isEmpty } from 'lodash';
import path from 'path';
import * as R from 'ramda';
@@ -329,7 +329,7 @@ const booleanValuesRepresentingTrue: string[] = ['true', '1'];
const booleanValuesRepresentingFalse: string[] = ['false', '0'];
const normalizeValue = (value: any): string =>
value.toString().trim().toLowerCase();
value?.toString().trim().toLowerCase();
const booleanValues: string[] = [
...booleanValuesRepresentingTrue,
@@ -337,8 +337,11 @@ const booleanValues: string[] = [
].map((value) => normalizeValue(value));
export const parseBoolean = <T>(value: any, defaultValue: T): T | boolean => {
if (typeof value === 'boolean') {
return value; // Retrun early we have nothing to parse.
}
const normalizedValue = normalizeValue(value);
if (booleanValues.indexOf(normalizedValue) === -1) {
if (isEmpty(value) || booleanValues.indexOf(normalizedValue) === -1) {
return defaultValue;
}
return booleanValuesRepresentingTrue.indexOf(normalizedValue) !== -1;

View File

@@ -0,0 +1,424 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Bigcapital | Reset your password</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
/*All the styling goes here*/
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%;
}
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%; }
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%;
}
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block;
margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px;
}
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
margin: 0 auto;
max-width: 580px;
padding: 10px;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%;
}
.wrapper {
box-sizing: border-box;
padding: 20px;
}
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
margin-top: 10px;
text-align: center;
width: 100%;
}
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center;
}
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: bold;
line-height: 1.4;
margin: 0;
margin-bottom: 0;
}
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize;
}
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px;
}
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px;
}
a {
color: #3498db;
text-decoration: underline;
}
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%;
}
.btn > tbody > tr > td {
padding-bottom: 15px; }
.btn table {
width: auto;
}
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center;
}
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize;
}
.btn-primary table td {
background-color: #2d95fd;
}
.btn-primary a {
background-color: #1968F0;
border-color: #1968F0;
color: #ffffff;
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.clear {
clear: both;
}
.mt0 {
margin-top: 0;
}
.mb0 {
margin-bottom: 0;
}
.mb4{
margin-bottom: 4rem;
}
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
}
.powered-by a {
text-decoration: none;
}
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
margin: 20px 0;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
.btn-primary table td:hover {
background-color: #004dd0 !important;
}
.btn-primary a:hover {
background-color: #004dd0 !important;
border-color: #004dd0 !important;
}
}
[data-icon="bigcapital"] path {
fill: #004dd0;
}
[data-icon='bigcapital'] .path-1,
[data-icon='bigcapital'] .path-13 {
fill: #2d95fd;
}
</style>
</head>
<body class="">
<span class="preheader">Verify your email.</span>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<!-- START CENTERED WHITE CONTAINER -->
<table role="presentation" class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p class="align-center">
<img src="cid:bigcapital_logo" />
</p>
<hr />
<p class="align-center">
<h2>Verify your email</h2>
</p>
<p class="mgb-1x">Hi <strong>{{ fullName }}<strong>,</p>
<p class="mgb-2-5x">To continue setting up your Bigcapital account, please verify that this is your email address.</p>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
<tbody>
<tr>
<td align="left">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td> <a href="{{ verifyUrl }}" target="_blank">Verify email address</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<p>If this was a mistake, just ignore this email and nothing will happen.</p>
<p class="email-note">This is an automatically generated email please do not reply to this email.</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- END CENTERED WHITE CONTAINER -->
<!-- START FOOTER -->
<div class="footer">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td class="content-block powered-by">
Powered by <a href="https://Bigcapital.ly">Bigcapital.ly</a>.
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@@ -7,8 +7,8 @@ WORKDIR /app
# Copy application dependency manifests to the container image.
COPY ./package*.json ./
COPY ./pnpm-lock.yaml ./pnpm-lock.yaml
COPY ./pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY ./lerna.json ./lerna.json
COPY ./pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY ./packages/webapp/package*.json ./packages/webapp/
# Install application dependencies
@@ -24,7 +24,7 @@ RUN pnpm install
# Build webapp package
COPY ./packages/webapp /app/packages/webapp
RUN npm run build:webapp
RUN pnpm run build:webapp
FROM nginx

View File

@@ -9,13 +9,24 @@ import 'moment/locale/ar-ly';
import 'moment/locale/es-us';
import AppIntlLoader from './AppIntlLoader';
import PrivateRoute from '@/components/Guards/PrivateRoute';
import { EnsureAuthenticated } from '@/components/Guards/EnsureAuthenticated';
import GlobalErrors from '@/containers/GlobalErrors/GlobalErrors';
import DashboardPrivatePages from '@/components/Dashboard/PrivatePages';
import { Authentication } from '@/containers/Authentication/Authentication';
import LazyLoader from '@/components/LazyLoader';
import { SplashScreen, DashboardThemeProvider } from '../components';
import { queryConfig } from '../hooks/query/base';
import { EnsureUserEmailVerified } from './Guards/EnsureUserEmailVerified';
import { EnsureAuthNotAuthenticated } from './Guards/EnsureAuthNotAuthenticated';
import { EnsureUserEmailNotVerified } from './Guards/EnsureUserEmailNotVerified';
const EmailConfirmation = LazyLoader({
loader: () => import('@/containers/Authentication/EmailConfirmation'),
});
const RegisterVerify = LazyLoader({
loader: () => import('@/containers/Authentication/RegisterVerify'),
});
/**
* App inner.
@@ -26,9 +37,30 @@ function AppInsider({ history }) {
<DashboardThemeProvider>
<Router history={history}>
<Switch>
<Route path={'/auth'} component={Authentication} />
<Route path={'/auth/register/verify'}>
<EnsureAuthenticated>
<EnsureUserEmailNotVerified>
<RegisterVerify />
</EnsureUserEmailNotVerified>
</EnsureAuthenticated>
</Route>
<Route path={'/auth/email_confirmation'}>
<EmailConfirmation />
</Route>
<Route path={'/auth'}>
<EnsureAuthNotAuthenticated>
<Authentication />
</EnsureAuthNotAuthenticated>
</Route>
<Route path={'/'}>
<PrivateRoute component={DashboardPrivatePages} />
<EnsureAuthenticated>
<EnsureUserEmailVerified>
<DashboardPrivatePages />
</EnsureUserEmailVerified>
</EnsureAuthenticated>
</Route>
</Switch>
</Router>

View File

@@ -1,5 +1,5 @@
// @ts-nocheck
import React from 'react';
import React, { useEffect } from 'react';
import {
useAuthenticatedAccount,
useCurrentOrganization,
@@ -116,6 +116,14 @@ export function useApplicationBoot() {
isBooted.current = true;
},
);
// Reset the loading states once the hook unmount.
useEffect(
() => () => {
isAuthUserLoading && !isBooted.current && stopLoading();
isOrgLoading && !isBooted.current && stopLoading();
},
[isAuthUserLoading, isOrgLoading, stopLoading],
);
return {
isLoading: isOrgLoading || isAuthUserLoading,

View File

@@ -51,6 +51,7 @@ import EstimateMailDialog from '@/containers/Sales/Estimates/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';
/**
* Dialogs container.
@@ -148,6 +149,8 @@ export default function DialogsContainer() {
<ReceiptMailDialog dialogName={DialogsName.ReceiptMail} />
<PaymentMailDialog dialogName={DialogsName.PaymentMail} />
<ConnectBankDialog dialogName={DialogsName.ConnectBankCreditCard} />
<ExportDialog dialogName={DialogsName.Export} />
</div>
);
}

View File

@@ -0,0 +1,22 @@
// @ts-nocheck
import React from 'react';
import { Redirect } from 'react-router-dom';
import { useIsAuthenticated } from '@/hooks/state';
interface EnsureAuthNotAuthenticatedProps {
children: React.ReactNode;
redirectTo?: string;
}
export function EnsureAuthNotAuthenticated({
children,
redirectTo = '/',
}: EnsureAuthNotAuthenticatedProps) {
const isAuthenticated = useIsAuthenticated();
return !isAuthenticated ? (
<>{children}</>
) : (
<Redirect to={{ pathname: redirectTo }} />
);
}

View File

@@ -0,0 +1,22 @@
// @ts-nocheck
import React from 'react';
import { Redirect } from 'react-router-dom';
import { useIsAuthenticated } from '@/hooks/state';
interface EnsureAuthenticatedProps {
children: React.ReactNode;
redirectTo?: string;
}
export function EnsureAuthenticated({
children,
redirectTo = '/auth/login',
}: EnsureAuthenticatedProps) {
const isAuthenticated = useIsAuthenticated();
return isAuthenticated ? (
<>{children}</>
) : (
<Redirect to={{ pathname: redirectTo }} />
);
}

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
import { useAuthUserVerified } from '@/hooks/state';
interface EnsureUserEmailNotVerifiedProps {
children: React.ReactNode;
redirectTo?: string;
}
/**
* Higher Order Component to ensure that the user's email is not verified.
* If is verified, redirects to the inner setup page.
*/
export function EnsureUserEmailNotVerified({
children,
redirectTo = '/',
}: EnsureUserEmailNotVerifiedProps) {
const isAuthVerified = useAuthUserVerified();
if (isAuthVerified) {
return <Redirect to={{ pathname: redirectTo }} />;
}
return <>{children}</>;
}

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
import { useAuthUserVerified } from '@/hooks/state';
interface EnsureUserEmailVerifiedProps {
children: React.ReactNode;
redirectTo?: string;
}
/**
* Higher Order Component to ensure that the user's email is verified.
* If not verified, redirects to the email verification page.
*/
export function EnsureUserEmailVerified({
children,
redirectTo = '/auth/register/verify',
}: EnsureUserEmailVerifiedProps) {
const isAuthVerified = useAuthUserVerified();
if (!isAuthVerified) {
return <Redirect to={{ pathname: redirectTo }} />;
}
return <>{children}</>;
}

View File

@@ -1,19 +0,0 @@
// @ts-nocheck
import React from 'react';
import BodyClassName from 'react-body-classname';
import { Redirect } from 'react-router-dom';
import { useIsAuthenticated } from '@/hooks/state';
export default function PrivateRoute({ component: Component, ...rest }) {
const isAuthenticated = useIsAuthenticated();
return (
<BodyClassName className={''}>
{isAuthenticated ? (
<Component />
) : (
<Redirect to={{ pathname: '/auth/login' }} />
)}
</BodyClassName>
);
}

View File

@@ -73,5 +73,6 @@ export enum DialogsName {
CustomerTransactionsPdfPreview = 'CustomerTransactionsPdfPreview',
VendorTransactionsPdfPreview = 'VendorTransactionsPdfPreview',
GeneralLedgerPdfPreview = 'GeneralLedgerPdfPreview',
SalesTaxLiabilitySummaryPdfPreview = 'SalesTaxLiabilitySummaryPdfPreview'
SalesTaxLiabilitySummaryPdfPreview = 'SalesTaxLiabilitySummaryPdfPreview',
Export = 'Export',
}

View File

@@ -18,7 +18,7 @@ import {
Can,
If,
DashboardActionViewsList,
DashboardActionsBar
DashboardActionsBar,
} from '@/components';
import { useRefreshJournals } from '@/hooks/query/manualJournals';
import { useManualJournalsContext } from './ManualJournalsListProvider';
@@ -31,6 +31,7 @@ import withSettingsActions from '@/containers/Settings/withSettingsActions';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { compose } from '@/utils';
import { DialogsName } from '@/constants/dialogs';
/**
* Manual journal actions bar.
@@ -47,6 +48,9 @@ function ManualJournalActionsBar({
// #withSettingsActions
addSetting,
// #withDialogActions
openDialog
}) {
// History context.
const history = useHistory();
@@ -75,13 +79,18 @@ function ManualJournalActionsBar({
// Handle import button click.
const handleImportBtnClick = () => {
history.push('/manual-journals/import');
}
};
// Handle table row size change.
const handleTableRowSizeChange = (size) => {
addSetting('manualJournals', 'tableSize', size);
};
// Handle the export button click.
const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'manual_journal' });
};
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -140,6 +149,7 @@ function ManualJournalActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
onClick={handleExportBtnClick}
/>
<NavbarDivider />
<DashboardRowsHeightButton

View File

@@ -118,6 +118,10 @@ function AccountsActionsBar({
const handleImportBtnClick = () => {
history.push('/accounts/import');
};
// Handle the export button click.
const handleExportBtnClick = () => {
openDialog(DialogsName.Export, { resource: 'account' });
};
return (
<DashboardActionsBar>
@@ -182,17 +186,18 @@ function AccountsActionsBar({
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />}
onClick={handleImportBtnClick}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
onClick={handleExportBtnClick}
/>
<NavbarDivider />
<DashboardRowsHeightButton
initialValue={accountsTableSize}

View File

@@ -0,0 +1,34 @@
// @ts-nocheck
import styled from 'styled-components';
import { Icon, FormattedMessage as T } from '@/components';
interface AuthContainerProps {
children: React.ReactNode;
}
export function AuthContainer({ children }: AuthContainerProps) {
return (
<AuthPage>
<AuthInsider>
<AuthLogo>
<Icon icon="bigcapital" height={37} width={214} />
</AuthLogo>
{children}
</AuthInsider>
</AuthPage>
);
}
const AuthPage = styled.div``;
const AuthInsider = styled.div`
width: 384px;
margin: 0 auto;
margin-bottom: 40px;
padding-top: 80px;
`;
const AuthLogo = styled.div`
text-align: center;
margin-bottom: 40px;
`;

View File

@@ -1,24 +1,16 @@
// @ts-nocheck
import React from 'react';
import { Redirect, Route, Switch, useLocation } from 'react-router-dom';
import { Route, Switch, useLocation } from 'react-router-dom';
import BodyClassName from 'react-body-classname';
import styled from 'styled-components';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import authenticationRoutes from '@/routes/authentication';
import { Icon, FormattedMessage as T } from '@/components';
import { useIsAuthenticated } from '@/hooks/state';
import { AuthMetaBootProvider } from './AuthMetaBoot';
import '@/style/pages/Authentication/Auth.scss';
export function Authentication() {
const to = { pathname: '/' };
const isAuthenticated = useIsAuthenticated();
if (isAuthenticated) {
return <Redirect to={to} />;
}
return (
<BodyClassName className={'authentication'}>
<AuthPage>

View File

@@ -0,0 +1,46 @@
// @ts-nocheck
import { useEffect, useMemo } from 'react';
import { useLocation, useHistory } from 'react-router-dom';
import { useAuthSignUpVerify } from '@/hooks/query';
import { AppToaster } from '@/components';
import { Intent } from '@blueprintjs/core';
function useQuery() {
const { search } = useLocation();
return useMemo(() => new URLSearchParams(search), [search]);
}
export default function EmailConfirmation() {
const { mutateAsync: authSignupVerify } = useAuthSignUpVerify();
const history = useHistory();
const query = useQuery();
const token = query.get('token');
const email = query.get('email');
useEffect(() => {
if (!token || !email) {
history.push('/auth/login');
}
}, [history, token, email]);
useEffect(() => {
authSignupVerify({ token, email })
.then(() => {
AppToaster.show({
message: 'Your email has been verified, Congrats!',
intent: Intent.SUCCESS,
});
history.push('/');
})
.catch(() => {
AppToaster.show({
message: 'Something went wrong',
intent: Intent.DANGER,
});
history.push('/');
});
}, [token, email, authSignupVerify, history]);
return null;
}

Some files were not shown because too many files have changed in this diff Show More