mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 07:10:33 +00:00
fix: import resource imporements
This commit is contained in:
@@ -27,7 +27,7 @@ export default class AccountsController extends BaseController {
|
|||||||
/**
|
/**
|
||||||
* Router constructor method.
|
* Router constructor method.
|
||||||
*/
|
*/
|
||||||
router() {
|
public router() {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
@@ -98,7 +98,7 @@ export default class AccountsController extends BaseController {
|
|||||||
/**
|
/**
|
||||||
* Create account DTO Schema validation.
|
* Create account DTO Schema validation.
|
||||||
*/
|
*/
|
||||||
get createAccountDTOSchema() {
|
private get createAccountDTOSchema() {
|
||||||
return [
|
return [
|
||||||
check('name')
|
check('name')
|
||||||
.exists()
|
.exists()
|
||||||
@@ -131,7 +131,7 @@ export default class AccountsController extends BaseController {
|
|||||||
/**
|
/**
|
||||||
* Account DTO Schema validation.
|
* Account DTO Schema validation.
|
||||||
*/
|
*/
|
||||||
get editAccountDTOSchema() {
|
private get editAccountDTOSchema() {
|
||||||
return [
|
return [
|
||||||
check('name')
|
check('name')
|
||||||
.exists()
|
.exists()
|
||||||
@@ -160,14 +160,14 @@ export default class AccountsController extends BaseController {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
get accountParamSchema() {
|
private get accountParamSchema() {
|
||||||
return [param('id').exists().isNumeric().toInt()];
|
return [param('id').exists().isNumeric().toInt()];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accounts list validation schema.
|
* Accounts list validation schema.
|
||||||
*/
|
*/
|
||||||
get accountsListSchema() {
|
private get accountsListSchema() {
|
||||||
return [
|
return [
|
||||||
query('view_slug').optional({ nullable: true }).isString().trim(),
|
query('view_slug').optional({ nullable: true }).isString().trim(),
|
||||||
query('stringified_filter_roles').optional().isJSON(),
|
query('stringified_filter_roles').optional().isJSON(),
|
||||||
|
|||||||
@@ -160,10 +160,8 @@ export default class CustomersController extends ContactsController {
|
|||||||
try {
|
try {
|
||||||
const contact = await this.customersApplication.createCustomer(
|
const contact = await this.customersApplication.createCustomer(
|
||||||
tenantId,
|
tenantId,
|
||||||
contactDTO,
|
contactDTO
|
||||||
user
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
id: contact.id,
|
id: contact.id,
|
||||||
message: 'The customer has been created successfully.',
|
message: 'The customer has been created successfully.',
|
||||||
@@ -291,7 +289,7 @@ export default class CustomersController extends ContactsController {
|
|||||||
const filter = {
|
const filter = {
|
||||||
inactiveMode: false,
|
inactiveMode: false,
|
||||||
sortOrder: 'desc',
|
sortOrder: 'desc',
|
||||||
columnSortBy: 'created_at',
|
columnSortBy: 'createdAt',
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 12,
|
pageSize: 12,
|
||||||
...this.matchedQueryData(req),
|
...this.matchedQueryData(req),
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ export default class VendorsController extends ContactsController {
|
|||||||
const vendorsFilter: IVendorsFilter = {
|
const vendorsFilter: IVendorsFilter = {
|
||||||
inactiveMode: false,
|
inactiveMode: false,
|
||||||
sortOrder: 'desc',
|
sortOrder: 'desc',
|
||||||
columnSortBy: 'created_at',
|
columnSortBy: 'createdAt',
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 12,
|
pageSize: 12,
|
||||||
...this.matchedQueryData(req),
|
...this.matchedQueryData(req),
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { body, param } from 'express-validator';
|
import { body, param, query } from 'express-validator';
|
||||||
|
import { defaultTo } from 'lodash';
|
||||||
import BaseController from '@/api/controllers/BaseController';
|
import BaseController from '@/api/controllers/BaseController';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import { ImportResourceApplication } from '@/services/Import/ImportResourceApplication';
|
import { ImportResourceApplication } from '@/services/Import/ImportResourceApplication';
|
||||||
import { uploadImportFile } from './_utils';
|
import { uploadImportFile } from './_utils';
|
||||||
|
import { parseJsonSafe } from '@/utils/parse-json-safe';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportController extends BaseController {
|
export class ImportController extends BaseController {
|
||||||
@@ -42,6 +44,17 @@ export class ImportController extends BaseController {
|
|||||||
this.asyncMiddleware(this.mapping.bind(this)),
|
this.asyncMiddleware(this.mapping.bind(this)),
|
||||||
this.catchServiceErrors
|
this.catchServiceErrors
|
||||||
);
|
);
|
||||||
|
router.get(
|
||||||
|
'/sample',
|
||||||
|
[query('resource').exists(), query('format').optional()],
|
||||||
|
this.downloadImportSample.bind(this),
|
||||||
|
this.catchServiceErrors
|
||||||
|
);
|
||||||
|
router.get(
|
||||||
|
'/:import_id',
|
||||||
|
this.asyncMiddleware(this.getImportFileMeta.bind(this)),
|
||||||
|
this.catchServiceErrors
|
||||||
|
);
|
||||||
router.get(
|
router.get(
|
||||||
'/:import_id/preview',
|
'/:import_id/preview',
|
||||||
this.asyncMiddleware(this.preview.bind(this)),
|
this.asyncMiddleware(this.preview.bind(this)),
|
||||||
@@ -55,7 +68,7 @@ export class ImportController extends BaseController {
|
|||||||
* @returns {ValidationSchema[]}
|
* @returns {ValidationSchema[]}
|
||||||
*/
|
*/
|
||||||
private get importValidationSchema() {
|
private get importValidationSchema() {
|
||||||
return [body('resource').exists()];
|
return [body('resource').exists(), body('params').optional()];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,12 +79,15 @@ export class ImportController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
private async fileUpload(req: Request, res: Response, next: NextFunction) {
|
private async fileUpload(req: Request, res: Response, next: NextFunction) {
|
||||||
const { tenantId } = req;
|
const { tenantId } = req;
|
||||||
|
const body = this.matchedBodyData(req);
|
||||||
|
const params = defaultTo(parseJsonSafe(body.params), {});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await this.importResourceApp.import(
|
const data = await this.importResourceApp.import(
|
||||||
tenantId,
|
tenantId,
|
||||||
req.body.resource,
|
body.resource,
|
||||||
req.file.filename
|
req.file.filename,
|
||||||
|
params
|
||||||
);
|
);
|
||||||
return res.status(200).send(data);
|
return res.status(200).send(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -140,6 +156,54 @@ export class ImportController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the csv/xlsx sample sheet of the given resource name.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
*/
|
||||||
|
private async downloadImportSample(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const { format, resource } = this.matchedQueryData(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = this.importResourceApp.sample(tenantId, resource, format);
|
||||||
|
|
||||||
|
return res.status(200).send(result);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the import file meta.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
*/
|
||||||
|
private async getImportFileMeta(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const { import_id: importId } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.importResourceApp.importMeta(
|
||||||
|
tenantId,
|
||||||
|
importId
|
||||||
|
);
|
||||||
|
return res.status(200).send(result);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms service errors to response.
|
* Transforms service errors to response.
|
||||||
* @param {Error}
|
* @param {Error}
|
||||||
@@ -174,7 +238,11 @@ export class ImportController extends BaseController {
|
|||||||
errors: [{ type: 'IMPORTED_FILE_EXTENSION_INVALID' }],
|
errors: [{ type: 'IMPORTED_FILE_EXTENSION_INVALID' }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [{ type: error.errorType }],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ exports.up = function (knex) {
|
|||||||
table.string('resource');
|
table.string('resource');
|
||||||
table.json('columns');
|
table.json('columns');
|
||||||
table.json('mapping');
|
table.json('mapping');
|
||||||
|
table.json('params');
|
||||||
table.timestamps();
|
table.timestamps();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default {
|
|||||||
unique: true,
|
unique: true,
|
||||||
required: true,
|
required: true,
|
||||||
importable: true,
|
importable: true,
|
||||||
|
exportable: true,
|
||||||
order: 1,
|
order: 1,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
@@ -22,6 +23,7 @@ export default {
|
|||||||
column: 'description',
|
column: 'description',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
importable: true,
|
importable: true,
|
||||||
|
exportable: true,
|
||||||
},
|
},
|
||||||
slug: {
|
slug: {
|
||||||
name: 'account.field.slug',
|
name: 'account.field.slug',
|
||||||
@@ -35,6 +37,7 @@ export default {
|
|||||||
name: 'account.field.code',
|
name: 'account.field.code',
|
||||||
column: 'code',
|
column: 'code',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
exportable: true,
|
||||||
importable: true,
|
importable: true,
|
||||||
minLength: 3,
|
minLength: 3,
|
||||||
maxLength: 6,
|
maxLength: 6,
|
||||||
@@ -75,6 +78,7 @@ export default {
|
|||||||
})),
|
})),
|
||||||
required: true,
|
required: true,
|
||||||
importable: true,
|
importable: true,
|
||||||
|
exportable: true,
|
||||||
order: 2,
|
order: 2,
|
||||||
},
|
},
|
||||||
active: {
|
active: {
|
||||||
@@ -82,6 +86,7 @@ export default {
|
|||||||
column: 'active',
|
column: 'active',
|
||||||
fieldType: 'boolean',
|
fieldType: 'boolean',
|
||||||
filterable: false,
|
filterable: false,
|
||||||
|
exportable: true,
|
||||||
importable: true,
|
importable: true,
|
||||||
},
|
},
|
||||||
balance: {
|
balance: {
|
||||||
@@ -96,6 +101,7 @@ export default {
|
|||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
filterable: false,
|
filterable: false,
|
||||||
importable: true,
|
importable: true,
|
||||||
|
exportable: true,
|
||||||
},
|
},
|
||||||
parentAccount: {
|
parentAccount: {
|
||||||
name: 'account.field.parent_account',
|
name: 'account.field.parent_account',
|
||||||
@@ -109,6 +115,7 @@ export default {
|
|||||||
column: 'created_at',
|
column: 'created_at',
|
||||||
fieldType: 'date',
|
fieldType: 'date',
|
||||||
importable: false,
|
importable: false,
|
||||||
|
exportable: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,70 +1,89 @@
|
|||||||
export default {
|
export default {
|
||||||
|
importable: true,
|
||||||
|
defaultFilterField: 'displayName',
|
||||||
|
defaultSort: {
|
||||||
|
sortOrder: 'DESC',
|
||||||
|
sortField: 'createdAt',
|
||||||
|
},
|
||||||
fields: {
|
fields: {
|
||||||
first_name: {
|
firstName: {
|
||||||
name: 'customer.field.first_name',
|
name: 'customer.field.first_name',
|
||||||
column: 'first_name',
|
column: 'first_name',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
last_name: {
|
lastName: {
|
||||||
name: 'customer.field.last_name',
|
name: 'customer.field.last_name',
|
||||||
column: 'last_name',
|
column: 'last_name',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
display_name: {
|
displayName: {
|
||||||
name: 'customer.field.display_name',
|
name: 'customer.field.display_name',
|
||||||
column: 'display_name',
|
column: 'display_name',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
required: true,
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
name: 'customer.field.email',
|
name: 'customer.field.email',
|
||||||
column: 'email',
|
column: 'email',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
work_phone: {
|
workPhone: {
|
||||||
name: 'customer.field.work_phone',
|
name: 'customer.field.work_phone',
|
||||||
column: 'work_phone',
|
column: 'work_phone',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
personal_phone: {
|
personalPhone: {
|
||||||
name: 'customer.field.personal_phone',
|
name: 'customer.field.personal_phone',
|
||||||
column: 'personal_phone',
|
column: 'personal_phone',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
company_name: {
|
companyName: {
|
||||||
name: 'customer.field.company_name',
|
name: 'customer.field.company_name',
|
||||||
column: 'company_name',
|
column: 'company_name',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
website: {
|
website: {
|
||||||
name: 'customer.field.website',
|
name: 'customer.field.website',
|
||||||
column: 'website',
|
column: 'website',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
},
|
importable: true,
|
||||||
created_at: {
|
|
||||||
name: 'customer.field.created_at',
|
|
||||||
column: 'created_at',
|
|
||||||
fieldType: 'date',
|
|
||||||
},
|
},
|
||||||
balance: {
|
balance: {
|
||||||
name: 'customer.field.balance',
|
name: 'customer.field.balance',
|
||||||
column: 'balance',
|
column: 'balance',
|
||||||
fieldType: 'number',
|
fieldType: 'number',
|
||||||
},
|
},
|
||||||
opening_balance: {
|
openingBalance: {
|
||||||
name: 'customer.field.opening_balance',
|
name: 'customer.field.opening_balance',
|
||||||
column: 'opening_balance',
|
column: 'opening_balance',
|
||||||
fieldType: 'number',
|
fieldType: 'number',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
opening_balance_at: {
|
openingBalanceAt: {
|
||||||
name: 'customer.field.opening_balance_at',
|
name: 'customer.field.opening_balance_at',
|
||||||
column: 'opening_balance_at',
|
column: 'opening_balance_at',
|
||||||
filterable: false,
|
filterable: false,
|
||||||
fieldType: 'date',
|
fieldType: 'date',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
currency_code: {
|
openingBalanceExchangeRate: {
|
||||||
|
name: 'Opening Balance Ex. Rate',
|
||||||
|
column: 'opening_balance_exchange_rate',
|
||||||
|
fieldType: 'number',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
currencyCode: {
|
||||||
name: 'customer.field.currency',
|
name: 'customer.field.currency',
|
||||||
column: 'currency_code',
|
column: 'currency_code',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
name: 'customer.field.status',
|
name: 'customer.field.status',
|
||||||
@@ -76,6 +95,99 @@ export default {
|
|||||||
{ key: 'unpaid', label: 'customer.field.status.unpaid' },
|
{ key: 'unpaid', label: 'customer.field.status.unpaid' },
|
||||||
],
|
],
|
||||||
filterCustomQuery: statusFieldFilterQuery,
|
filterCustomQuery: statusFieldFilterQuery,
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
// Billing Address
|
||||||
|
billingAddress1: {
|
||||||
|
name: 'Billing Address 1',
|
||||||
|
column: 'billing_address1',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
billingAddress2: {
|
||||||
|
name: 'Billing Address 2',
|
||||||
|
column: 'billing_address2',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
billingAddressCity: {
|
||||||
|
name: 'Billing Address City',
|
||||||
|
column: 'billing_address_city',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
billingAddressCountry: {
|
||||||
|
name: 'Billing Address Country',
|
||||||
|
column: 'billing_address_country',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
billingAddressPostcode: {
|
||||||
|
name: 'Billing Address Postcode',
|
||||||
|
column: 'billing_address_postcode',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
billingAddressState: {
|
||||||
|
name: 'Billing Address State',
|
||||||
|
column: 'billing_address_state',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
billingAddressPhone: {
|
||||||
|
name: 'Billing Address Phone',
|
||||||
|
column: 'billing_address_phone',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
// Shipping Address
|
||||||
|
shippingAddress1: {
|
||||||
|
name: 'Shipping Address 1',
|
||||||
|
column: 'shipping_address1',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
shippingAddress2: {
|
||||||
|
name: 'Shipping Address 2',
|
||||||
|
column: 'shipping_address2',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
shippingAddressCity: {
|
||||||
|
name: 'Shipping Address City',
|
||||||
|
column: 'shipping_address_city',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
shippingAddressCountry: {
|
||||||
|
name: 'Shipping Address Country',
|
||||||
|
column: 'shipping_address_country',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
shippingAddressPostcode: {
|
||||||
|
name: 'Shipping Address Postcode',
|
||||||
|
column: 'shipping_address_postcode',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
shippingAddressPhone: {
|
||||||
|
name: 'Shipping Address Phone',
|
||||||
|
column: 'shipping_address_phone',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
shippingAddressState: {
|
||||||
|
name: 'Shipping Address State',
|
||||||
|
column: 'shipping_address_state',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
//
|
||||||
|
createdAt: {
|
||||||
|
name: 'customer.field.created_at',
|
||||||
|
column: 'created_at',
|
||||||
|
fieldType: 'date',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import TenantModel from 'models/TenantModel';
|
import TenantModel from 'models/TenantModel';
|
||||||
|
|
||||||
export default class Import extends TenantModel {
|
export default class Import extends TenantModel {
|
||||||
|
resource!: string;
|
||||||
mapping!: string;
|
mapping!: string;
|
||||||
columns!: string;
|
columns!: string;
|
||||||
|
params!: Record<string, any>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name.
|
* Table name.
|
||||||
@@ -49,6 +51,14 @@ export default class Import extends TenantModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public get paramsParsed() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(this.params);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public get mappingParsed() {
|
public get mappingParsed() {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(this.mapping);
|
return JSON.parse(this.mapping);
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
export default {
|
||||||
|
defaultFilterField: 'createdAt',
|
||||||
|
defaultSort: {
|
||||||
|
sortOrder: 'DESC',
|
||||||
|
sortField: 'createdAt',
|
||||||
|
},
|
||||||
|
importable: true,
|
||||||
|
fields: {
|
||||||
|
date: {
|
||||||
|
name: 'Date',
|
||||||
|
column: 'date',
|
||||||
|
fieldType: 'date',
|
||||||
|
importable: true,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
payee: {
|
||||||
|
name: 'Payee',
|
||||||
|
column: 'payee',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
name: 'Description',
|
||||||
|
column: 'description',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
referenceNo: {
|
||||||
|
name: 'Reference No.',
|
||||||
|
column: 'reference_no',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
amount: {
|
||||||
|
name: 'Amount',
|
||||||
|
column: 'Amount',
|
||||||
|
fieldType: 'numeric',
|
||||||
|
required: true,
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
account: {
|
||||||
|
name: 'Account',
|
||||||
|
column: 'account_id',
|
||||||
|
fieldType: 'relation',
|
||||||
|
to: { model: 'Account', to: 'id' },
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
name: 'Created At',
|
||||||
|
column: 'createdAt',
|
||||||
|
fieldType: 'date',
|
||||||
|
importable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
/* eslint-disable global-require */
|
/* eslint-disable global-require */
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import { Model, ModelOptions, QueryContext, mixin } from 'objection';
|
||||||
import TenantModel from 'models/TenantModel';
|
import TenantModel from 'models/TenantModel';
|
||||||
import { Model, ModelOptions, QueryContext } from 'objection';
|
import ModelSettings from './ModelSetting';
|
||||||
import Account from './Account';
|
import Account from './Account';
|
||||||
|
import UncategorizedCashflowTransactionMeta from './UncategorizedCashflowTransaction.meta';
|
||||||
|
|
||||||
export default class UncategorizedCashflowTransaction extends TenantModel {
|
export default class UncategorizedCashflowTransaction extends mixin(
|
||||||
|
TenantModel,
|
||||||
|
[ModelSettings]
|
||||||
|
) {
|
||||||
id!: number;
|
id!: number;
|
||||||
amount!: number;
|
amount!: number;
|
||||||
categorized!: boolean;
|
categorized!: boolean;
|
||||||
@@ -35,6 +41,10 @@ export default class UncategorizedCashflowTransaction extends TenantModel {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get meta() {
|
||||||
|
return UncategorizedCashflowTransactionMeta;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the withdrawal amount.
|
* Retrieves the withdrawal amount.
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
|
|||||||
@@ -1,74 +1,88 @@
|
|||||||
export default {
|
export default {
|
||||||
defaultFilterField: 'display_name',
|
defaultFilterField: 'displayName',
|
||||||
defaultSort: {
|
defaultSort: {
|
||||||
sortOrder: 'DESC',
|
sortOrder: 'DESC',
|
||||||
sortField: 'created_at',
|
sortField: 'createdAt',
|
||||||
},
|
},
|
||||||
|
importable: true,
|
||||||
fields: {
|
fields: {
|
||||||
first_name: {
|
firstName: {
|
||||||
name: 'vendor.field.first_name',
|
name: 'vendor.field.first_name',
|
||||||
column: 'first_name',
|
column: 'first_name',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
last_name: {
|
lastName: {
|
||||||
name: 'vendor.field.last_name',
|
name: 'vendor.field.last_name',
|
||||||
column: 'last_name',
|
column: 'last_name',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
display_name: {
|
displayName: {
|
||||||
name: 'vendor.field.display_name',
|
name: 'vendor.field.display_name',
|
||||||
column: 'display_name',
|
column: 'display_name',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
required: true,
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
name: 'vendor.field.email',
|
name: 'vendor.field.email',
|
||||||
column: 'email',
|
column: 'email',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
work_phone: {
|
workPhone: {
|
||||||
name: 'vendor.field.work_phone',
|
name: 'vendor.field.work_phone',
|
||||||
column: 'work_phone',
|
column: 'work_phone',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
personal_phone: {
|
personalPhone: {
|
||||||
name: 'vendor.field.personal_pone',
|
name: 'vendor.field.personal_pone',
|
||||||
column: 'personal_phone',
|
column: 'personal_phone',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
company_name: {
|
companyName: {
|
||||||
name: 'vendor.field.company_name',
|
name: 'vendor.field.company_name',
|
||||||
column: 'company_name',
|
column: 'company_name',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
website: {
|
website: {
|
||||||
name: 'vendor.field.website',
|
name: 'vendor.field.website',
|
||||||
column: 'website',
|
column: 'website',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
},
|
importable: true,
|
||||||
created_at: {
|
|
||||||
name: 'vendor.field.created_at',
|
|
||||||
column: 'created_at',
|
|
||||||
fieldType: 'date',
|
|
||||||
},
|
},
|
||||||
balance: {
|
balance: {
|
||||||
name: 'vendor.field.balance',
|
name: 'vendor.field.balance',
|
||||||
column: 'balance',
|
column: 'balance',
|
||||||
fieldType: 'number',
|
fieldType: 'number',
|
||||||
},
|
},
|
||||||
opening_balance: {
|
openingBalance: {
|
||||||
name: 'vendor.field.opening_balance',
|
name: 'vendor.field.opening_balance',
|
||||||
column: 'opening_balance',
|
column: 'opening_balance',
|
||||||
fieldType: 'number',
|
fieldType: 'number',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
opening_balance_at: {
|
openingBalanceAt: {
|
||||||
name: 'vendor.field.opening_balance_at',
|
name: 'vendor.field.opening_balance_at',
|
||||||
column: 'opening_balance_at',
|
column: 'opening_balance_at',
|
||||||
fieldType: 'date',
|
fieldType: 'date',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
currency_code: {
|
openingBalanceExchangeRate: {
|
||||||
|
name: 'Opening Balance Ex. Rate',
|
||||||
|
column: 'opening_balance_exchange_rate',
|
||||||
|
fieldType: 'number',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
currencyCode: {
|
||||||
name: 'vendor.field.currency',
|
name: 'vendor.field.currency',
|
||||||
column: 'currency_code',
|
column: 'currency_code',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
name: 'vendor.field.status',
|
name: 'vendor.field.status',
|
||||||
@@ -87,6 +101,98 @@ export default {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
// Billing Address
|
||||||
|
billingAddress1: {
|
||||||
|
name: 'Billing Address 1',
|
||||||
|
column: 'billing_address1',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
billingAddress2: {
|
||||||
|
name: 'Billing Address 2',
|
||||||
|
column: 'billing_address2',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
billingAddressCity: {
|
||||||
|
name: 'Billing Address City',
|
||||||
|
column: 'billing_address_city',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
billingAddressCountry: {
|
||||||
|
name: 'Billing Address Country',
|
||||||
|
column: 'billing_address_country',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
billingAddressPostcode: {
|
||||||
|
name: 'Billing Address Postcode',
|
||||||
|
column: 'billing_address_postcode',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
billingAddressState: {
|
||||||
|
name: 'Billing Address State',
|
||||||
|
column: 'billing_address_state',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
billingAddressPhone: {
|
||||||
|
name: 'Billing Address Phone',
|
||||||
|
column: 'billing_address_phone',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
// Shipping Address
|
||||||
|
shippingAddress1: {
|
||||||
|
name: 'Shipping Address 1',
|
||||||
|
column: 'shipping_address1',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
shippingAddress2: {
|
||||||
|
name: 'Shipping Address 2',
|
||||||
|
column: 'shipping_address2',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
shippingAddressCity: {
|
||||||
|
name: 'Shipping Address City',
|
||||||
|
column: 'shipping_address_city',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
shippingAddressCountry: {
|
||||||
|
name: 'Shipping Address Country',
|
||||||
|
column: 'shipping_address_country',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
shippingAddressPostcode: {
|
||||||
|
name: 'Shipping Address Postcode',
|
||||||
|
column: 'shipping_address_postcode',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
shippingAddressState: {
|
||||||
|
name: 'Shipping Address State',
|
||||||
|
column: 'shipping_address_state',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
shippingAddressPhone: {
|
||||||
|
name: 'Shipping Address Phone',
|
||||||
|
column: 'shipping_address_phone',
|
||||||
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
name: 'vendor.field.created_at',
|
||||||
|
column: 'created_at',
|
||||||
|
fieldType: 'date',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,4 +34,12 @@ export class AccountsImportable extends Importable {
|
|||||||
public get concurrency() {
|
public get concurrency() {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public public sampleData(): any[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
import UnitOfWork, { IsolationLevel } from '../UnitOfWork';
|
import UnitOfWork from '../UnitOfWork';
|
||||||
import { Knex } from 'knex';
|
|
||||||
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
|
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -19,10 +19,10 @@ export class CreateUncategorizedTransaction {
|
|||||||
*/
|
*/
|
||||||
public create(
|
public create(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
createDTO: CreateUncategorizedTransactionDTO
|
createDTO: CreateUncategorizedTransactionDTO,
|
||||||
|
trx?: Knex.Transaction
|
||||||
) {
|
) {
|
||||||
const { UncategorizedCashflowTransaction, Account } =
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
this.tenancy.models(tenantId);
|
|
||||||
|
|
||||||
return this.uow.withTransaction(
|
return this.uow.withTransaction(
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -32,9 +32,9 @@ export class CreateUncategorizedTransaction {
|
|||||||
).insertAndFetch({
|
).insertAndFetch({
|
||||||
...createDTO,
|
...createDTO,
|
||||||
});
|
});
|
||||||
|
|
||||||
return transaction;
|
return transaction;
|
||||||
},
|
},
|
||||||
|
trx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
import { Importable } from '../Import/Importable';
|
||||||
|
import { CreateUncategorizedTransaction } from './CreateUncategorizedTransaction';
|
||||||
|
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
|
||||||
|
import { ImportableContext } from '../Import/interfaces';
|
||||||
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
import { ImportSampleData } from './constants';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class UncategorizedTransactionsImportable extends Importable {
|
||||||
|
@Inject()
|
||||||
|
private createUncategorizedTransaction: CreateUncategorizedTransaction;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
/**
|
||||||
|
* Passing the sheet DTO to create uncategorized transaction.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {any} createDTO
|
||||||
|
* @param {Knex.Transaction} trx
|
||||||
|
*/
|
||||||
|
public async importable(
|
||||||
|
tenantId: number,
|
||||||
|
createDTO: CreateUncategorizedTransactionDTO,
|
||||||
|
trx?: Knex.Transaction
|
||||||
|
) {
|
||||||
|
return this.createUncategorizedTransaction.create(tenantId, createDTO, trx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformes the DTO before validating and importing.
|
||||||
|
* @param {CreateUncategorizedTransactionDTO} createDTO
|
||||||
|
* @param {ImportableContext} context
|
||||||
|
* @returns {CreateUncategorizedTransactionDTO}
|
||||||
|
*/
|
||||||
|
public transform(
|
||||||
|
createDTO: CreateUncategorizedTransactionDTO,
|
||||||
|
context?: ImportableContext
|
||||||
|
): CreateUncategorizedTransactionDTO {
|
||||||
|
return {
|
||||||
|
...createDTO,
|
||||||
|
accountId: context.import.paramsParsed.accountId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sample data used to download sample sheet.
|
||||||
|
* @returns {Record<string, any>[]}
|
||||||
|
*/
|
||||||
|
public sampleData(): Record<string, any>[] {
|
||||||
|
return ImportSampleData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params validation schema.
|
||||||
|
* @returns {ValidationSchema[]}
|
||||||
|
*/
|
||||||
|
public paramsValidationSchema() {
|
||||||
|
return yup.object().shape({
|
||||||
|
accountId: yup.number().required(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the params existance asyncly.
|
||||||
|
* @param {number} tenantId -
|
||||||
|
* @param {Record<string, any>} params -
|
||||||
|
*/
|
||||||
|
public async validateParams(
|
||||||
|
tenantId: number,
|
||||||
|
params: Record<string, any>
|
||||||
|
): Promise<void> {
|
||||||
|
const { Account } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
if (params.accountId) {
|
||||||
|
await Account.query()
|
||||||
|
.findById(params.accountId)
|
||||||
|
.throwIfNotFound({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,3 +75,25 @@ export interface ICashflowTransactionTypeMeta {
|
|||||||
direction: CASHFLOW_DIRECTION;
|
direction: CASHFLOW_DIRECTION;
|
||||||
creditType: string[];
|
creditType: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const ImportSampleData = [
|
||||||
|
{
|
||||||
|
Amount: 5000,
|
||||||
|
Date: '2024-01-01',
|
||||||
|
Payee: 'John Roberts',
|
||||||
|
Description: 'Cheque deposit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Amount: 5000,
|
||||||
|
Date: '2024-01-01',
|
||||||
|
Payee: 'John Roberts',
|
||||||
|
Description: 'Cheque deposit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Amount: 5000,
|
||||||
|
Date: '2024-01-01',
|
||||||
|
Payee: 'John Roberts',
|
||||||
|
Description: 'Cheque deposit',
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -36,7 +36,7 @@ export class CreateCustomer {
|
|||||||
public async createCustomer(
|
public async createCustomer(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
customerDTO: ICustomerNewDTO,
|
customerDTO: ICustomerNewDTO,
|
||||||
authorizedUser: ISystemUser
|
trx?: Knex.Transaction
|
||||||
): Promise<ICustomer> {
|
): Promise<ICustomer> {
|
||||||
const { Contact } = this.tenancy.models(tenantId);
|
const { Contact } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
@@ -46,28 +46,31 @@ export class CreateCustomer {
|
|||||||
customerDTO
|
customerDTO
|
||||||
);
|
);
|
||||||
// Creates a new customer under unit-of-work envirement.
|
// Creates a new customer under unit-of-work envirement.
|
||||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
return this.uow.withTransaction(
|
||||||
// Triggers `onCustomerCreating` event.
|
tenantId,
|
||||||
await this.eventPublisher.emitAsync(events.customers.onCreating, {
|
async (trx: Knex.Transaction) => {
|
||||||
tenantId,
|
// Triggers `onCustomerCreating` event.
|
||||||
customerDTO,
|
await this.eventPublisher.emitAsync(events.customers.onCreating, {
|
||||||
trx,
|
tenantId,
|
||||||
} as ICustomerEventCreatingPayload);
|
customerDTO,
|
||||||
|
trx,
|
||||||
|
} as ICustomerEventCreatingPayload);
|
||||||
|
|
||||||
// Creates a new contact as customer.
|
// Creates a new contact as customer.
|
||||||
const customer = await Contact.query(trx).insertAndFetch({
|
const customer = await Contact.query(trx).insertAndFetch({
|
||||||
...customerObj,
|
...customerObj,
|
||||||
});
|
});
|
||||||
// Triggers `onCustomerCreated` event.
|
// Triggers `onCustomerCreated` event.
|
||||||
await this.eventPublisher.emitAsync(events.customers.onCreated, {
|
await this.eventPublisher.emitAsync(events.customers.onCreated, {
|
||||||
customer,
|
customer,
|
||||||
tenantId,
|
tenantId,
|
||||||
customerId: customer.id,
|
customerId: customer.id,
|
||||||
authorizedUser,
|
trx,
|
||||||
trx,
|
} as ICustomerEventCreatedPayload);
|
||||||
} as ICustomerEventCreatedPayload);
|
|
||||||
|
|
||||||
return customer;
|
return customer;
|
||||||
});
|
},
|
||||||
|
trx
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { defaultTo, omit, isEmpty } from 'lodash';
|
import { defaultTo, omit, isEmpty } from 'lodash';
|
||||||
import { Service, Inject } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
import {
|
import {
|
||||||
ContactService,
|
ContactService,
|
||||||
ICustomer,
|
ICustomer,
|
||||||
@@ -51,6 +51,10 @@ export class CreateEditCustomerDTO {
|
|||||||
).toMySqlDateTime(),
|
).toMySqlDateTime(),
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
|
openingBalanceExchangeRate: defaultTo(
|
||||||
|
customerDTO.openingBalanceExchangeRate,
|
||||||
|
1
|
||||||
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
ICustomerEditDTO,
|
ICustomerEditDTO,
|
||||||
ICustomerEventEditedPayload,
|
ICustomerEventEditedPayload,
|
||||||
ICustomerEventEditingPayload,
|
ICustomerEventEditingPayload,
|
||||||
ISystemUser,
|
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
import UnitOfWork from '@/services/UnitOfWork';
|
import UnitOfWork from '@/services/UnitOfWork';
|
||||||
|
|||||||
@@ -53,13 +53,8 @@ export class CustomersApplication {
|
|||||||
public createCustomer = (
|
public createCustomer = (
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
customerDTO: ICustomerNewDTO,
|
customerDTO: ICustomerNewDTO,
|
||||||
authorizedUser: ISystemUser
|
|
||||||
) => {
|
) => {
|
||||||
return this.createCustomerService.createCustomer(
|
return this.createCustomerService.createCustomer(tenantId, customerDTO);
|
||||||
tenantId,
|
|
||||||
customerDTO,
|
|
||||||
authorizedUser
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { Importable } from '@/services/Import/Importable';
|
||||||
|
import { CreateCustomer } from './CRUD/CreateCustomer';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { ICustomer, ICustomerNewDTO } from '@/interfaces';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class CustomersImportable extends Importable {
|
||||||
|
@Inject()
|
||||||
|
private createCustomerService: CreateCustomer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapps the imported data to create a new customer service.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {ICustomerNewDTO} createDTO
|
||||||
|
* @param {Knex.Transaction} trx
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async importable(
|
||||||
|
tenantId: number,
|
||||||
|
createDTO: ICustomerNewDTO,
|
||||||
|
trx?: Knex.Transaction<any, any[]>
|
||||||
|
): Promise<void> {
|
||||||
|
await this.createCustomerService.createCustomer(tenantId, createDTO, trx);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,10 @@ export class CreateEditVendorDTO {
|
|||||||
).toMySqlDateTime(),
|
).toMySqlDateTime(),
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
|
openingBalanceExchangeRate: defaultTo(
|
||||||
|
vendorDTO.openingBalanceExchangeRate,
|
||||||
|
1
|
||||||
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export class CreateVendor {
|
|||||||
public async createVendor(
|
public async createVendor(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
vendorDTO: IVendorNewDTO,
|
vendorDTO: IVendorNewDTO,
|
||||||
authorizedUser: ISystemUser
|
trx?: Knex.Transaction
|
||||||
) {
|
) {
|
||||||
const { Contact } = this.tenancy.models(tenantId);
|
const { Contact } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
@@ -45,28 +45,31 @@ export class CreateVendor {
|
|||||||
vendorDTO
|
vendorDTO
|
||||||
);
|
);
|
||||||
// Creates vendor contact under unit-of-work evnirement.
|
// Creates vendor contact under unit-of-work evnirement.
|
||||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
return this.uow.withTransaction(
|
||||||
// Triggers `onVendorCreating` event.
|
tenantId,
|
||||||
await this.eventPublisher.emitAsync(events.vendors.onCreating, {
|
async (trx: Knex.Transaction) => {
|
||||||
tenantId,
|
// Triggers `onVendorCreating` event.
|
||||||
vendorDTO,
|
await this.eventPublisher.emitAsync(events.vendors.onCreating, {
|
||||||
trx,
|
tenantId,
|
||||||
} as IVendorEventCreatingPayload);
|
vendorDTO,
|
||||||
|
trx,
|
||||||
|
} as IVendorEventCreatingPayload);
|
||||||
|
|
||||||
// Creates a new contact as vendor.
|
// Creates a new contact as vendor.
|
||||||
const vendor = await Contact.query(trx).insertAndFetch({
|
const vendor = await Contact.query(trx).insertAndFetch({
|
||||||
...vendorObject,
|
...vendorObject,
|
||||||
});
|
});
|
||||||
// Triggers `onVendorCreated` event.
|
// Triggers `onVendorCreated` event.
|
||||||
await this.eventPublisher.emitAsync(events.vendors.onCreated, {
|
await this.eventPublisher.emitAsync(events.vendors.onCreated, {
|
||||||
tenantId,
|
tenantId,
|
||||||
vendorId: vendor.id,
|
vendorId: vendor.id,
|
||||||
vendor,
|
vendor,
|
||||||
authorizedUser,
|
trx,
|
||||||
trx,
|
} as IVendorEventCreatedPayload);
|
||||||
} as IVendorEventCreatedPayload);
|
|
||||||
|
|
||||||
return vendor;
|
return vendor;
|
||||||
});
|
},
|
||||||
|
trx
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Importable } from '@/services/Import/Importable';
|
||||||
|
import { CreateVendor } from './CRUD/CreateVendor';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class VendorsImportable extends Importable {
|
||||||
|
@Inject()
|
||||||
|
private createVendorService: CreateVendor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the imported data to create a new vendor service.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {} createDTO
|
||||||
|
* @param {Knex.Transaction} trx
|
||||||
|
*/
|
||||||
|
public async importable(
|
||||||
|
tenantId: number,
|
||||||
|
createDTO: any,
|
||||||
|
trx?: Knex.Transaction<any, any[]>
|
||||||
|
): Promise<void> {
|
||||||
|
await this.createVendorService.createVendor(tenantId, createDTO, trx);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,16 +7,16 @@ import { first } from 'lodash';
|
|||||||
import { ImportFileDataValidator } from './ImportFileDataValidator';
|
import { ImportFileDataValidator } from './ImportFileDataValidator';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import {
|
import {
|
||||||
ImportInsertError,
|
|
||||||
ImportOperError,
|
ImportOperError,
|
||||||
ImportOperSuccess,
|
ImportOperSuccess,
|
||||||
|
ImportableContext,
|
||||||
} from './interfaces';
|
} from './interfaces';
|
||||||
import { AccountsImportable } from '../Accounts/AccountsImportable';
|
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import { trimObject } from './_utils';
|
import { trimObject } from './_utils';
|
||||||
import { ImportableResources } from './ImportableResources';
|
import { ImportableResources } from './ImportableResources';
|
||||||
import ResourceService from '../Resource/ResourceService';
|
import ResourceService from '../Resource/ResourceService';
|
||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
import Import from '@/models/Import';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportFileCommon {
|
export class ImportFileCommon {
|
||||||
@@ -39,12 +39,12 @@ export class ImportFileCommon {
|
|||||||
* @returns {Record<string, any>[]} - The mapped data objects.
|
* @returns {Record<string, any>[]} - The mapped data objects.
|
||||||
*/
|
*/
|
||||||
public parseXlsxSheet(buffer: Buffer): Record<string, unknown>[] {
|
public parseXlsxSheet(buffer: Buffer): Record<string, unknown>[] {
|
||||||
const workbook = XLSX.read(buffer, { type: 'buffer' });
|
const workbook = XLSX.read(buffer, { type: 'buffer', raw: true });
|
||||||
|
|
||||||
const firstSheetName = workbook.SheetNames[0];
|
const firstSheetName = workbook.SheetNames[0];
|
||||||
const worksheet = workbook.Sheets[firstSheetName];
|
const worksheet = workbook.Sheets[firstSheetName];
|
||||||
|
|
||||||
return XLSX.utils.sheet_to_json(worksheet);
|
return XLSX.utils.sheet_to_json(worksheet, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,7 +57,7 @@ export class ImportFileCommon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Imports the given parsed data to the resource storage through registered importable service.
|
* Imports the given parsed data to the resource storage through registered importable service.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
* @param {string} resourceName - Resource name.
|
* @param {string} resourceName - Resource name.
|
||||||
* @param {Record<string, any>} parsedData - Parsed data.
|
* @param {Record<string, any>} parsedData - Parsed data.
|
||||||
@@ -66,16 +66,16 @@ export class ImportFileCommon {
|
|||||||
*/
|
*/
|
||||||
public async import(
|
public async import(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
resourceName: string,
|
importFile: Import,
|
||||||
parsedData: Record<string, any>[],
|
parsedData: Record<string, any>[],
|
||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
): Promise<[ImportOperSuccess[], ImportOperError[]]> {
|
): Promise<[ImportOperSuccess[], ImportOperError[]]> {
|
||||||
const importableFields = this.resource.getResourceImportableFields(
|
const importableFields = this.resource.getResourceImportableFields(
|
||||||
tenantId,
|
tenantId,
|
||||||
resourceName
|
importFile.resource
|
||||||
);
|
);
|
||||||
const ImportableRegistry = this.importable.registry;
|
const ImportableRegistry = this.importable.registry;
|
||||||
const importable = ImportableRegistry.getImportable(resourceName);
|
const importable = ImportableRegistry.getImportable(importFile.resource);
|
||||||
|
|
||||||
const concurrency = importable.concurrency || 10;
|
const concurrency = importable.concurrency || 10;
|
||||||
|
|
||||||
@@ -83,15 +83,25 @@ export class ImportFileCommon {
|
|||||||
const failed: ImportOperError[] = [];
|
const failed: ImportOperError[] = [];
|
||||||
|
|
||||||
const importAsync = async (objectDTO, index: number): Promise<void> => {
|
const importAsync = async (objectDTO, index: number): Promise<void> => {
|
||||||
|
const context: ImportableContext = {
|
||||||
|
rowIndex: index,
|
||||||
|
import: importFile,
|
||||||
|
};
|
||||||
|
const transformedDTO = importable.transform(objectDTO, context);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Validate the DTO object before passing it to the service layer.
|
// Validate the DTO object before passing it to the service layer.
|
||||||
await this.importFileValidator.validateData(
|
await this.importFileValidator.validateData(
|
||||||
importableFields,
|
importableFields,
|
||||||
objectDTO
|
transformedDTO
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
// Run the importable function and listen to the errors.
|
// Run the importable function and listen to the errors.
|
||||||
const data = await importable.importable(tenantId, objectDTO, trx);
|
const data = await importable.importable(
|
||||||
|
tenantId,
|
||||||
|
transformedDTO,
|
||||||
|
trx
|
||||||
|
);
|
||||||
success.push({ index, data });
|
success.push({ index, data });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof ServiceError) {
|
if (err instanceof ServiceError) {
|
||||||
@@ -115,6 +125,60 @@ export class ImportFileCommon {
|
|||||||
return [success, failed];
|
return [success, failed];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} resourceName
|
||||||
|
* @param {Record<string, any>} params
|
||||||
|
*/
|
||||||
|
public async validateParamsSchema(
|
||||||
|
resourceName: string,
|
||||||
|
params: Record<string, any>
|
||||||
|
) {
|
||||||
|
const ImportableRegistry = this.importable.registry;
|
||||||
|
const importable = ImportableRegistry.getImportable(resourceName);
|
||||||
|
|
||||||
|
const yupSchema = importable.paramsValidationSchema();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await yupSchema.validate(params, { abortEarly: false });
|
||||||
|
} catch (validationError) {
|
||||||
|
const errors = validationError.inner.map((error) => ({
|
||||||
|
errorCode: 'ParamsValidationError',
|
||||||
|
errorMessage: error.errors,
|
||||||
|
}));
|
||||||
|
throw errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} resourceName
|
||||||
|
* @param {Record<string, any>} params
|
||||||
|
*/
|
||||||
|
public async validateParams(
|
||||||
|
tenantId: number,
|
||||||
|
resourceName: string,
|
||||||
|
params: Record<string, any>
|
||||||
|
) {
|
||||||
|
const ImportableRegistry = this.importable.registry;
|
||||||
|
const importable = ImportableRegistry.getImportable(resourceName);
|
||||||
|
|
||||||
|
await importable.validateParams(tenantId, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} resourceName
|
||||||
|
* @param {Record<string, any>} params
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public transformParams(resourceName: string, params: Record<string, any>) {
|
||||||
|
const ImportableRegistry = this.importable.registry;
|
||||||
|
const importable = ImportableRegistry.getImportable(resourceName);
|
||||||
|
|
||||||
|
return importable.transformParams(params);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the sheet columns from the given sheet data.
|
* Retrieves the sheet columns from the given sheet data.
|
||||||
* @param {unknown[]} json
|
* @param {unknown[]} json
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { fromPairs } from 'lodash';
|
import { fromPairs } from 'lodash';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
import { ImportFileMapPOJO, ImportMappingAttr } from './interfaces';
|
import {
|
||||||
|
ImportDateFormats,
|
||||||
|
ImportFileMapPOJO,
|
||||||
|
ImportMappingAttr,
|
||||||
|
} from './interfaces';
|
||||||
import ResourceService from '../Resource/ResourceService';
|
import ResourceService from '../Resource/ResourceService';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import { ERRORS } from './_utils';
|
import { ERRORS } from './_utils';
|
||||||
@@ -37,12 +41,14 @@ export class ImportFileMapping {
|
|||||||
// Validate the diplicated relations of map attrs.
|
// Validate the diplicated relations of map attrs.
|
||||||
this.validateDuplicatedMapAttrs(maps);
|
this.validateDuplicatedMapAttrs(maps);
|
||||||
|
|
||||||
|
// Validate the date format mapping.
|
||||||
|
this.validateDateFormatMapping(tenantId, importFile.resource, maps);
|
||||||
|
|
||||||
const mappingStringified = JSON.stringify(maps);
|
const mappingStringified = JSON.stringify(maps);
|
||||||
|
|
||||||
await Import.query().findById(importFile.id).patch({
|
await Import.query().findById(importFile.id).patch({
|
||||||
mapping: mappingStringified,
|
mapping: mappingStringified,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
import: {
|
import: {
|
||||||
importId: importFile.importId,
|
importId: importFile.importId,
|
||||||
@@ -106,4 +112,34 @@ export class ImportFileMapping {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the date format mapping.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {string} resource
|
||||||
|
* @param {ImportMappingAttr[]} maps
|
||||||
|
*/
|
||||||
|
private validateDateFormatMapping(
|
||||||
|
tenantId: number,
|
||||||
|
resource: string,
|
||||||
|
maps: ImportMappingAttr[]
|
||||||
|
) {
|
||||||
|
const fields = this.resource.getResourceImportableFields(
|
||||||
|
tenantId,
|
||||||
|
resource
|
||||||
|
);
|
||||||
|
maps.forEach((map) => {
|
||||||
|
if (
|
||||||
|
typeof fields[map.to] !== 'undefined' &&
|
||||||
|
fields[map.to].fieldType === 'date'
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
typeof map.dateFormat !== 'undefined' &&
|
||||||
|
ImportDateFormats.indexOf(map.dateFormat) === -1
|
||||||
|
) {
|
||||||
|
throw new ServiceError(ERRORS.INVALID_MAP_DATE_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
packages/server/src/services/Import/ImportFileMeta.ts
Normal file
32
packages/server/src/services/Import/ImportFileMeta.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
|
import { ImportFileMetaTransformer } from './ImportFileMetaTransformer';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class ImportFileMeta {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private transformer: TransformerInjectable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} importId
|
||||||
|
* @returns {}
|
||||||
|
*/
|
||||||
|
async getImportMeta(tenantId: number, importId: string) {
|
||||||
|
const { Import } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const importFile = await Import.query().findOne('importId', importId);
|
||||||
|
|
||||||
|
// Retrieves the transformed accounts collection.
|
||||||
|
return this.transformer.transform(
|
||||||
|
tenantId,
|
||||||
|
importFile,
|
||||||
|
new ImportFileMetaTransformer()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
|
||||||
|
export class ImportFileMetaTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Include these attributes to sale invoice object.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return ['map'];
|
||||||
|
};
|
||||||
|
|
||||||
|
public excludeAttributes = (): string[] => {
|
||||||
|
return ['id', 'filename', 'columns', 'mappingParsed', 'mapping'];
|
||||||
|
}
|
||||||
|
|
||||||
|
map(importFile) {
|
||||||
|
return importFile.mappingParsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,12 +67,7 @@ export class ImportFileProcess {
|
|||||||
const [successedImport, failedImport] = await this.uow.withTransaction(
|
const [successedImport, failedImport] = await this.uow.withTransaction(
|
||||||
tenantId,
|
tenantId,
|
||||||
(trx: Knex.Transaction) =>
|
(trx: Knex.Transaction) =>
|
||||||
this.importCommon.import(
|
this.importCommon.import(tenantId, importFile, parsedData, trx),
|
||||||
tenantId,
|
|
||||||
importFile.resource,
|
|
||||||
parsedData,
|
|
||||||
trx
|
|
||||||
),
|
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
const mapping = importFile.mappingParsed;
|
const mapping = importFile.mappingParsed;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { IModelMetaField } from '@/interfaces';
|
|||||||
import { ImportFileCommon } from './ImportFileCommon';
|
import { ImportFileCommon } from './ImportFileCommon';
|
||||||
import { ImportFileDataValidator } from './ImportFileDataValidator';
|
import { ImportFileDataValidator } from './ImportFileDataValidator';
|
||||||
import { ImportFileUploadPOJO } from './interfaces';
|
import { ImportFileUploadPOJO } from './interfaces';
|
||||||
|
import { ServiceError } from '@/exceptions';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportFileUploadService {
|
export class ImportFileUploadService {
|
||||||
@@ -32,13 +33,15 @@ export class ImportFileUploadService {
|
|||||||
public async import(
|
public async import(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
resourceName: string,
|
resourceName: string,
|
||||||
filename: string
|
filename: string,
|
||||||
|
params: Record<string, number | string>
|
||||||
): Promise<ImportFileUploadPOJO> {
|
): Promise<ImportFileUploadPOJO> {
|
||||||
const { Import } = this.tenancy.models(tenantId);
|
const { Import } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const resource = sanitizeResourceName(resourceName);
|
||||||
const resourceMeta = this.resourceService.getResourceMeta(
|
const resourceMeta = this.resourceService.getResourceMeta(
|
||||||
tenantId,
|
tenantId,
|
||||||
resourceName
|
resource
|
||||||
);
|
);
|
||||||
// Throw service error if the resource does not support importing.
|
// Throw service error if the resource does not support importing.
|
||||||
this.importValidator.validateResourceImportable(resourceMeta);
|
this.importValidator.validateResourceImportable(resourceMeta);
|
||||||
@@ -48,22 +51,32 @@ export class ImportFileUploadService {
|
|||||||
|
|
||||||
// Parse the buffer file to array data.
|
// Parse the buffer file to array data.
|
||||||
const sheetData = this.importFileCommon.parseXlsxSheet(buffer);
|
const sheetData = this.importFileCommon.parseXlsxSheet(buffer);
|
||||||
|
|
||||||
const sheetColumns = this.importFileCommon.parseSheetColumns(sheetData);
|
const sheetColumns = this.importFileCommon.parseSheetColumns(sheetData);
|
||||||
const coumnsStringified = JSON.stringify(sheetColumns);
|
const coumnsStringified = JSON.stringify(sheetColumns);
|
||||||
|
|
||||||
const _resourceName = sanitizeResourceName(resourceName);
|
try {
|
||||||
|
// Validates the params Yup schema.
|
||||||
|
await this.importFileCommon.validateParamsSchema(resource, params);
|
||||||
|
|
||||||
|
// Validates importable params asyncly.
|
||||||
|
await this.importFileCommon.validateParams(tenantId, resource, params);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const _params = this.importFileCommon.transformParams(resource, params);
|
||||||
|
const paramsStringified = JSON.stringify(_params);
|
||||||
|
|
||||||
// Store the import model with related metadata.
|
// Store the import model with related metadata.
|
||||||
const importFile = await Import.query().insert({
|
const importFile = await Import.query().insert({
|
||||||
filename,
|
filename,
|
||||||
|
resource,
|
||||||
importId: filename,
|
importId: filename,
|
||||||
resource: _resourceName,
|
|
||||||
columns: coumnsStringified,
|
columns: coumnsStringified,
|
||||||
|
params: paramsStringified,
|
||||||
});
|
});
|
||||||
const resourceColumnsMap = this.resourceService.getResourceImportableFields(
|
const resourceColumnsMap = this.resourceService.getResourceImportableFields(
|
||||||
tenantId,
|
tenantId,
|
||||||
_resourceName
|
resource
|
||||||
);
|
);
|
||||||
const resourceColumns = this.getResourceColumns(resourceColumnsMap);
|
const resourceColumns = this.getResourceColumns(resourceColumnsMap);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { ImportFileMapping } from './ImportFileMapping';
|
|||||||
import { ImportMappingAttr } from './interfaces';
|
import { ImportMappingAttr } from './interfaces';
|
||||||
import { ImportFileProcess } from './ImportFileProcess';
|
import { ImportFileProcess } from './ImportFileProcess';
|
||||||
import { ImportFilePreview } from './ImportFilePreview';
|
import { ImportFilePreview } from './ImportFilePreview';
|
||||||
|
import { ImportSampleService } from './ImportSample';
|
||||||
|
import { ImportFileMeta } from './ImportFileMeta';
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
export class ImportResourceApplication {
|
export class ImportResourceApplication {
|
||||||
@@ -19,25 +21,32 @@ export class ImportResourceApplication {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private ImportFilePreviewService: ImportFilePreview;
|
private ImportFilePreviewService: ImportFilePreview;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private importSampleService: ImportSampleService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private importMetaService: ImportFileMeta;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the imported file and stores the import file meta under unqiue id.
|
* Reads the imported file and stores the import file meta under unqiue id.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
* @param {string} resource -
|
* @param {string} resource - Resource name.
|
||||||
* @param {string} fileName -
|
* @param {string} fileName - File name.
|
||||||
* @returns {Promise<ImportFileUploadPOJO>}
|
* @returns {Promise<ImportFileUploadPOJO>}
|
||||||
*/
|
*/
|
||||||
public async import(
|
public async import(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
resource: string,
|
resource: string,
|
||||||
filename: string
|
filename: string,
|
||||||
|
params: Record<string, any>
|
||||||
) {
|
) {
|
||||||
return this.importFileService.import(tenantId, resource, filename);
|
return this.importFileService.import(tenantId, resource, filename, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapping the excel sheet columns with resource columns.
|
* Mapping the excel sheet columns with resource columns.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {number} importId
|
* @param {number} importId - Import id.
|
||||||
* @param {ImportMappingAttr} maps
|
* @param {ImportMappingAttr} maps
|
||||||
*/
|
*/
|
||||||
public async mapping(
|
public async mapping(
|
||||||
@@ -51,7 +60,7 @@ export class ImportResourceApplication {
|
|||||||
/**
|
/**
|
||||||
* Preview the mapped results before process importing.
|
* Preview the mapped results before process importing.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {number} importId
|
* @param {number} importId - Import id.
|
||||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||||
*/
|
*/
|
||||||
public async preview(tenantId: number, importId: number) {
|
public async preview(tenantId: number, importId: number) {
|
||||||
@@ -67,4 +76,27 @@ export class ImportResourceApplication {
|
|||||||
public async process(tenantId: number, importId: number) {
|
public async process(tenantId: number, importId: number) {
|
||||||
return this.importProcessService.import(tenantId, importId);
|
return this.importProcessService.import(tenantId, importId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the import meta of the given import id.
|
||||||
|
* @param {number} tenantId -
|
||||||
|
* @param {string} importId - Import id.
|
||||||
|
* @returns {}
|
||||||
|
*/
|
||||||
|
public importMeta(tenantId: number, importId: string) {
|
||||||
|
return this.importMetaService.getImportMeta(tenantId, importId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the csv/xlsx sample sheet of the given
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} resource - Resource name.
|
||||||
|
*/
|
||||||
|
public sample(
|
||||||
|
tenantId: number,
|
||||||
|
resource: string,
|
||||||
|
format: 'csv' | 'xlsx' = 'csv'
|
||||||
|
) {
|
||||||
|
return this.importSampleService.sample(tenantId, resource, format);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
packages/server/src/services/Import/ImportSample.ts
Normal file
46
packages/server/src/services/Import/ImportSample.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import XLSX from 'xlsx';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { ImportableResources } from './ImportableResources';
|
||||||
|
import { sanitizeResourceName } from './_utils';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class ImportSampleService {
|
||||||
|
@Inject()
|
||||||
|
private importable: ImportableResources;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the sample sheet of the given resource.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {string} resource
|
||||||
|
* @param {string} format
|
||||||
|
* @returns {Buffer | string}
|
||||||
|
*/
|
||||||
|
public sample(
|
||||||
|
tenantId: number,
|
||||||
|
resource: string,
|
||||||
|
format: 'csv' | 'xlsx'
|
||||||
|
): Buffer | string {
|
||||||
|
const _resource = sanitizeResourceName(resource);
|
||||||
|
|
||||||
|
const ImportableRegistry = this.importable.registry;
|
||||||
|
const importable = ImportableRegistry.getImportable(_resource);
|
||||||
|
|
||||||
|
const data = importable.sampleData();
|
||||||
|
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
const worksheet = XLSX.utils.json_to_sheet(data);
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
||||||
|
|
||||||
|
// Determine the output format
|
||||||
|
if (format === 'csv') {
|
||||||
|
const csvOutput = XLSX.utils.sheet_to_csv(worksheet);
|
||||||
|
return csvOutput;
|
||||||
|
} else {
|
||||||
|
const xlsxOutput = XLSX.write(workbook, {
|
||||||
|
bookType: 'xlsx',
|
||||||
|
type: 'buffer',
|
||||||
|
});
|
||||||
|
return xlsxOutput;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import { ImportableContext } from './interfaces';
|
||||||
|
|
||||||
export abstract class Importable {
|
export abstract class Importable {
|
||||||
/**
|
/**
|
||||||
@@ -13,6 +15,16 @@ export abstract class Importable {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformes the DTO before passing it to importable and validation.
|
||||||
|
* @param {Record<string, any>} createDTO
|
||||||
|
* @param {ImportableContext} context
|
||||||
|
* @returns {Record<string, any>}
|
||||||
|
*/
|
||||||
|
public transform(createDTO: Record<string, any>, context: ImportableContext) {
|
||||||
|
return createDTO;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Concurrency controlling of the importing process.
|
* Concurrency controlling of the importing process.
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
@@ -20,4 +32,41 @@ export abstract class Importable {
|
|||||||
public get concurrency() {
|
public get concurrency() {
|
||||||
return 10;
|
return 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the sample data of importable.
|
||||||
|
* @returns {Array<any>}
|
||||||
|
*/
|
||||||
|
public sampleData(): Array<any> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------
|
||||||
|
// # Params
|
||||||
|
// ------------------
|
||||||
|
/**
|
||||||
|
* Params Yup validation schema.
|
||||||
|
* @returns {Yup.ObjectSchema<object, object>}
|
||||||
|
*/
|
||||||
|
public paramsValidationSchema(): Yup.ObjectSchema<object, object> {
|
||||||
|
return Yup.object().nullable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the params of the importable service.
|
||||||
|
* @param {Record<string, any>}
|
||||||
|
* @returns {Promise<boolean>} - True means passed and false failed.
|
||||||
|
*/
|
||||||
|
public async validateParams(
|
||||||
|
tenantId: number,
|
||||||
|
params: Record<string, any>
|
||||||
|
): Promise<void> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformes the import params before storing them.
|
||||||
|
* @param {Record<string, any>} parmas
|
||||||
|
*/
|
||||||
|
public transformParams(parmas: Record<string, any>) {
|
||||||
|
return parmas;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import Container, { Service } from 'typedi';
|
import Container, { Service } from 'typedi';
|
||||||
import { AccountsImportable } from '../Accounts/AccountsImportable';
|
import { AccountsImportable } from '../Accounts/AccountsImportable';
|
||||||
import { ImportableRegistry } from './ImportableRegistry';
|
import { ImportableRegistry } from './ImportableRegistry';
|
||||||
|
import { UncategorizedTransactionsImportable } from '../Cashflow/UncategorizedTransactionsImportable';
|
||||||
|
import { CustomersImportable } from '../Contacts/Customers/CustomersImportable';
|
||||||
|
import { VendorsImportable } from '../Contacts/Vendors/VendorsImportable';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportableResources {
|
export class ImportableResources {
|
||||||
@@ -15,6 +18,12 @@ export class ImportableResources {
|
|||||||
*/
|
*/
|
||||||
private importables = [
|
private importables = [
|
||||||
{ resource: 'Account', importable: AccountsImportable },
|
{ resource: 'Account', importable: AccountsImportable },
|
||||||
|
{
|
||||||
|
resource: 'UncategorizedCashflowTransaction',
|
||||||
|
importable: UncategorizedTransactionsImportable,
|
||||||
|
},
|
||||||
|
{ resource: 'Customer', importable: CustomersImportable },
|
||||||
|
{ resource: 'Vendor', importable: VendorsImportable },
|
||||||
];
|
];
|
||||||
|
|
||||||
public get registry() {
|
public get registry() {
|
||||||
|
|||||||
@@ -3,6 +3,17 @@ import { upperFirst, camelCase, first } from 'lodash';
|
|||||||
import pluralize from 'pluralize';
|
import pluralize from 'pluralize';
|
||||||
import { ResourceMetaFieldsMap } from './interfaces';
|
import { ResourceMetaFieldsMap } from './interfaces';
|
||||||
import { IModelMetaField } from '@/interfaces';
|
import { IModelMetaField } from '@/interfaces';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export const ERRORS = {
|
||||||
|
RESOURCE_NOT_IMPORTABLE: 'RESOURCE_NOT_IMPORTABLE',
|
||||||
|
INVALID_MAP_ATTRS: 'INVALID_MAP_ATTRS',
|
||||||
|
DUPLICATED_FROM_MAP_ATTR: 'DUPLICATED_FROM_MAP_ATTR',
|
||||||
|
DUPLICATED_TO_MAP_ATTR: 'DUPLICATED_TO_MAP_ATTR',
|
||||||
|
IMPORT_FILE_NOT_MAPPED: 'IMPORT_FILE_NOT_MAPPED',
|
||||||
|
INVALID_MAP_DATE_FORMAT: 'INVALID_MAP_DATE_FORMAT',
|
||||||
|
MAP_DATE_FORMAT_NOT_DEFINED: 'MAP_DATE_FORMAT_NOT_DEFINED',
|
||||||
|
};
|
||||||
|
|
||||||
export function trimObject(obj) {
|
export function trimObject(obj) {
|
||||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||||
@@ -47,6 +58,18 @@ export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
fieldSchema = Yup.string().oneOf(Object.keys(options)).label(field.name);
|
fieldSchema = Yup.string().oneOf(Object.keys(options)).label(field.name);
|
||||||
|
// Validate date field type.
|
||||||
|
} else if (field.fieldType === 'date') {
|
||||||
|
fieldSchema = fieldSchema.test(
|
||||||
|
'date validation',
|
||||||
|
'Invalid date or format. The string should be a valid YYYY-MM-DD format.',
|
||||||
|
(val) => {
|
||||||
|
if (!val) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return moment(val, 'YYYY-MM-DD', true).isValid();
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (field.required) {
|
if (field.required) {
|
||||||
fieldSchema = fieldSchema.required();
|
fieldSchema = fieldSchema.required();
|
||||||
@@ -56,14 +79,6 @@ export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
|
|||||||
return Yup.object().shape(yupSchema);
|
return Yup.object().shape(yupSchema);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ERRORS = {
|
|
||||||
RESOURCE_NOT_IMPORTABLE: 'RESOURCE_NOT_IMPORTABLE',
|
|
||||||
INVALID_MAP_ATTRS: 'INVALID_MAP_ATTRS',
|
|
||||||
DUPLICATED_FROM_MAP_ATTR: 'DUPLICATED_FROM_MAP_ATTR',
|
|
||||||
DUPLICATED_TO_MAP_ATTR: 'DUPLICATED_TO_MAP_ATTR',
|
|
||||||
IMPORT_FILE_NOT_MAPPED: 'IMPORT_FILE_NOT_MAPPED',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUnmappedSheetColumns = (columns, mapping) => {
|
export const getUnmappedSheetColumns = (columns, mapping) => {
|
||||||
return columns.filter(
|
return columns.filter(
|
||||||
(column) => !mapping.some((map) => map.from === column)
|
(column) => !mapping.some((map) => map.from === column)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { IModelMetaField } from '@/interfaces';
|
import { IModelMetaField } from '@/interfaces';
|
||||||
|
import Import from '@/models/Import';
|
||||||
|
|
||||||
export interface ImportMappingAttr {
|
export interface ImportMappingAttr {
|
||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
|
dateFormat?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImportValidationError {
|
export interface ImportValidationError {
|
||||||
@@ -59,3 +61,16 @@ export interface ImportOperError {
|
|||||||
error: ImportInsertError;
|
error: ImportInsertError;
|
||||||
index: number;
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImportableContext {
|
||||||
|
import: Import,
|
||||||
|
rowIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const ImportDateFormats = [
|
||||||
|
'yyyy-MM-dd',
|
||||||
|
'dd.MM.yy',
|
||||||
|
'MM/dd/yy',
|
||||||
|
'dd/MMM/yyyy'
|
||||||
|
]
|
||||||
|
|||||||
7
packages/server/src/utils/parse-json-safe.ts
Normal file
7
packages/server/src/utils/parse-json-safe.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const parseJsonSafe = (value: string) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
25
packages/webapp/src/containers/Accounts/AccountsImport.tsx
Normal file
25
packages/webapp/src/containers/Accounts/AccountsImport.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { DashboardInsider } from '@/components';
|
||||||
|
import { ImportView } from '../Import/ImportView';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default function AccountsImport() {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const handleCancelBtnClick = () => {
|
||||||
|
history.push('/accounts');
|
||||||
|
};
|
||||||
|
const handleImportSuccess = () => {
|
||||||
|
history.push('/accounts');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardInsider name={'import-accounts'}>
|
||||||
|
<ImportView
|
||||||
|
resource={'accounts'}
|
||||||
|
onCancelClick={handleCancelBtnClick}
|
||||||
|
onImportSuccess={handleImportSuccess}
|
||||||
|
/>
|
||||||
|
</DashboardInsider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
NavbarDivider,
|
NavbarDivider,
|
||||||
Alignment,
|
Alignment,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
DashboardActionsBar,
|
DashboardActionsBar,
|
||||||
@@ -48,6 +49,8 @@ function AccountTransactionsActionsBar({
|
|||||||
const addMoneyInOptions = useMemo(() => getAddMoneyInOptions(), []);
|
const addMoneyInOptions = useMemo(() => getAddMoneyInOptions(), []);
|
||||||
const addMoneyOutOptions = useMemo(() => getAddMoneyOutOptions(), []);
|
const addMoneyOutOptions = useMemo(() => getAddMoneyOutOptions(), []);
|
||||||
|
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
// Handle money in form
|
// Handle money in form
|
||||||
const handleMoneyInFormTransaction = (account) => {
|
const handleMoneyInFormTransaction = (account) => {
|
||||||
openDialog('money-in', {
|
openDialog('money-in', {
|
||||||
@@ -64,6 +67,11 @@ function AccountTransactionsActionsBar({
|
|||||||
account_name: account.name,
|
account_name: account.name,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
// Handle import button click.
|
||||||
|
const handleImportBtnClick = () => {
|
||||||
|
history.push(`/cashflow-accounts/${accountId}/import`);
|
||||||
|
};
|
||||||
|
|
||||||
// Refresh cashflow infinity transactions hook.
|
// Refresh cashflow infinity transactions hook.
|
||||||
const { refresh } = useRefreshCashflowTransactionsInfinity();
|
const { refresh } = useRefreshCashflowTransactionsInfinity();
|
||||||
|
|
||||||
@@ -106,6 +114,7 @@ function AccountTransactionsActionsBar({
|
|||||||
className={Classes.MINIMAL}
|
className={Classes.MINIMAL}
|
||||||
icon={<Icon icon="file-import-16" iconSize={16} />}
|
icon={<Icon icon="file-import-16" iconSize={16} />}
|
||||||
text={<T id={'import'} />}
|
text={<T id={'import'} />}
|
||||||
|
onClick={handleImportBtnClick}
|
||||||
/>
|
/>
|
||||||
<NavbarDivider />
|
<NavbarDivider />
|
||||||
<DashboardRowsHeightButton
|
<DashboardRowsHeightButton
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { DashboardInsider } from '@/components';
|
||||||
|
import { ImportView } from '@/containers/Import/ImportView';
|
||||||
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default function ImportUncategorizedTransactions() {
|
||||||
|
const history = useHistory();
|
||||||
|
const params = useParams();
|
||||||
|
|
||||||
|
const handleImportSuccess = () => {
|
||||||
|
history.push(
|
||||||
|
`/cashflow-accounts/${params.id}/transactions?filter=uncategorized`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const handleCnacelBtnClick = () => {
|
||||||
|
history.push(
|
||||||
|
`/cashflow-accounts/${params.id}/transactions?filter=uncategorized`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardInsider name={'import-uncategorized-bank-transactions'}>
|
||||||
|
<ImportView
|
||||||
|
resource={'uncategorized_cashflow_transaction'}
|
||||||
|
params={{ accountId: params.id }}
|
||||||
|
onImportSuccess={handleImportSuccess}
|
||||||
|
onCancelClick={handleCnacelBtnClick}
|
||||||
|
sampleFileName={'sample_bank_transactions'}
|
||||||
|
/>
|
||||||
|
</DashboardInsider>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
packages/webapp/src/containers/Customers/CustomersImport.tsx
Normal file
25
packages/webapp/src/containers/Customers/CustomersImport.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { DashboardInsider } from '@/components';
|
||||||
|
import { ImportView } from '../Import/ImportView';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default function CustomersImport() {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const handleImportSuccess = () => {
|
||||||
|
history.push('/customers');
|
||||||
|
};
|
||||||
|
const handleCancelBtnClick = () => {
|
||||||
|
history.push('/customers');
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<DashboardInsider name={'import-customers'}>
|
||||||
|
<ImportView
|
||||||
|
resource={'customers'}
|
||||||
|
onImportSuccess={handleImportSuccess}
|
||||||
|
onCancelClick={handleCancelBtnClick}
|
||||||
|
exampleTitle="Customers Example"
|
||||||
|
/>
|
||||||
|
</DashboardInsider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -95,6 +95,11 @@ function CustomerActionsBar({
|
|||||||
addSetting('customers', 'tableSize', size);
|
addSetting('customers', 'tableSize', size);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle import button click.
|
||||||
|
const handleImportBtnClick = () => {
|
||||||
|
history.push('/customers/import');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardActionsBar>
|
<DashboardActionsBar>
|
||||||
<NavbarGroup>
|
<NavbarGroup>
|
||||||
@@ -142,6 +147,7 @@ function CustomerActionsBar({
|
|||||||
<Button
|
<Button
|
||||||
className={Classes.MINIMAL}
|
className={Classes.MINIMAL}
|
||||||
icon={<Icon icon="file-import-16" iconSize={16} />}
|
icon={<Icon icon="file-import-16" iconSize={16} />}
|
||||||
|
onClick={handleImportBtnClick}
|
||||||
text={<T id={'import'} />}
|
text={<T id={'import'} />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import { useImportFileContext } from './ImportFileProvider';
|
|||||||
|
|
||||||
export function ImportFileUploadFooterActions() {
|
export function ImportFileUploadFooterActions() {
|
||||||
const { isSubmitting } = useFormikContext();
|
const { isSubmitting } = useFormikContext();
|
||||||
const { setStep } = useImportFileContext();
|
const { onCancelClick } = useImportFileContext();
|
||||||
|
|
||||||
const handleCancelBtnClick = () => {};
|
const handleCancelBtnClick = () => {
|
||||||
|
onCancelClick && onCancelClick();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(CLASSES.PAGE_FORM_FLOATING_ACTIONS, styles.root)}>
|
<div className={clsx(CLASSES.PAGE_FORM_FLOATING_ACTIONS, styles.root)}>
|
||||||
|
|||||||
@@ -7,33 +7,38 @@ import { ImportFileMappingForm } from './ImportFileMappingForm';
|
|||||||
import { EntityColumn, useImportFileContext } from './ImportFileProvider';
|
import { EntityColumn, useImportFileContext } from './ImportFileProvider';
|
||||||
import { CLASSES } from '@/constants';
|
import { CLASSES } from '@/constants';
|
||||||
import { ImportFileContainer } from './ImportFileContainer';
|
import { ImportFileContainer } from './ImportFileContainer';
|
||||||
import styles from './ImportFileMapping.module.scss';
|
|
||||||
import { ImportStepperStep } from './_types';
|
import { ImportStepperStep } from './_types';
|
||||||
|
import { ImportFileMapBootProvider } from './ImportFileMappingBoot';
|
||||||
|
import styles from './ImportFileMapping.module.scss';
|
||||||
|
|
||||||
export function ImportFileMapping() {
|
export function ImportFileMapping() {
|
||||||
|
const { importId } = useImportFileContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImportFileMappingForm>
|
<ImportFileMapBootProvider importId={importId}>
|
||||||
<ImportFileContainer>
|
<ImportFileMappingForm>
|
||||||
<p>
|
<ImportFileContainer>
|
||||||
Review and map the column headers in your csv/xlsx file with the
|
<p>
|
||||||
Bigcapital fields.
|
Review and map the column headers in your csv/xlsx file with the
|
||||||
</p>
|
Bigcapital fields.
|
||||||
|
</p>
|
||||||
|
|
||||||
<table className={clsx('bp4-html-table', styles.table)}>
|
<table className={clsx('bp4-html-table', styles.table)}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className={styles.label}>Bigcapital Fields</th>
|
<th className={styles.label}>Bigcapital Fields</th>
|
||||||
<th className={styles.field}>Sheet Column Headers</th>
|
<th className={styles.field}>Sheet Column Headers</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<ImportFileMappingFields />
|
<ImportFileMappingFields />
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</ImportFileContainer>
|
</ImportFileContainer>
|
||||||
|
|
||||||
<ImportFileMappingFloatingActions />
|
<ImportFileMappingFloatingActions />
|
||||||
</ImportFileMappingForm>
|
</ImportFileMappingForm>
|
||||||
|
</ImportFileMapBootProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +72,7 @@ function ImportFileMappingFields() {
|
|||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
const columns = entityColumns.map(columnMapper);
|
const columns = entityColumns.map(columnMapper);
|
||||||
|
|
||||||
return <>{columns}</>;
|
return <>{columns}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +87,7 @@ function ImportFileMappingFloatingActions() {
|
|||||||
return (
|
return (
|
||||||
<div className={clsx(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
|
<div className={clsx(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
|
||||||
<Group spacing={10}>
|
<Group spacing={10}>
|
||||||
<Button onClick={handleCancelBtnClick}>Cancel</Button>
|
<Button onClick={handleCancelBtnClick}>Back</Button>
|
||||||
<Button type="submit" intent={Intent.PRIMARY} loading={isSubmitting}>
|
<Button type="submit" intent={Intent.PRIMARY} loading={isSubmitting}>
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Spinner } from '@blueprintjs/core';
|
||||||
|
import React, { createContext, useContext } from 'react';
|
||||||
|
import { Box } from '@/components';
|
||||||
|
import { useImportFileMeta } from '@/hooks/query/import';
|
||||||
|
|
||||||
|
interface ImportFileMapBootContextValue {}
|
||||||
|
|
||||||
|
const ImportFileMapBootContext = createContext<ImportFileMapBootContextValue>(
|
||||||
|
{} as ImportFileMapBootContextValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useImportFileMapBootContext = () => {
|
||||||
|
const context = useContext<ImportFileMapBootContextValue>(
|
||||||
|
ImportFileMapBootContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useImportFileMapBootContext must be used within an ImportFileMapBootProvider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ImportFileMapBootProps {
|
||||||
|
importId: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImportFileMapBootProvider = ({
|
||||||
|
importId,
|
||||||
|
children,
|
||||||
|
}: ImportFileMapBootProps) => {
|
||||||
|
const {
|
||||||
|
data: importFile,
|
||||||
|
isLoading: isImportFileLoading,
|
||||||
|
isFetching: isImportFileFetching,
|
||||||
|
} = useImportFileMeta(importId, {
|
||||||
|
enabled: Boolean(importId),
|
||||||
|
});
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
importFile,
|
||||||
|
isImportFileLoading,
|
||||||
|
isImportFileFetching,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<ImportFileMapBootContext.Provider value={value}>
|
||||||
|
{isImportFileLoading ? (
|
||||||
|
<Box style={{ padding: '2rem', textAlign: 'center' }}>
|
||||||
|
<Spinner size={26} />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<>{children}</>
|
||||||
|
)}
|
||||||
|
</ImportFileMapBootContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,6 +6,8 @@ import { useImportFileContext } from './ImportFileProvider';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { isEmpty, lowerCase } from 'lodash';
|
import { isEmpty, lowerCase } from 'lodash';
|
||||||
import { AppToaster } from '@/components';
|
import { AppToaster } from '@/components';
|
||||||
|
import { useImportFileMapBootContext } from './ImportFileMappingBoot';
|
||||||
|
import { transformToForm } from '@/utils';
|
||||||
|
|
||||||
interface ImportFileMappingFormProps {
|
interface ImportFileMappingFormProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -58,10 +60,23 @@ const transformValueToReq = (value: ImportFileMappingFormValues) => {
|
|||||||
return { mapping };
|
return { mapping };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const transformResToFormValues = (value: { from: string; to: string }[]) => {
|
||||||
|
return value?.reduce((acc, map) => {
|
||||||
|
acc[map.to] = map.from;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
const useImportFileMappingInitialValues = () => {
|
const useImportFileMappingInitialValues = () => {
|
||||||
|
const { importFile } = useImportFileMapBootContext();
|
||||||
const { entityColumns, sheetColumns } = useImportFileContext();
|
const { entityColumns, sheetColumns } = useImportFileContext();
|
||||||
|
|
||||||
return useMemo(
|
const initialResValues = useMemo(
|
||||||
|
() => transformResToFormValues(importFile?.map || []),
|
||||||
|
[importFile?.map],
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialValues = useMemo(
|
||||||
() =>
|
() =>
|
||||||
entityColumns.reduce((acc, { key, name }) => {
|
entityColumns.reduce((acc, { key, name }) => {
|
||||||
const _name = lowerCase(name);
|
const _name = lowerCase(name);
|
||||||
@@ -75,4 +90,12 @@ const useImportFileMappingInitialValues = () => {
|
|||||||
}, {}),
|
}, {}),
|
||||||
[entityColumns, sheetColumns],
|
[entityColumns, sheetColumns],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
...transformToForm(initialResValues, initialValues),
|
||||||
|
...initialValues,
|
||||||
|
}),
|
||||||
|
[initialValues, initialResValues],
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { Button, Callout, Intent, Text } from '@blueprintjs/core';
|
import { Button, Callout, Intent, Text } from '@blueprintjs/core';
|
||||||
import clsx from 'classnames';
|
import clsx from 'classnames';
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
import {
|
import {
|
||||||
ImportFilePreviewBootProvider,
|
ImportFilePreviewBootProvider,
|
||||||
useImportFilePreviewBootContext,
|
useImportFilePreviewBootContext,
|
||||||
@@ -144,12 +143,12 @@ function ImportFilePreviewUnmapped() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ImportFilePreviewFloatingActions() {
|
function ImportFilePreviewFloatingActions() {
|
||||||
const { importId, setStep } = useImportFileContext();
|
const { importId, setStep, onImportSuccess, onImportFailed } =
|
||||||
|
useImportFileContext();
|
||||||
const { importPreview } = useImportFilePreviewBootContext();
|
const { importPreview } = useImportFilePreviewBootContext();
|
||||||
const { mutateAsync: importFile, isLoading: isImportFileLoading } =
|
const { mutateAsync: importFile, isLoading: isImportFileLoading } =
|
||||||
useImportFileProcess();
|
useImportFileProcess();
|
||||||
|
|
||||||
const history = useHistory();
|
|
||||||
const isValidToImport = importPreview?.createdCount > 0;
|
const isValidToImport = importPreview?.createdCount > 0;
|
||||||
|
|
||||||
const handleSubmitBtn = () => {
|
const handleSubmitBtn = () => {
|
||||||
@@ -161,9 +160,11 @@ function ImportFilePreviewFloatingActions() {
|
|||||||
importPreview.createdCount
|
importPreview.createdCount
|
||||||
} of ${10} has imported successfully.`,
|
} of ${10} has imported successfully.`,
|
||||||
});
|
});
|
||||||
history.push('/accounts');
|
onImportSuccess && onImportSuccess();
|
||||||
})
|
})
|
||||||
.catch((error) => {});
|
.catch((error) => {
|
||||||
|
onImportFailed && onImportFailed();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const handleCancelBtnClick = () => {
|
const handleCancelBtnClick = () => {
|
||||||
setStep(ImportStepperStep.Mapping);
|
setStep(ImportStepperStep.Mapping);
|
||||||
|
|||||||
@@ -33,12 +33,36 @@ interface ImportFileContextValue {
|
|||||||
setImportId: Dispatch<SetStateAction<string>>;
|
setImportId: Dispatch<SetStateAction<string>>;
|
||||||
|
|
||||||
resource: string;
|
resource: string;
|
||||||
|
description?: string;
|
||||||
|
params: Record<string, any>;
|
||||||
|
onImportSuccess?: () => void;
|
||||||
|
onImportFailed?: () => void;
|
||||||
|
onCancelClick?: () => void;
|
||||||
|
sampleFileName?: string;
|
||||||
|
|
||||||
|
exampleDownload?: boolean;
|
||||||
|
exampleTitle?: string;
|
||||||
|
exampleDescription?: string;
|
||||||
}
|
}
|
||||||
interface ImportFileProviderProps {
|
interface ImportFileProviderProps {
|
||||||
resource: string;
|
resource: string;
|
||||||
|
description?: string;
|
||||||
|
params: Record<string, any>;
|
||||||
|
onImportSuccess?: () => void;
|
||||||
|
onImportFailed?: () => void;
|
||||||
|
onCancelClick?: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
sampleFileName?: string;
|
||||||
|
|
||||||
|
exampleDownload?: boolean;
|
||||||
|
exampleTitle?: string;
|
||||||
|
exampleDescription?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ExampleDescription =
|
||||||
|
'You can download the sample file to obtain detailed information about the data fields used during the import.';
|
||||||
|
const ExampleTitle = 'Table Example';
|
||||||
|
|
||||||
const ImportFileContext = createContext<ImportFileContextValue>(
|
const ImportFileContext = createContext<ImportFileContextValue>(
|
||||||
{} as ImportFileContextValue,
|
{} as ImportFileContextValue,
|
||||||
);
|
);
|
||||||
@@ -57,6 +81,16 @@ export const useImportFileContext = () => {
|
|||||||
export const ImportFileProvider = ({
|
export const ImportFileProvider = ({
|
||||||
resource,
|
resource,
|
||||||
children,
|
children,
|
||||||
|
description,
|
||||||
|
params,
|
||||||
|
onImportFailed,
|
||||||
|
onImportSuccess,
|
||||||
|
onCancelClick,
|
||||||
|
sampleFileName,
|
||||||
|
|
||||||
|
exampleDownload = true,
|
||||||
|
exampleTitle = ExampleTitle,
|
||||||
|
exampleDescription = ExampleDescription,
|
||||||
}: ImportFileProviderProps) => {
|
}: ImportFileProviderProps) => {
|
||||||
const [sheetColumns, setSheetColumns] = useState<SheetColumn[]>([]);
|
const [sheetColumns, setSheetColumns] = useState<SheetColumn[]>([]);
|
||||||
const [entityColumns, setEntityColumns] = useState<SheetColumn[]>([]);
|
const [entityColumns, setEntityColumns] = useState<SheetColumn[]>([]);
|
||||||
@@ -82,6 +116,18 @@ export const ImportFileProvider = ({
|
|||||||
setImportId,
|
setImportId,
|
||||||
|
|
||||||
resource,
|
resource,
|
||||||
|
description,
|
||||||
|
params,
|
||||||
|
|
||||||
|
onImportSuccess,
|
||||||
|
onImportFailed,
|
||||||
|
onCancelClick,
|
||||||
|
|
||||||
|
sampleFileName,
|
||||||
|
|
||||||
|
exampleDownload,
|
||||||
|
exampleTitle,
|
||||||
|
exampleDescription,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function ImportFileUploadForm({
|
|||||||
formProps,
|
formProps,
|
||||||
}: ImportFileUploadFormProps) {
|
}: ImportFileUploadFormProps) {
|
||||||
const { mutateAsync: uploadImportFile } = useImportFileUpload();
|
const { mutateAsync: uploadImportFile } = useImportFileUpload();
|
||||||
const { setStep, setSheetColumns, setEntityColumns, setImportId } =
|
const { resource, params, setStep, setSheetColumns, setEntityColumns, setImportId } =
|
||||||
useImportFileContext();
|
useImportFileContext();
|
||||||
|
|
||||||
const handleSubmit = (
|
const handleSubmit = (
|
||||||
@@ -41,7 +41,8 @@ export function ImportFileUploadForm({
|
|||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', values.file);
|
formData.append('file', values.file);
|
||||||
formData.append('resource', 'Account');
|
formData.append('resource', resource);
|
||||||
|
formData.append('params', JSON.stringify(params));
|
||||||
|
|
||||||
uploadImportFile(formData)
|
uploadImportFile(formData)
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
import { Classes } from '@blueprintjs/core';
|
||||||
import { Stack } from '@/components';
|
import { Stack } from '@/components';
|
||||||
import { ImportDropzone } from './ImportDropzone';
|
import { ImportDropzone } from './ImportDropzone';
|
||||||
import { ImportSampleDownload } from './ImportSampleDownload';
|
import { ImportSampleDownload } from './ImportSampleDownload';
|
||||||
import { ImportFileUploadForm } from './ImportFileUploadForm';
|
import { ImportFileUploadForm } from './ImportFileUploadForm';
|
||||||
import { ImportFileUploadFooterActions } from './ImportFileFooterActions';
|
import { ImportFileUploadFooterActions } from './ImportFileFooterActions';
|
||||||
import { ImportFileContainer } from './ImportFileContainer';
|
import { ImportFileContainer } from './ImportFileContainer';
|
||||||
|
import { useImportFileContext } from './ImportFileProvider';
|
||||||
|
|
||||||
export function ImportFileUploadStep() {
|
export function ImportFileUploadStep() {
|
||||||
|
const { exampleDownload } = useImportFileContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImportFileUploadForm>
|
<ImportFileUploadForm>
|
||||||
<ImportFileContainer>
|
<ImportFileContainer>
|
||||||
<p style={{ marginBottom: 18 }}>
|
<p
|
||||||
Download a sample file and compare it to your import file to ensure
|
className={Classes.TEXT_MUTED}
|
||||||
you have the file perfect for the import.
|
style={{ marginBottom: 18, lineHeight: 1.6 }}
|
||||||
|
>
|
||||||
|
Download a sample file and compare it with your import file to ensure
|
||||||
|
it is properly formatted. It's not necessary for the columns to be in
|
||||||
|
the same order, you can map them later.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Stack spacing={40}>
|
<Stack spacing={40}>
|
||||||
<ImportDropzone />
|
<ImportDropzone />
|
||||||
<ImportSampleDownload />
|
{exampleDownload && <ImportSampleDownload />}
|
||||||
</Stack>
|
</Stack>
|
||||||
</ImportFileContainer>
|
</ImportFileContainer>
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import { ImportStepper } from './ImportStepper';
|
|
||||||
import { Box, DashboardInsider } from '@/components';
|
|
||||||
import { ImportFileProvider } from './ImportFileProvider';
|
|
||||||
import styles from './ImportPage.module.scss';
|
|
||||||
|
|
||||||
|
|
||||||
export default function ImportPage() {
|
|
||||||
return (
|
|
||||||
<DashboardInsider>
|
|
||||||
<Box className={styles.root}>
|
|
||||||
<ImportFileProvider resource="account">
|
|
||||||
<ImportStepper />
|
|
||||||
</ImportFileProvider>
|
|
||||||
</Box>
|
|
||||||
</DashboardInsider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,64 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { Box, Group } from '@/components';
|
import { AppToaster, Box, Group } from '@/components';
|
||||||
import { Button } from '@blueprintjs/core';
|
import {
|
||||||
|
Button,
|
||||||
|
Intent,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Popover,
|
||||||
|
PopoverInteractionKind,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
import styles from './ImportSampleDownload.module.scss';
|
import styles from './ImportSampleDownload.module.scss';
|
||||||
|
import { useSampleSheetImport } from '@/hooks/query/import';
|
||||||
|
import { useImportFileContext } from './ImportFileProvider';
|
||||||
|
|
||||||
export function ImportSampleDownload() {
|
export function ImportSampleDownload() {
|
||||||
|
const { resource, sampleFileName, exampleTitle, exampleDescription } =
|
||||||
|
useImportFileContext();
|
||||||
|
const { mutateAsync: downloadSample } = useSampleSheetImport();
|
||||||
|
|
||||||
|
// Handle download button click.
|
||||||
|
const handleDownloadBtnClick = (format) => () => {
|
||||||
|
downloadSample({
|
||||||
|
filename: sampleFileName || `sample-${resource}`,
|
||||||
|
resource,
|
||||||
|
format: format,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
message: 'The sample sheet has been downloaded successfully.',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group className={styles.root} noWrap>
|
<Group className={styles.root} noWrap>
|
||||||
<Box>
|
<Box>
|
||||||
<h3 className={styles.title}>Table Example</h3>
|
<h3 className={styles.title}>{exampleTitle}</h3>
|
||||||
<p className={styles.description}>
|
<p className={styles.description}>{exampleDescription}</p>
|
||||||
Download a sample file and compare it to your import file to ensure
|
|
||||||
you have the file perfect for the import.
|
|
||||||
</p>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box className={styles.buttonWrap}>
|
<Box className={styles.buttonWrap}>
|
||||||
<Button minimal outlined>
|
<Popover
|
||||||
Download File
|
content={
|
||||||
</Button>
|
<Menu>
|
||||||
|
<MenuItem onClick={handleDownloadBtnClick('csv')} text={'CSV'} />
|
||||||
|
<MenuItem
|
||||||
|
onClick={handleDownloadBtnClick('xlsx')}
|
||||||
|
text={'XLSX'}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
|
interactionKind={PopoverInteractionKind.CLICK}
|
||||||
|
placement="bottom-start"
|
||||||
|
minimal
|
||||||
|
>
|
||||||
|
<Button minimal outlined>
|
||||||
|
Download File
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
</Box>
|
</Box>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
|||||||
28
packages/webapp/src/containers/Import/ImportView.tsx
Normal file
28
packages/webapp/src/containers/Import/ImportView.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { ImportStepper } from './ImportStepper';
|
||||||
|
import { Box } from '@/components';
|
||||||
|
import { ImportFileProvider } from './ImportFileProvider';
|
||||||
|
import styles from './ImportView.module.scss';
|
||||||
|
|
||||||
|
interface ImportViewProps {
|
||||||
|
resource: string;
|
||||||
|
description?: string;
|
||||||
|
params: Record<string, any>;
|
||||||
|
onImportSuccess?: () => void;
|
||||||
|
onImportFailed?: () => void;
|
||||||
|
onCancelClick?: () => void;
|
||||||
|
sampleFileName?: string;
|
||||||
|
exampleDownload?: boolean;
|
||||||
|
exampleTitle?: string;
|
||||||
|
exampleDescription?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImportView({ ...props }: ImportViewProps) {
|
||||||
|
return (
|
||||||
|
<Box className={styles.root}>
|
||||||
|
<ImportFileProvider {...props}>
|
||||||
|
<ImportStepper />
|
||||||
|
</ImportFileProvider>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
packages/webapp/src/containers/Import/index.ts
Normal file
1
packages/webapp/src/containers/Import/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './ImportView';
|
||||||
11
packages/webapp/src/containers/Items/ItemsImportable.tsx
Normal file
11
packages/webapp/src/containers/Items/ItemsImportable.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { DashboardInsider } from '@/components';
|
||||||
|
import { ImportView } from '../Import/ImportView';
|
||||||
|
|
||||||
|
export default function ItemsImport() {
|
||||||
|
return (
|
||||||
|
<DashboardInsider name={'import-items'}>
|
||||||
|
<ImportView resource={'items'} />
|
||||||
|
</DashboardInsider>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
packages/webapp/src/containers/Vendors/VendorsImport.tsx
Normal file
26
packages/webapp/src/containers/Vendors/VendorsImport.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { DashboardInsider } from '@/components';
|
||||||
|
import { ImportView } from '../Import/ImportView';
|
||||||
|
|
||||||
|
export default function VendorsImport() {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const handleImportSuccess = () => {
|
||||||
|
history.push('/vendors');
|
||||||
|
};
|
||||||
|
const handleImportBtnClick = () => {
|
||||||
|
history.push('/vendors');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardInsider name={'import-vendors'}>
|
||||||
|
<ImportView
|
||||||
|
resource={'vendors'}
|
||||||
|
onImportSuccess={handleImportSuccess}
|
||||||
|
onCancelClick={handleImportBtnClick}
|
||||||
|
exampleTitle='Vendors Example'
|
||||||
|
/>
|
||||||
|
</DashboardInsider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -83,6 +83,10 @@ function VendorActionsBar({
|
|||||||
const handleTableRowSizeChange = (size) => {
|
const handleTableRowSizeChange = (size) => {
|
||||||
addSetting('vendors', 'tableSize', size);
|
addSetting('vendors', 'tableSize', size);
|
||||||
};
|
};
|
||||||
|
// Handle import button success.
|
||||||
|
const handleImportBtnSuccess = () => {
|
||||||
|
history.push('/vendors/import');
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<DashboardActionsBar>
|
<DashboardActionsBar>
|
||||||
<NavbarGroup>
|
<NavbarGroup>
|
||||||
@@ -128,6 +132,7 @@ function VendorActionsBar({
|
|||||||
className={Classes.MINIMAL}
|
className={Classes.MINIMAL}
|
||||||
icon={<Icon icon="file-import-16" iconSize={16} />}
|
icon={<Icon icon="file-import-16" iconSize={16} />}
|
||||||
text={<T id={'import'} />}
|
text={<T id={'import'} />}
|
||||||
|
onClick={handleImportBtnSuccess}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className={Classes.MINIMAL}
|
className={Classes.MINIMAL}
|
||||||
|
|||||||
@@ -2,7 +2,12 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from 'react-query';
|
import { useMutation, useQuery, useQueryClient } from 'react-query';
|
||||||
import useApiRequest from '../useRequest';
|
import useApiRequest from '../useRequest';
|
||||||
import { transformToCamelCase } from '@/utils';
|
import { transformToCamelCase } from '@/utils';
|
||||||
|
import { downloadFile, useDownloadFile } from '../useDownloadFile';
|
||||||
|
|
||||||
|
const QueryKeys = {
|
||||||
|
ImportPreview: 'ImportPreview',
|
||||||
|
ImportFileMeta: 'ImportFileMeta',
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@@ -28,22 +33,36 @@ export function useImportFileMapping(props = {}) {
|
|||||||
{
|
{
|
||||||
onSuccess: (res, id) => {
|
onSuccess: (res, id) => {
|
||||||
// Invalidate queries.
|
// Invalidate queries.
|
||||||
|
queryClient.invalidateQueries([QueryKeys.ImportPreview]);
|
||||||
|
queryClient.invalidateQueries([QueryKeys.ImportFileMeta]);
|
||||||
},
|
},
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useImportFilePreview(importId: string, props = {}) {
|
export function useImportFilePreview(importId: string, props = {}) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const apiRequest = useApiRequest();
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
return useQuery(['importPreview', importId], () =>
|
return useQuery([QueryKeys.ImportPreview, importId], () =>
|
||||||
apiRequest
|
apiRequest
|
||||||
.get(`import/${importId}/preview`)
|
.get(`import/${importId}/preview`)
|
||||||
.then((res) => transformToCamelCase(res.data)),
|
.then((res) => transformToCamelCase(res.data)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useImportFileMeta(importId: string, props = {}) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useQuery([QueryKeys.ImportFileMeta, importId], () =>
|
||||||
|
apiRequest
|
||||||
|
.get(`import/${importId}`)
|
||||||
|
.then((res) => transformToCamelCase(res.data)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@@ -61,3 +80,40 @@ export function useImportFileProcess(props = {}) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SampleSheetImportQuery {
|
||||||
|
resource: string;
|
||||||
|
filename: string;
|
||||||
|
format: 'xlsx' | 'csv';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates a download of the balance sheet in XLSX format.
|
||||||
|
* @param {Object} query - The query parameters for the request.
|
||||||
|
* @param {Object} args - Additional configurations for the download.
|
||||||
|
* @returns {Function} A function to trigger the file download.
|
||||||
|
*/
|
||||||
|
export const useSampleSheetImport = () => {
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useMutation<void, AxiosError, IArgs>(
|
||||||
|
(data: SampleSheetImportQuery) => {
|
||||||
|
return apiRequest
|
||||||
|
.get('/import/sample', {
|
||||||
|
responseType: 'blob',
|
||||||
|
headers: {
|
||||||
|
accept:
|
||||||
|
data.format === 'xlsx' ? 'application/xlsx' : 'application/csv',
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
resource: data.resource,
|
||||||
|
format: data.format,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
downloadFile(res.data, `${data.filename}.${data.format}`);
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
|
|||||||
const SUBSCRIPTION_TYPE = {
|
const SUBSCRIPTION_TYPE = {
|
||||||
MAIN: 'main',
|
MAIN: 'main',
|
||||||
};
|
};
|
||||||
// const BASE_URL = '/dashboard';
|
|
||||||
|
|
||||||
export const getDashboardRoutes = () => [
|
export const getDashboardRoutes = () => [
|
||||||
// Accounts.
|
// Accounts.
|
||||||
{
|
{
|
||||||
path: '/accounts/import',
|
path: '/accounts/import',
|
||||||
component: lazy(() => import('@/containers/Import/ImportPage')),
|
component: lazy(() => import('@/containers/Accounts/AccountsImport')),
|
||||||
breadcrumb: 'Accounts Import',
|
breadcrumb: 'Accounts Import',
|
||||||
pageTitle: 'Accounts Import',
|
pageTitle: 'Accounts Import',
|
||||||
},
|
},
|
||||||
@@ -77,6 +75,7 @@ export const getDashboardRoutes = () => [
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Items.
|
// Items.
|
||||||
|
|
||||||
{
|
{
|
||||||
path: `/items/:id/edit`,
|
path: `/items/:id/edit`,
|
||||||
component: lazy(() => import('@/containers/Items/ItemFormPage')),
|
component: lazy(() => import('@/containers/Items/ItemFormPage')),
|
||||||
@@ -512,8 +511,20 @@ export const getDashboardRoutes = () => [
|
|||||||
hotkey: 'shift+x',
|
hotkey: 'shift+x',
|
||||||
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Customers
|
// Customers
|
||||||
|
{
|
||||||
|
path: `/customers/import`,
|
||||||
|
component: lazy(
|
||||||
|
() =>
|
||||||
|
import(
|
||||||
|
'@/containers/Customers/CustomersImport'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backLink: true,
|
||||||
|
pageTitle: 'Customers Import',
|
||||||
|
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
||||||
|
defaultSearchResource: RESOURCES_TYPES.CUSTOMER,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: `/customers/:id/edit`,
|
path: `/customers/:id/edit`,
|
||||||
component: lazy(
|
component: lazy(
|
||||||
@@ -564,6 +575,16 @@ export const getDashboardRoutes = () => [
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Vendors
|
// Vendors
|
||||||
|
{
|
||||||
|
path: `/vendors/import`,
|
||||||
|
component: lazy(
|
||||||
|
() => import('@/containers/Vendors/VendorsImport'),
|
||||||
|
),
|
||||||
|
backLink: true,
|
||||||
|
pageTitle: 'Vendors Import',
|
||||||
|
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
||||||
|
defaultSearchResource: RESOURCES_TYPES.VENDOR,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: `/vendors/:id/edit`,
|
path: `/vendors/:id/edit`,
|
||||||
component: lazy(
|
component: lazy(
|
||||||
@@ -1031,6 +1052,19 @@ export const getDashboardRoutes = () => [
|
|||||||
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
||||||
defaultSearchResource: RESOURCES_TYPES.ACCOUNT,
|
defaultSearchResource: RESOURCES_TYPES.ACCOUNT,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: `/cashflow-accounts/:id/import`,
|
||||||
|
component: lazy(
|
||||||
|
() =>
|
||||||
|
import(
|
||||||
|
'@/containers/CashFlow/ImportIUncategorizedTransactions/ImportUncategorizedTransactionsPage'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backLink: true,
|
||||||
|
pageTitle: 'Bank Transactions Import',
|
||||||
|
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
||||||
|
defaultSearchResource: RESOURCES_TYPES.ACCOUNT,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: `/cashflow-accounts`,
|
path: `/cashflow-accounts`,
|
||||||
component: lazy(
|
component: lazy(
|
||||||
|
|||||||
Reference in New Issue
Block a user