GWM : favicon upload , gsitn update in company , excell export api error fixed
This commit is contained in:
parent
969647333f
commit
710b0b33c6
@ -309,9 +309,10 @@ Each sub-master supports: `GET /` (list), `GET /:id`, `POST /`, `PUT /:id`, `DEL
|
||||
|
||||
| Status | Method | Endpoint | RBAC | Notes |
|
||||
|--------|--------|----------|------|-------|
|
||||
| [x] | GET | `/settings/company` | view | Company profile (org name, GSTIN, contact, address, logo URL) |
|
||||
| [x] | GET | `/settings/company` | view | Company profile (org name, GSTIN, contact, address, logo/favicon URLs) |
|
||||
| [x] | PUT | `/settings/company` | edit | Update company profile |
|
||||
| [x] | POST | `/settings/company/logo` | edit | Upload logo (`multipart/form-data`, field `logo`) |
|
||||
| [x] | POST | `/settings/company/favicon` | edit | Upload favicon (`multipart/form-data`, field `favicon`) |
|
||||
| [x] | GET | `/settings/email` | view | SMTP settings (`has_smtp_password` flag; password never returned) |
|
||||
| [x] | PUT | `/settings/email` | edit | Update SMTP settings (password encrypted at rest) |
|
||||
|
||||
@ -319,6 +320,8 @@ Each sub-master supports: `GET /` (list), `GET /:id`, `POST /`, `PUT /:id`, `DEL
|
||||
|
||||
**DB patch:** `scripts/patch-company-gstin.sql` — adds `company.gstin` for PDF document headers.
|
||||
|
||||
**DB patch:** `scripts/patch-company-favicon.sql` — adds `company.favicon_path`.
|
||||
|
||||
---
|
||||
|
||||
### Audit Logs (`/audit-logs`) — module: `AUDIT_LOGS`
|
||||
|
||||
@ -1002,21 +1002,22 @@ model vendors {
|
||||
}
|
||||
|
||||
model company {
|
||||
id BigInt @id @default(1)
|
||||
org_name String? @db.VarChar(200)
|
||||
gstin String? @db.VarChar(15)
|
||||
mobile String? @db.VarChar(20)
|
||||
email String? @db.VarChar(200)
|
||||
website String? @db.VarChar(255)
|
||||
logo_path String? @db.VarChar(500)
|
||||
address String?
|
||||
city String? @db.VarChar(100)
|
||||
state String? @db.VarChar(100)
|
||||
pincode String? @db.VarChar(10)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
id BigInt @id @default(1)
|
||||
org_name String? @db.VarChar(200)
|
||||
gstin String? @db.VarChar(15)
|
||||
mobile String? @db.VarChar(20)
|
||||
email String? @db.VarChar(200)
|
||||
website String? @db.VarChar(255)
|
||||
logo_path String? @db.VarChar(500)
|
||||
favicon_path String? @db.VarChar(500)
|
||||
address String?
|
||||
city String? @db.VarChar(100)
|
||||
state String? @db.VarChar(100)
|
||||
pincode String? @db.VarChar(10)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
}
|
||||
|
||||
model email_settings {
|
||||
|
||||
@ -8,6 +8,7 @@ CREATE TABLE IF NOT EXISTS company (
|
||||
email VARCHAR(200),
|
||||
website VARCHAR(255),
|
||||
logo_path VARCHAR(500),
|
||||
favicon_path VARCHAR(500),
|
||||
address TEXT,
|
||||
city VARCHAR(100),
|
||||
state VARCHAR(100),
|
||||
|
||||
4
scripts/patch-company-favicon.sql
Normal file
4
scripts/patch-company-favicon.sql
Normal file
@ -0,0 +1,4 @@
|
||||
-- Add favicon_path to company profile (browser tab / app icon)
|
||||
|
||||
ALTER TABLE company
|
||||
ADD COLUMN IF NOT EXISTS favicon_path VARCHAR(500);
|
||||
@ -67,6 +67,26 @@ paths:
|
||||
"200":
|
||||
description: Logo updated
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
/settings/company/favicon:
|
||||
post:
|
||||
tags: [Settings]
|
||||
summary: Upload company favicon
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
required: [favicon]
|
||||
properties:
|
||||
favicon:
|
||||
type: string
|
||||
format: binary
|
||||
description: JPEG, PNG, or WebP image
|
||||
responses:
|
||||
"200":
|
||||
description: Favicon updated
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
/settings/email:
|
||||
get:
|
||||
tags: [Settings]
|
||||
|
||||
@ -2,6 +2,10 @@ const prisma = require('../../../config/prisma');
|
||||
const ApiError = require('../../../utils/ApiError');
|
||||
const auditLog = require('../../../utils/auditLog');
|
||||
const { getPagination } = require('../../../utils/pagination');
|
||||
const { parseId } = require('../../../utils/parseId');
|
||||
const { rowsToCsv } = require('../../../utils/csv');
|
||||
|
||||
const AUDIT_FIELD_NAMES = new Set(['created_by', 'updated_by']);
|
||||
|
||||
const normalizePayload = (payload, fields) => {
|
||||
const out = { ...payload };
|
||||
@ -22,6 +26,17 @@ const normalizePayload = (payload, fields) => {
|
||||
return out;
|
||||
};
|
||||
|
||||
const defaultExportColumns = (fields, columns) => {
|
||||
if (columns && columns.length) return columns;
|
||||
return [
|
||||
{ key: 'id', header: 'ID' },
|
||||
...fields
|
||||
.filter((f) => !AUDIT_FIELD_NAMES.has(f.name))
|
||||
.map((f) => ({ key: f.name, header: f.name })),
|
||||
{ key: 'created_at', header: 'Created At' },
|
||||
];
|
||||
};
|
||||
|
||||
const buildMasterService = ({
|
||||
modelName,
|
||||
tableName,
|
||||
@ -30,9 +45,36 @@ const buildMasterService = ({
|
||||
softDelete = true,
|
||||
include = undefined,
|
||||
mapRow = (row) => row,
|
||||
exportColumns = null,
|
||||
}) => {
|
||||
const model = prisma[modelName];
|
||||
const withInclude = include ? { include } : {};
|
||||
const csvColumns = defaultExportColumns(fields, exportColumns);
|
||||
|
||||
const buildListWhere = (query) => {
|
||||
const searchFields = fields.filter((f) => f.searchable).map((f) => f.name);
|
||||
|
||||
const where = {
|
||||
...(softDelete ? { deleted_at: null } : {}),
|
||||
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
||||
...(query.search && searchFields.length
|
||||
? {
|
||||
OR: searchFields.map((name) => ({
|
||||
[name]: { contains: query.search, mode: 'insensitive' },
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
for (const f of fields) {
|
||||
if (!f.filterable || query[f.name] === undefined || query[f.name] === null || query[f.name] === '') {
|
||||
continue;
|
||||
}
|
||||
where[f.name] = f.type === 'int' ? BigInt(query[f.name]) : query[f.name];
|
||||
}
|
||||
|
||||
return where;
|
||||
};
|
||||
|
||||
const createOne = async (payload, userId, requestId) => {
|
||||
const data = normalizePayload(payload, fields);
|
||||
@ -68,26 +110,7 @@ const buildMasterService = ({
|
||||
|
||||
const list = async (query) => {
|
||||
const { page, limit, skip } = getPagination(query);
|
||||
const searchFields = fields.filter((f) => f.searchable).map((f) => f.name);
|
||||
|
||||
const where = {
|
||||
...(softDelete ? { deleted_at: null } : {}),
|
||||
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
||||
...(query.search && searchFields.length
|
||||
? {
|
||||
OR: searchFields.map((name) => ({
|
||||
[name]: { contains: query.search, mode: 'insensitive' },
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
for (const f of fields) {
|
||||
if (!f.filterable || query[f.name] === undefined || query[f.name] === null || query[f.name] === '') {
|
||||
continue;
|
||||
}
|
||||
where[f.name] = f.type === 'int' ? BigInt(query[f.name]) : query[f.name];
|
||||
}
|
||||
const where = buildListWhere(query);
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
model.findMany({ where, orderBy: { created_at: 'desc' }, skip, take: limit, ...withInclude }),
|
||||
@ -97,9 +120,19 @@ const buildMasterService = ({
|
||||
return { data: rows.map(mapRow), meta: { page, limit, total } };
|
||||
};
|
||||
|
||||
const exportCsv = async (query) => {
|
||||
const where = buildListWhere(query);
|
||||
const rows = await model.findMany({
|
||||
where,
|
||||
orderBy: { created_at: 'desc' },
|
||||
...withInclude,
|
||||
});
|
||||
return rowsToCsv(csvColumns, rows.map(mapRow));
|
||||
};
|
||||
|
||||
const getOne = async (id) => {
|
||||
const one = await model.findFirst({
|
||||
where: { id: BigInt(id), ...(softDelete ? { deleted_at: null } : {}) },
|
||||
where: { id: parseId(id), ...(softDelete ? { deleted_at: null } : {}) },
|
||||
...withInclude,
|
||||
});
|
||||
if (!one) throw new ApiError(404, `${tableName} not found`);
|
||||
@ -107,12 +140,13 @@ const buildMasterService = ({
|
||||
};
|
||||
|
||||
const updateOne = async (id, payload, userId, requestId) => {
|
||||
const idBigInt = parseId(id);
|
||||
const existing = await getOne(id);
|
||||
const data = normalizePayload(payload, fields);
|
||||
if (fields.some((f) => f.name === 'updated_by'))
|
||||
data.updated_by = userId ? BigInt(userId) : null;
|
||||
|
||||
const updated = await model.update({ where: { id: BigInt(id) }, data, ...withInclude });
|
||||
const updated = await model.update({ where: { id: idBigInt }, data, ...withInclude });
|
||||
const mapped = mapRow(updated);
|
||||
|
||||
await auditLog({
|
||||
@ -129,10 +163,11 @@ const buildMasterService = ({
|
||||
};
|
||||
|
||||
const removeOne = async (id, userId, requestId) => {
|
||||
const idBigInt = parseId(id);
|
||||
const existing = await getOne(id);
|
||||
if (softDelete) {
|
||||
await model.update({
|
||||
where: { id: BigInt(id) },
|
||||
where: { id: idBigInt },
|
||||
data: {
|
||||
deleted_at: new Date(),
|
||||
...(fields.some((f) => f.name === 'updated_by')
|
||||
@ -141,7 +176,7 @@ const buildMasterService = ({
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await model.delete({ where: { id: BigInt(id) } });
|
||||
await model.delete({ where: { id: idBigInt } });
|
||||
}
|
||||
|
||||
await auditLog({
|
||||
@ -155,7 +190,7 @@ const buildMasterService = ({
|
||||
});
|
||||
};
|
||||
|
||||
return { createOne, list, getOne, updateOne, removeOne };
|
||||
return { createOne, list, exportCsv, getOne, updateOne, removeOne };
|
||||
};
|
||||
|
||||
module.exports = { buildMasterService };
|
||||
|
||||
@ -69,6 +69,15 @@ const listQuerySchema = Joi.object({
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
/** Strip pagination for CSV export (same filters as list). */
|
||||
const toExportQuerySchema = (schema = listQuerySchema) =>
|
||||
schema.keys({
|
||||
page: Joi.strip(),
|
||||
limit: Joi.strip(),
|
||||
});
|
||||
|
||||
const exportQuerySchema = toExportQuerySchema(listQuerySchema);
|
||||
|
||||
module.exports = {
|
||||
MASTER_CODE_REGEX,
|
||||
MASTER_NAME_REGEX,
|
||||
@ -77,4 +86,6 @@ module.exports = {
|
||||
masterCode,
|
||||
masterName,
|
||||
listQuerySchema,
|
||||
exportQuerySchema,
|
||||
toExportQuerySchema,
|
||||
};
|
||||
|
||||
@ -27,4 +27,12 @@ const remove = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, null, 'brands deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
const exportCsv = asyncHandler(async (req, res) => {
|
||||
const csv = await service.exportBrands(req.query);
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="brands-export.csv"');
|
||||
res.send(csv);
|
||||
});
|
||||
|
||||
module.exports = { create, list, exportCsv, getOne, update, remove };
|
||||
|
||||
|
||||
@ -3,12 +3,18 @@ const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./brands.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./brands.validation');
|
||||
const { createSchema, updateSchema, listQuerySchema, exportQuerySchema } = require('./brands.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get(
|
||||
'/export',
|
||||
authorize('MASTERS', 'export'),
|
||||
validate(exportQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
|
||||
@ -68,6 +68,7 @@ const service = buildMasterService(config);
|
||||
module.exports = {
|
||||
createBrands: service.createOne,
|
||||
listBrands: service.list,
|
||||
exportBrands: service.exportCsv,
|
||||
getBrandsById: service.getOne,
|
||||
updateBrands: service.updateOne,
|
||||
deleteBrands: service.removeOne,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
const { masterCode, masterName, listQuerySchema, exportQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const BRAND_TYPES = ['OWN', 'OEM', 'THIRD_PARTY'];
|
||||
|
||||
@ -25,4 +25,4 @@ const updateSchema = Joi.object({
|
||||
is_active: Joi.boolean().optional(),
|
||||
}).min(1);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema, BRAND_TYPES };
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema, exportQuerySchema, BRAND_TYPES };
|
||||
|
||||
@ -27,4 +27,12 @@ const remove = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, null, 'delivery_terms deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
const exportCsv = asyncHandler(async (req, res) => {
|
||||
const csv = await service.exportDeliveryTerms(req.query);
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="delivery-terms-export.csv"');
|
||||
res.send(csv);
|
||||
});
|
||||
|
||||
module.exports = { create, list, exportCsv, getOne, update, remove };
|
||||
|
||||
|
||||
@ -3,12 +3,18 @@ const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./delivery-terms.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./delivery-terms.validation');
|
||||
const { createSchema, updateSchema, listQuerySchema, exportQuerySchema } = require('./delivery-terms.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get(
|
||||
'/export',
|
||||
authorize('MASTERS', 'export'),
|
||||
validate(exportQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
|
||||
@ -50,6 +50,7 @@ const service = buildMasterService(config);
|
||||
module.exports = {
|
||||
createDeliveryTerms: service.createOne,
|
||||
listDeliveryTerms: service.list,
|
||||
exportDeliveryTerms: service.exportCsv,
|
||||
getDeliveryTermsById: service.getOne,
|
||||
updateDeliveryTerms: service.updateOne,
|
||||
deleteDeliveryTerms: service.removeOne,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
const { masterCode, masterName, listQuerySchema, exportQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: masterCode({ max: 30 }),
|
||||
@ -15,4 +15,4 @@ const updateSchema = Joi.object({
|
||||
is_active: Joi.boolean().optional(),
|
||||
}).min(1);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema, exportQuerySchema };
|
||||
|
||||
@ -27,4 +27,12 @@ const remove = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, null, 'departments deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
const exportCsv = asyncHandler(async (req, res) => {
|
||||
const csv = await service.exportDepartments(req.query);
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="departments-export.csv"');
|
||||
res.send(csv);
|
||||
});
|
||||
|
||||
module.exports = { create, list, exportCsv, getOne, update, remove };
|
||||
|
||||
|
||||
@ -3,12 +3,18 @@ const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./departments.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./departments.validation');
|
||||
const { createSchema, updateSchema, listQuerySchema, exportQuerySchema } = require('./departments.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get(
|
||||
'/export',
|
||||
authorize('MASTERS', 'export'),
|
||||
validate(exportQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
|
||||
@ -38,6 +38,7 @@ const service = buildMasterService(config);
|
||||
module.exports = {
|
||||
createDepartments: service.createOne,
|
||||
listDepartments: service.list,
|
||||
exportDepartments: service.exportCsv,
|
||||
getDepartmentsById: service.getOne,
|
||||
updateDepartments: service.updateOne,
|
||||
deleteDepartments: service.removeOne,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
const Joi = require('joi');
|
||||
const { masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
const { masterName, listQuerySchema, exportQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
name: masterName({ max: 150 }),
|
||||
@ -11,4 +11,4 @@ const updateSchema = Joi.object({
|
||||
is_active: Joi.boolean().optional(),
|
||||
}).min(1);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema, exportQuerySchema };
|
||||
|
||||
@ -27,4 +27,12 @@ const remove = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, null, 'designations deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
const exportCsv = asyncHandler(async (req, res) => {
|
||||
const csv = await service.exportDesignations(req.query);
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="designations-export.csv"');
|
||||
res.send(csv);
|
||||
});
|
||||
|
||||
module.exports = { create, list, exportCsv, getOne, update, remove };
|
||||
|
||||
|
||||
@ -3,12 +3,18 @@ const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./designations.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./designations.validation');
|
||||
const { createSchema, updateSchema, listQuerySchema, exportQuerySchema } = require('./designations.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get(
|
||||
'/export',
|
||||
authorize('MASTERS', 'export'),
|
||||
validate(exportQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
|
||||
@ -38,6 +38,7 @@ const service = buildMasterService(config);
|
||||
module.exports = {
|
||||
createDesignations: service.createOne,
|
||||
listDesignations: service.list,
|
||||
exportDesignations: service.exportCsv,
|
||||
getDesignationsById: service.getOne,
|
||||
updateDesignations: service.updateOne,
|
||||
deleteDesignations: service.removeOne,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
const Joi = require('joi');
|
||||
const { masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
const { masterName, listQuerySchema, exportQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
name: masterName({ max: 150 }),
|
||||
@ -11,4 +11,4 @@ const updateSchema = Joi.object({
|
||||
is_active: Joi.boolean().optional(),
|
||||
}).min(1);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema, exportQuerySchema };
|
||||
|
||||
@ -27,4 +27,12 @@ const remove = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, null, 'document_series deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
const exportCsv = asyncHandler(async (req, res) => {
|
||||
const csv = await service.exportDocumentSeries(req.query);
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="document-series-export.csv"');
|
||||
res.send(csv);
|
||||
});
|
||||
|
||||
module.exports = { create, list, exportCsv, getOne, update, remove };
|
||||
|
||||
|
||||
@ -3,12 +3,18 @@ const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./document-series.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./document-series.validation');
|
||||
const { createSchema, updateSchema, listQuerySchema, exportQuerySchema } = require('./document-series.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get(
|
||||
'/export',
|
||||
authorize('MASTERS', 'export'),
|
||||
validate(exportQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
|
||||
@ -62,6 +62,7 @@ const service = buildMasterService(config);
|
||||
module.exports = {
|
||||
createDocumentSeries: service.createOne,
|
||||
listDocumentSeries: service.list,
|
||||
exportDocumentSeries: service.exportCsv,
|
||||
getDocumentSeriesById: service.getOne,
|
||||
updateDocumentSeries: service.updateOne,
|
||||
deleteDocumentSeries: service.removeOne,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, listQuerySchema } = require('../_shared/masters.validation');
|
||||
const { masterCode, listQuerySchema, exportQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: masterCode({ max: 30 }),
|
||||
@ -19,4 +19,4 @@ const updateSchema = Joi.object({
|
||||
is_active: Joi.boolean().optional(),
|
||||
}).min(1);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema, exportQuerySchema };
|
||||
|
||||
@ -27,4 +27,12 @@ const remove = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, null, 'gst_rates deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
const exportCsv = asyncHandler(async (req, res) => {
|
||||
const csv = await service.exportGstRates(req.query);
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="gst-rates-export.csv"');
|
||||
res.send(csv);
|
||||
});
|
||||
|
||||
module.exports = { create, list, exportCsv, getOne, update, remove };
|
||||
|
||||
|
||||
@ -3,12 +3,18 @@ const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./gst-rates.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./gst-rates.validation');
|
||||
const { createSchema, updateSchema, listQuerySchema, exportQuerySchema } = require('./gst-rates.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get(
|
||||
'/export',
|
||||
authorize('MASTERS', 'export'),
|
||||
validate(exportQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
|
||||
@ -32,6 +32,7 @@ const service = buildMasterService(config);
|
||||
module.exports = {
|
||||
createGstRates: service.createOne,
|
||||
listGstRates: service.list,
|
||||
exportGstRates: service.exportCsv,
|
||||
getGstRatesById: service.getOne,
|
||||
updateGstRates: service.updateOne,
|
||||
deleteGstRates: service.removeOne,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
const Joi = require('joi');
|
||||
const { listQuerySchema, toExportQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
rate_pct: Joi.number().precision(2).optional(),
|
||||
@ -8,11 +9,6 @@ const createSchema = Joi.object({
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
const exportQuerySchema = toExportQuerySchema(listQuerySchema);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema, exportQuerySchema };
|
||||
|
||||
@ -27,4 +27,12 @@ const remove = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, null, 'hsn_codes deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
const exportCsv = asyncHandler(async (req, res) => {
|
||||
const csv = await service.exportHsnCodes(req.query);
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="hsn-codes-export.csv"');
|
||||
res.send(csv);
|
||||
});
|
||||
|
||||
module.exports = { create, list, exportCsv, getOne, update, remove };
|
||||
|
||||
|
||||
@ -3,12 +3,18 @@ const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./hsn-codes.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./hsn-codes.validation');
|
||||
const { createSchema, updateSchema, listQuerySchema, exportQuerySchema } = require('./hsn-codes.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get(
|
||||
'/export',
|
||||
authorize('MASTERS', 'export'),
|
||||
validate(exportQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
|
||||
@ -32,6 +32,7 @@ const service = buildMasterService(config);
|
||||
module.exports = {
|
||||
createHsnCodes: service.createOne,
|
||||
listHsnCodes: service.list,
|
||||
exportHsnCodes: service.exportCsv,
|
||||
getHsnCodesById: service.getOne,
|
||||
updateHsnCodes: service.updateOne,
|
||||
deleteHsnCodes: service.removeOne,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
const Joi = require('joi');
|
||||
const { listQuerySchema, toExportQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const HSN_CODE_REGEX = /^[0-9]{4,8}$/;
|
||||
const HSN_CODE_MESSAGE = 'HSN/SAC code must be 4 to 8 digits';
|
||||
@ -21,11 +22,6 @@ const updateSchema = Joi.object({
|
||||
is_active: Joi.boolean().optional(),
|
||||
}).min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
const exportQuerySchema = toExportQuerySchema(listQuerySchema);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema, exportQuerySchema };
|
||||
|
||||
@ -27,4 +27,12 @@ const remove = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, null, 'item_categories deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
const exportCsv = asyncHandler(async (req, res) => {
|
||||
const csv = await service.exportItemCategories(req.query);
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="item-categories-export.csv"');
|
||||
res.send(csv);
|
||||
});
|
||||
|
||||
module.exports = { create, list, exportCsv, getOne, update, remove };
|
||||
|
||||
|
||||
@ -3,12 +3,18 @@ const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./item-categories.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./item-categories.validation');
|
||||
const { createSchema, updateSchema, listQuerySchema, exportQuerySchema } = require('./item-categories.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get(
|
||||
'/export',
|
||||
authorize('MASTERS', 'export'),
|
||||
validate(exportQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
|
||||
@ -77,6 +77,7 @@ module.exports = {
|
||||
createItemCategories: (payload, userId, requestId) =>
|
||||
service.createOne(normalizeAssetDefaults(payload), userId, requestId),
|
||||
listItemCategories: service.list,
|
||||
exportItemCategories: service.exportCsv,
|
||||
getItemCategoriesById: service.getOne,
|
||||
updateItemCategories: (id, payload, userId, requestId) =>
|
||||
service.updateOne(id, normalizeAssetDefaults(payload), userId, requestId),
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
const { masterCode, masterName, listQuerySchema, exportQuerySchema } = require('../_shared/masters.validation');
|
||||
const { DEPRECIATION_METHODS } = require('../../assets/assets.constants');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
@ -26,4 +26,4 @@ const updateSchema = Joi.object({
|
||||
is_active: Joi.boolean().optional(),
|
||||
}).min(1);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema, exportQuerySchema };
|
||||
|
||||
@ -27,4 +27,12 @@ const remove = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, null, 'item_subcategories deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
const exportCsv = asyncHandler(async (req, res) => {
|
||||
const csv = await service.exportItemSubcategories(req.query);
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="item-subcategories-export.csv"');
|
||||
res.send(csv);
|
||||
});
|
||||
|
||||
module.exports = { create, list, exportCsv, getOne, update, remove };
|
||||
|
||||
|
||||
@ -3,12 +3,18 @@ const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./item-subcategories.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./item-subcategories.validation');
|
||||
const { createSchema, updateSchema, listQuerySchema, exportQuerySchema } = require('./item-subcategories.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get(
|
||||
'/export',
|
||||
authorize('MASTERS', 'export'),
|
||||
validate(exportQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
|
||||
@ -20,6 +20,15 @@ const config = {
|
||||
softDelete: true,
|
||||
include: subcategoryInclude,
|
||||
mapRow: sanitizeSubcategory,
|
||||
exportColumns: [
|
||||
{ key: 'id', header: 'ID' },
|
||||
{ key: 'code', header: 'Code' },
|
||||
{ key: 'name', header: 'Name' },
|
||||
{ key: (row) => row.item_category?.code || '', header: 'Category Code' },
|
||||
{ key: (row) => row.item_category?.name || '', header: 'Category Name' },
|
||||
{ key: 'is_active', header: 'Is Active' },
|
||||
{ key: 'created_at', header: 'Created At' },
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
name: 'item_category_id',
|
||||
@ -67,6 +76,7 @@ const service = buildMasterService(config);
|
||||
module.exports = {
|
||||
createItemSubcategories: service.createOne,
|
||||
listItemSubcategories: service.list,
|
||||
exportItemSubcategories: service.exportCsv,
|
||||
getItemSubcategoriesById: service.getOne,
|
||||
updateItemSubcategories: service.updateOne,
|
||||
deleteItemSubcategories: service.removeOne,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, masterName, listQuerySchema: baseListQuerySchema } = require('../_shared/masters.validation');
|
||||
const { masterCode, masterName, listQuerySchema: baseListQuerySchema, toExportQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
item_category_id: Joi.number().integer().positive().required(),
|
||||
@ -19,4 +19,6 @@ const listQuerySchema = baseListQuerySchema.keys({
|
||||
item_category_id: Joi.number().integer().positive().optional(),
|
||||
});
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
const exportQuerySchema = toExportQuerySchema(listQuerySchema);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema, exportQuerySchema };
|
||||
|
||||
@ -27,4 +27,12 @@ const remove = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, null, 'items deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
const exportCsv = asyncHandler(async (req, res) => {
|
||||
const csv = await service.exportItems(req.query);
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="items-export.csv"');
|
||||
res.send(csv);
|
||||
});
|
||||
|
||||
module.exports = { create, list, exportCsv, getOne, update, remove };
|
||||
|
||||
|
||||
@ -3,12 +3,18 @@ const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./items.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./items.validation');
|
||||
const { createSchema, updateSchema, listQuerySchema, exportQuerySchema } = require('./items.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get(
|
||||
'/export',
|
||||
authorize('MASTERS', 'export'),
|
||||
validate(exportQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
|
||||
@ -3,6 +3,8 @@ const ApiError = require('../../../utils/ApiError');
|
||||
const auditLog = require('../../../utils/auditLog');
|
||||
const { getPagination } = require('../../../utils/pagination');
|
||||
const { nextDocumentNumber } = require('../../../utils/generateCode');
|
||||
const { parseId } = require('../../../utils/parseId');
|
||||
const { rowsToCsv } = require('../../../utils/csv');
|
||||
|
||||
const ITEM_SERIES_CODE = 'ITEM';
|
||||
|
||||
@ -200,21 +202,22 @@ const createItems = async (payload, userId, requestId) => {
|
||||
return sanitizeItem(created);
|
||||
};
|
||||
|
||||
const buildItemsWhere = (query) => ({
|
||||
deleted_at: null,
|
||||
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
||||
...(query.item_category_id ? { item_category_id: BigInt(query.item_category_id) } : {}),
|
||||
...(query.search
|
||||
? {
|
||||
OR: SEARCH_FIELDS.map((name) => ({
|
||||
[name]: { contains: query.search, mode: 'insensitive' },
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
const listItems = async (query) => {
|
||||
const { page, limit, skip } = getPagination(query);
|
||||
|
||||
const where = {
|
||||
deleted_at: null,
|
||||
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
||||
...(query.item_category_id ? { item_category_id: BigInt(query.item_category_id) } : {}),
|
||||
...(query.search
|
||||
? {
|
||||
OR: SEARCH_FIELDS.map((name) => ({
|
||||
[name]: { contains: query.search, mode: 'insensitive' },
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
const where = buildItemsWhere(query);
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
prisma.items.findMany({
|
||||
@ -230,9 +233,36 @@ const listItems = async (query) => {
|
||||
return { data: rows.map(sanitizeItem), meta: { page, limit, total } };
|
||||
};
|
||||
|
||||
const exportItems = async (query) => {
|
||||
const rows = await prisma.items.findMany({
|
||||
where: buildItemsWhere(query),
|
||||
include: itemInclude,
|
||||
orderBy: { created_at: 'desc' },
|
||||
});
|
||||
|
||||
return rowsToCsv(
|
||||
[
|
||||
{ key: 'id', header: 'ID' },
|
||||
{ key: 'item_code', header: 'Item Code' },
|
||||
{ key: 'item_name', header: 'Item Name' },
|
||||
{ key: (row) => row.item_category?.code || '', header: 'Category Code' },
|
||||
{ key: (row) => row.item_category?.name || '', header: 'Category Name' },
|
||||
{ key: (row) => row.item_subcategory?.code || '', header: 'Subcategory Code' },
|
||||
{ key: (row) => row.uom?.code || '', header: 'UOM' },
|
||||
{ key: (row) => row.hsn_code?.code || '', header: 'HSN Code' },
|
||||
{ key: (row) => (row.gst_rate ? row.gst_rate.rate_pct : ''), header: 'GST %' },
|
||||
{ key: (row) => row.brand?.name || '', header: 'Brand' },
|
||||
{ key: 'is_asset_item', header: 'Is Asset Item' },
|
||||
{ key: 'is_active', header: 'Is Active' },
|
||||
{ key: 'created_at', header: 'Created At' },
|
||||
],
|
||||
rows.map(sanitizeItem)
|
||||
);
|
||||
};
|
||||
|
||||
const getItemsById = async (id) => {
|
||||
const one = await prisma.items.findFirst({
|
||||
where: { id: BigInt(id), deleted_at: null },
|
||||
where: { id: parseId(id), deleted_at: null },
|
||||
include: itemInclude,
|
||||
});
|
||||
if (!one) throw new ApiError(404, 'items not found');
|
||||
@ -240,6 +270,7 @@ const getItemsById = async (id) => {
|
||||
};
|
||||
|
||||
const updateItems = async (id, payload, userId, requestId) => {
|
||||
const idBigInt = parseId(id);
|
||||
const existing = await getItemsById(id);
|
||||
const data = normalizePayload(payload);
|
||||
|
||||
@ -251,7 +282,7 @@ const updateItems = async (id, payload, userId, requestId) => {
|
||||
data.updated_by = userId ? BigInt(userId) : null;
|
||||
|
||||
const updated = await prisma.items.update({
|
||||
where: { id: BigInt(id) },
|
||||
where: { id: idBigInt },
|
||||
data,
|
||||
include: itemInclude,
|
||||
});
|
||||
@ -270,10 +301,11 @@ const updateItems = async (id, payload, userId, requestId) => {
|
||||
};
|
||||
|
||||
const deleteItems = async (id, userId, requestId) => {
|
||||
const idBigInt = parseId(id);
|
||||
const existing = await getItemsById(id);
|
||||
|
||||
await prisma.items.update({
|
||||
where: { id: BigInt(id) },
|
||||
where: { id: idBigInt },
|
||||
data: {
|
||||
deleted_at: new Date(),
|
||||
updated_by: userId ? BigInt(userId) : null,
|
||||
@ -294,6 +326,7 @@ const deleteItems = async (id, userId, requestId) => {
|
||||
module.exports = {
|
||||
createItems,
|
||||
listItems,
|
||||
exportItems,
|
||||
getItemsById,
|
||||
updateItems,
|
||||
deleteItems,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
const Joi = require('joi');
|
||||
const { masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
const { masterName, listQuerySchema, toExportQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
item_name: masterName({ max: 200, required: true }),
|
||||
@ -37,4 +37,6 @@ const listQuerySchemaExtended = listQuerySchema.keys({
|
||||
item_category_id: Joi.number().integer().positive().optional(),
|
||||
});
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema: listQuerySchemaExtended };
|
||||
const exportQuerySchema = toExportQuerySchema(listQuerySchemaExtended);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema: listQuerySchemaExtended, exportQuerySchema };
|
||||
|
||||
@ -27,4 +27,12 @@ const remove = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, null, 'Location deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
const exportCsv = asyncHandler(async (req, res) => {
|
||||
const csv = await service.exportLocations(req.query);
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="locations-export.csv"');
|
||||
res.send(csv);
|
||||
});
|
||||
|
||||
module.exports = { create, list, exportCsv, getOne, update, remove };
|
||||
|
||||
|
||||
@ -7,12 +7,19 @@ const {
|
||||
createLocationSchema,
|
||||
updateLocationSchema,
|
||||
listQuerySchema,
|
||||
exportQuerySchema,
|
||||
} = require('./locations.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get(
|
||||
'/export',
|
||||
authorize('MASTERS', 'export'),
|
||||
validate(exportQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createLocationSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateLocationSchema), controller.update);
|
||||
|
||||
@ -2,6 +2,8 @@ const prisma = require('../../../config/prisma');
|
||||
const ApiError = require('../../../utils/ApiError');
|
||||
const auditLog = require('../../../utils/auditLog');
|
||||
const { getPagination } = require('../../../utils/pagination');
|
||||
const { parseId } = require('../../../utils/parseId');
|
||||
const { rowsToCsv } = require('../../../utils/csv');
|
||||
const {
|
||||
LOCATION_TYPES,
|
||||
assertPlant,
|
||||
@ -55,7 +57,7 @@ const assertUniqueCode = async (code, excludeId = null) => {
|
||||
where: {
|
||||
code,
|
||||
deleted_at: null,
|
||||
...(excludeId ? { id: { not: BigInt(excludeId) } } : {}),
|
||||
...(excludeId ? { id: { not: parseId(excludeId) } } : {}),
|
||||
},
|
||||
});
|
||||
if (existing) throw new ApiError(409, 'Location code already exists');
|
||||
@ -98,10 +100,8 @@ const createLocation = async (type, payload, userId, requestId) => {
|
||||
return response;
|
||||
};
|
||||
|
||||
const listLocations = async (query, type = null) => {
|
||||
const { page, limit, skip } = getPagination(query);
|
||||
const buildLocationsWhere = (query, type = null) => {
|
||||
const effectiveType = type || query.type || null;
|
||||
|
||||
const where = {
|
||||
deleted_at: null,
|
||||
...(effectiveType ? { type: effectiveType } : {}),
|
||||
@ -116,6 +116,12 @@ const listLocations = async (query, type = null) => {
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
return { where, effectiveType };
|
||||
};
|
||||
|
||||
const listLocations = async (query, type = null) => {
|
||||
const { page, limit, skip } = getPagination(query);
|
||||
const { where, effectiveType } = buildLocationsWhere(query, type);
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
prisma.locations.findMany({
|
||||
@ -140,10 +146,62 @@ const listLocations = async (query, type = null) => {
|
||||
return { data: rows.map(mapRow), meta: { page, limit, total } };
|
||||
};
|
||||
|
||||
const exportLocations = async (query, type = null) => {
|
||||
const { where, effectiveType } = buildLocationsWhere(query, type);
|
||||
const rows = await prisma.locations.findMany({
|
||||
where,
|
||||
include:
|
||||
effectiveType === LOCATION_TYPES.WAREHOUSE || !effectiveType
|
||||
? warehouseInclude
|
||||
: undefined,
|
||||
orderBy: { created_at: 'desc' },
|
||||
});
|
||||
|
||||
const mapRow = (row) => {
|
||||
if (effectiveType === LOCATION_TYPES.PLANT) return toPlantResponse(row);
|
||||
if (effectiveType === LOCATION_TYPES.WAREHOUSE) return toWarehouseResponse(row);
|
||||
return toLocationResponse(row);
|
||||
};
|
||||
|
||||
const columns =
|
||||
effectiveType === LOCATION_TYPES.WAREHOUSE
|
||||
? [
|
||||
{ key: 'id', header: 'ID' },
|
||||
{ key: 'code', header: 'Code' },
|
||||
{ key: 'name', header: 'Name' },
|
||||
{ key: (row) => row.parent?.code || row.plant?.code || '', header: 'Plant Code' },
|
||||
{ key: (row) => row.parent?.name || row.plant?.name || '', header: 'Plant Name' },
|
||||
{ key: 'location', header: 'Location' },
|
||||
{ key: 'is_active', header: 'Is Active' },
|
||||
{ key: 'created_at', header: 'Created At' },
|
||||
]
|
||||
: effectiveType === LOCATION_TYPES.PLANT
|
||||
? [
|
||||
{ key: 'id', header: 'ID' },
|
||||
{ key: 'code', header: 'Code' },
|
||||
{ key: 'name', header: 'Name' },
|
||||
{ key: 'gstin', header: 'GSTIN' },
|
||||
{ key: 'city', header: 'City' },
|
||||
{ key: 'state', header: 'State' },
|
||||
{ key: 'is_active', header: 'Is Active' },
|
||||
{ key: 'created_at', header: 'Created At' },
|
||||
]
|
||||
: [
|
||||
{ key: 'id', header: 'ID' },
|
||||
{ key: 'type', header: 'Type' },
|
||||
{ key: 'code', header: 'Code' },
|
||||
{ key: 'name', header: 'Name' },
|
||||
{ key: 'is_active', header: 'Is Active' },
|
||||
{ key: 'created_at', header: 'Created At' },
|
||||
];
|
||||
|
||||
return rowsToCsv(columns, rows.map(mapRow));
|
||||
};
|
||||
|
||||
const getLocationById = async (id, type = null) => {
|
||||
const row = await prisma.locations.findFirst({
|
||||
where: {
|
||||
id: BigInt(id),
|
||||
id: parseId(id),
|
||||
deleted_at: null,
|
||||
...(type ? { type } : {}),
|
||||
},
|
||||
@ -157,8 +215,9 @@ const getLocationById = async (id, type = null) => {
|
||||
};
|
||||
|
||||
const updateLocation = async (id, type, payload, userId, requestId) => {
|
||||
const idBigInt = parseId(id);
|
||||
const existing = await prisma.locations.findFirst({
|
||||
where: { id: BigInt(id), deleted_at: null, type },
|
||||
where: { id: idBigInt, deleted_at: null, type },
|
||||
include: warehouseInclude,
|
||||
});
|
||||
if (!existing) throw new ApiError(404, `${type} not found`);
|
||||
@ -180,7 +239,7 @@ const updateLocation = async (id, type, payload, userId, requestId) => {
|
||||
data.updated_by = userId ? BigInt(userId) : null;
|
||||
|
||||
const updated = await prisma.locations.update({
|
||||
where: { id: BigInt(id) },
|
||||
where: { id: idBigInt },
|
||||
data,
|
||||
include: type === LOCATION_TYPES.WAREHOUSE ? warehouseInclude : undefined,
|
||||
});
|
||||
@ -204,14 +263,15 @@ const updateLocation = async (id, type, payload, userId, requestId) => {
|
||||
};
|
||||
|
||||
const deleteLocation = async (id, type, userId, requestId) => {
|
||||
const idBigInt = parseId(id);
|
||||
const existing = await prisma.locations.findFirst({
|
||||
where: { id: BigInt(id), deleted_at: null, type },
|
||||
where: { id: idBigInt, deleted_at: null, type },
|
||||
include: warehouseInclude,
|
||||
});
|
||||
if (!existing) throw new ApiError(404, `${type} not found`);
|
||||
|
||||
await prisma.locations.update({
|
||||
where: { id: BigInt(id) },
|
||||
where: { id: idBigInt },
|
||||
data: { deleted_at: new Date(), updated_by: userId ? BigInt(userId) : null },
|
||||
});
|
||||
|
||||
@ -231,7 +291,7 @@ const deleteLocation = async (id, type, userId, requestId) => {
|
||||
|
||||
const deleteLocationById = async (id, userId, requestId) => {
|
||||
const existing = await prisma.locations.findFirst({
|
||||
where: { id: BigInt(id), deleted_at: null },
|
||||
where: { id: parseId(id), deleted_at: null },
|
||||
include: warehouseInclude,
|
||||
});
|
||||
if (!existing) throw new ApiError(404, 'Location not found');
|
||||
@ -240,7 +300,7 @@ const deleteLocationById = async (id, userId, requestId) => {
|
||||
|
||||
const updateLocationById = async (id, payload, userId, requestId) => {
|
||||
const existing = await prisma.locations.findFirst({
|
||||
where: { id: BigInt(id), deleted_at: null },
|
||||
where: { id: parseId(id), deleted_at: null },
|
||||
});
|
||||
if (!existing) throw new ApiError(404, 'Location not found');
|
||||
return updateLocation(id, existing.type, payload, userId, requestId);
|
||||
@ -251,6 +311,7 @@ module.exports = {
|
||||
createPlant: (payload, userId, requestId) =>
|
||||
createLocation(LOCATION_TYPES.PLANT, payload, userId, requestId),
|
||||
listPlants: (query) => listLocations(query, LOCATION_TYPES.PLANT),
|
||||
exportPlants: (query) => exportLocations(query, LOCATION_TYPES.PLANT),
|
||||
getPlantById: (id) => getLocationById(id, LOCATION_TYPES.PLANT),
|
||||
updatePlant: (id, payload, userId, requestId) =>
|
||||
updateLocation(id, LOCATION_TYPES.PLANT, payload, userId, requestId),
|
||||
@ -259,6 +320,7 @@ module.exports = {
|
||||
createWarehouse: (payload, userId, requestId) =>
|
||||
createLocation(LOCATION_TYPES.WAREHOUSE, payload, userId, requestId),
|
||||
listWarehouses: (query) => listLocations(query, LOCATION_TYPES.WAREHOUSE),
|
||||
exportWarehouses: (query) => exportLocations(query, LOCATION_TYPES.WAREHOUSE),
|
||||
getWarehouseById: (id) => getLocationById(id, LOCATION_TYPES.WAREHOUSE),
|
||||
updateWarehouse: (id, payload, userId, requestId) =>
|
||||
updateLocation(id, LOCATION_TYPES.WAREHOUSE, payload, userId, requestId),
|
||||
@ -272,6 +334,7 @@ module.exports = {
|
||||
return createLocation(payload.type, payload, userId, requestId);
|
||||
},
|
||||
listLocations: (query) => listLocations(query),
|
||||
exportLocations: (query) => exportLocations(query),
|
||||
getLocationById: (id) => getLocationById(id),
|
||||
updateLocationById,
|
||||
deleteLocationById,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
const { masterCode, masterName, listQuerySchema, toExportQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const locationFields = {
|
||||
gstin: Joi.string().max(15).allow(null, '').optional(),
|
||||
@ -92,6 +92,8 @@ const listLocationsQuerySchema = listQuerySchema.keys({
|
||||
parent_id: Joi.number().integer().positive().optional(),
|
||||
});
|
||||
|
||||
const exportQuerySchema = toExportQuerySchema(listLocationsQuerySchema);
|
||||
|
||||
module.exports = {
|
||||
createPlantSchema,
|
||||
updatePlantSchema,
|
||||
@ -100,4 +102,5 @@ module.exports = {
|
||||
createLocationSchema,
|
||||
updateLocationSchema,
|
||||
listQuerySchema: listLocationsQuerySchema,
|
||||
exportQuerySchema,
|
||||
};
|
||||
|
||||
@ -27,4 +27,12 @@ const remove = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, null, 'payment_terms deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
const exportCsv = asyncHandler(async (req, res) => {
|
||||
const csv = await service.exportPaymentTerms(req.query);
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="payment-terms-export.csv"');
|
||||
res.send(csv);
|
||||
});
|
||||
|
||||
module.exports = { create, list, exportCsv, getOne, update, remove };
|
||||
|
||||
|
||||
@ -3,12 +3,18 @@ const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./payment-terms.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./payment-terms.validation');
|
||||
const { createSchema, updateSchema, listQuerySchema, exportQuerySchema } = require('./payment-terms.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get(
|
||||
'/export',
|
||||
authorize('MASTERS', 'export'),
|
||||
validate(exportQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
|
||||
@ -56,6 +56,7 @@ const service = buildMasterService(config);
|
||||
module.exports = {
|
||||
createPaymentTerms: service.createOne,
|
||||
listPaymentTerms: service.list,
|
||||
exportPaymentTerms: service.exportCsv,
|
||||
getPaymentTermsById: service.getOne,
|
||||
updatePaymentTerms: service.updateOne,
|
||||
deletePaymentTerms: service.removeOne,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
const { masterCode, masterName, listQuerySchema, exportQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: masterCode({ max: 30 }),
|
||||
@ -17,4 +17,4 @@ const updateSchema = Joi.object({
|
||||
is_active: Joi.boolean().optional(),
|
||||
}).min(1);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema, exportQuerySchema };
|
||||
|
||||
@ -27,4 +27,12 @@ const remove = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, null, 'plants deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
const exportCsv = asyncHandler(async (req, res) => {
|
||||
const csv = await service.exportPlants(req.query);
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="plants-export.csv"');
|
||||
res.send(csv);
|
||||
});
|
||||
|
||||
module.exports = { create, list, exportCsv, getOne, update, remove };
|
||||
|
||||
|
||||
@ -3,12 +3,18 @@ const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./plants.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./plants.validation');
|
||||
const { createSchema, updateSchema, listQuerySchema, exportQuerySchema } = require('./plants.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get(
|
||||
'/export',
|
||||
authorize('MASTERS', 'export'),
|
||||
validate(exportQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
|
||||
@ -3,6 +3,7 @@ const service = require('../locations/locations.service');
|
||||
module.exports = {
|
||||
createPlants: service.createPlant,
|
||||
listPlants: service.listPlants,
|
||||
exportPlants: service.exportPlants,
|
||||
getPlantsById: service.getPlantById,
|
||||
updatePlants: service.updatePlant,
|
||||
deletePlants: service.deletePlant,
|
||||
|
||||
@ -2,6 +2,7 @@ const {
|
||||
createPlantSchema: createSchema,
|
||||
updatePlantSchema: updateSchema,
|
||||
listQuerySchema,
|
||||
exportQuerySchema,
|
||||
} = require('../locations/locations.validation');
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema, exportQuerySchema };
|
||||
|
||||
@ -27,4 +27,12 @@ const remove = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, null, 'UOM deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
const exportCsv = asyncHandler(async (req, res) => {
|
||||
const csv = await service.exportUom(req.query);
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="uom-export.csv"');
|
||||
res.send(csv);
|
||||
});
|
||||
|
||||
module.exports = { create, list, exportCsv, getOne, update, remove };
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./uom.controller');
|
||||
const { createUomSchema, updateUomSchema, listUomQuerySchema } = require('./uom.validation');
|
||||
const { createUomSchema, updateUomSchema, listUomQuerySchema, exportUomQuerySchema } = require('./uom.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@ -15,6 +15,12 @@ router.get(
|
||||
validate(listUomQuerySchema, 'query'),
|
||||
controller.list
|
||||
);
|
||||
router.get(
|
||||
'/export',
|
||||
authorize('MASTERS', 'export'),
|
||||
validate(exportUomQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createUomSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateUomSchema), controller.update);
|
||||
|
||||
@ -2,6 +2,21 @@ const prisma = require('../../../config/prisma');
|
||||
const ApiError = require('../../../utils/ApiError');
|
||||
const auditLog = require('../../../utils/auditLog');
|
||||
const { getPagination } = require('../../../utils/pagination');
|
||||
const { parseId } = require('../../../utils/parseId');
|
||||
const { rowsToCsv } = require('../../../utils/csv');
|
||||
|
||||
const buildUomWhere = (query) => ({
|
||||
deleted_at: null,
|
||||
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
||||
...(query.search
|
||||
? {
|
||||
OR: [
|
||||
{ code: { contains: query.search, mode: 'insensitive' } },
|
||||
{ name: { contains: query.search, mode: 'insensitive' } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
const createUom = async (payload, userId, requestId) => {
|
||||
const exists = await prisma.uom.findFirst({
|
||||
@ -32,18 +47,7 @@ const createUom = async (payload, userId, requestId) => {
|
||||
|
||||
const listUom = async (query) => {
|
||||
const { page, limit, skip } = getPagination(query);
|
||||
const where = {
|
||||
deleted_at: null,
|
||||
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
||||
...(query.search
|
||||
? {
|
||||
OR: [
|
||||
{ code: { contains: query.search, mode: 'insensitive' } },
|
||||
{ name: { contains: query.search, mode: 'insensitive' } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
const where = buildUomWhere(query);
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.uom.findMany({ where, orderBy: { created_at: 'desc' }, skip, take: limit }),
|
||||
@ -53,16 +57,34 @@ const listUom = async (query) => {
|
||||
return { data, meta: { page, limit, total } };
|
||||
};
|
||||
|
||||
const exportUom = async (query) => {
|
||||
const rows = await prisma.uom.findMany({
|
||||
where: buildUomWhere(query),
|
||||
orderBy: { created_at: 'desc' },
|
||||
});
|
||||
return rowsToCsv(
|
||||
[
|
||||
{ key: 'id', header: 'ID' },
|
||||
{ key: 'code', header: 'Code' },
|
||||
{ key: 'name', header: 'Name' },
|
||||
{ key: 'is_active', header: 'Is Active' },
|
||||
{ key: 'created_at', header: 'Created At' },
|
||||
],
|
||||
rows
|
||||
);
|
||||
};
|
||||
|
||||
const getUomById = async (id) => {
|
||||
const uom = await prisma.uom.findFirst({ where: { id: BigInt(id), deleted_at: null } });
|
||||
const uom = await prisma.uom.findFirst({ where: { id: parseId(id), deleted_at: null } });
|
||||
if (!uom) throw new ApiError(404, 'UOM not found');
|
||||
return uom;
|
||||
};
|
||||
|
||||
const updateUom = async (id, payload, userId, requestId) => {
|
||||
const idBigInt = parseId(id);
|
||||
const existing = await getUomById(id);
|
||||
const updated = await prisma.uom.update({
|
||||
where: { id: BigInt(id) },
|
||||
where: { id: idBigInt },
|
||||
data: {
|
||||
...payload,
|
||||
...(payload.code ? { code: payload.code.toUpperCase().trim() } : {}),
|
||||
@ -84,9 +106,10 @@ const updateUom = async (id, payload, userId, requestId) => {
|
||||
};
|
||||
|
||||
const deleteUom = async (id, userId, requestId) => {
|
||||
const idBigInt = parseId(id);
|
||||
const existing = await getUomById(id);
|
||||
await prisma.uom.update({
|
||||
where: { id: BigInt(id) },
|
||||
where: { id: idBigInt },
|
||||
data: { deleted_at: new Date(), updated_by: userId ? BigInt(userId) : null },
|
||||
});
|
||||
|
||||
@ -101,4 +124,4 @@ const deleteUom = async (id, userId, requestId) => {
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { createUom, listUom, getUomById, updateUom, deleteUom };
|
||||
module.exports = { createUom, listUom, exportUom, getUomById, updateUom, deleteUom };
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
const { masterCode, masterName, listQuerySchema, exportQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const createUomSchema = Joi.object({
|
||||
code: masterCode({ max: 20, required: true }),
|
||||
@ -13,4 +13,4 @@ const updateUomSchema = Joi.object({
|
||||
is_active: Joi.boolean().optional(),
|
||||
}).min(1);
|
||||
|
||||
module.exports = { createUomSchema, updateUomSchema, listUomQuerySchema: listQuerySchema };
|
||||
module.exports = { createUomSchema, updateUomSchema, listUomQuerySchema: listQuerySchema, exportUomQuerySchema: exportQuerySchema };
|
||||
|
||||
@ -27,4 +27,12 @@ const remove = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, null, 'warehouses deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
const exportCsv = asyncHandler(async (req, res) => {
|
||||
const csv = await service.exportWarehouses(req.query);
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="warehouses-export.csv"');
|
||||
res.send(csv);
|
||||
});
|
||||
|
||||
module.exports = { create, list, exportCsv, getOne, update, remove };
|
||||
|
||||
|
||||
@ -3,12 +3,18 @@ const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./warehouses.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./warehouses.validation');
|
||||
const { createSchema, updateSchema, listQuerySchema, exportQuerySchema } = require('./warehouses.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get(
|
||||
'/export',
|
||||
authorize('MASTERS', 'export'),
|
||||
validate(exportQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
|
||||
@ -3,6 +3,7 @@ const service = require('../locations/locations.service');
|
||||
module.exports = {
|
||||
createWarehouses: service.createWarehouse,
|
||||
listWarehouses: service.listWarehouses,
|
||||
exportWarehouses: service.exportWarehouses,
|
||||
getWarehousesById: service.getWarehouseById,
|
||||
updateWarehouses: service.updateWarehouse,
|
||||
deleteWarehouses: service.deleteWarehouse,
|
||||
|
||||
@ -2,6 +2,7 @@ const {
|
||||
createWarehouseSchema: createSchema,
|
||||
updateWarehouseSchema: updateSchema,
|
||||
listQuerySchema,
|
||||
exportQuerySchema,
|
||||
} = require('../locations/locations.validation');
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema, exportQuerySchema };
|
||||
|
||||
@ -17,6 +17,11 @@ const uploadCompanyLogo = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, data, 'Company logo updated successfully'));
|
||||
});
|
||||
|
||||
const uploadCompanyFavicon = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateCompanyFavicon(req.file, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'Company favicon updated successfully'));
|
||||
});
|
||||
|
||||
const getEmailSettings = asyncHandler(async (_req, res) => {
|
||||
const data = await service.getEmailSettings();
|
||||
res.json(new ApiResponse(200, data, 'Email settings fetched'));
|
||||
@ -31,6 +36,7 @@ module.exports = {
|
||||
getCompany,
|
||||
updateCompany,
|
||||
uploadCompanyLogo,
|
||||
uploadCompanyFavicon,
|
||||
getEmailSettings,
|
||||
updateEmailSettings,
|
||||
};
|
||||
|
||||
@ -18,6 +18,12 @@ router.post(
|
||||
upload.single('logo'),
|
||||
controller.uploadCompanyLogo
|
||||
);
|
||||
router.post(
|
||||
'/company/favicon',
|
||||
authorize('SETTINGS', 'edit'),
|
||||
upload.single('favicon'),
|
||||
controller.uploadCompanyFavicon
|
||||
);
|
||||
|
||||
router.get('/email', authorize('SETTINGS', 'view'), controller.getEmailSettings);
|
||||
router.put('/email', authorize('SETTINGS', 'edit'), validate(emailUpdateSchema), controller.updateEmailSettings);
|
||||
|
||||
@ -9,12 +9,20 @@ const { encrypt, decrypt } = require('../../utils/encryption');
|
||||
const COMPANY_ID = BigInt(1);
|
||||
const EMAIL_SETTINGS_ID = BigInt(1);
|
||||
|
||||
const buildLogoUrl = (logoPath) => {
|
||||
if (!logoPath) return null;
|
||||
const normalized = logoPath.replace(/\\/g, '/');
|
||||
const buildPublicUrl = (filePath) => {
|
||||
if (!filePath) return null;
|
||||
const normalized = filePath.replace(/\\/g, '/');
|
||||
return normalized.startsWith('/') ? normalized : `/${normalized}`;
|
||||
};
|
||||
|
||||
const unlinkIfExists = (relativePath) => {
|
||||
if (!relativePath) return;
|
||||
const absolute = path.resolve(process.cwd(), relativePath);
|
||||
if (fs.existsSync(absolute)) {
|
||||
fs.unlinkSync(absolute);
|
||||
}
|
||||
};
|
||||
|
||||
const sanitizeCompany = (row) => {
|
||||
if (!row) return null;
|
||||
return {
|
||||
@ -25,7 +33,9 @@ const sanitizeCompany = (row) => {
|
||||
email: row.email,
|
||||
website: row.website,
|
||||
logo_path: row.logo_path,
|
||||
logo_url: buildLogoUrl(row.logo_path),
|
||||
logo_url: buildPublicUrl(row.logo_path),
|
||||
favicon_path: row.favicon_path,
|
||||
favicon_url: buildPublicUrl(row.favicon_path),
|
||||
address: row.address,
|
||||
city: row.city,
|
||||
state: row.state,
|
||||
@ -106,13 +116,7 @@ const updateCompanyLogo = async (file, userId, requestId) => {
|
||||
if (!file) throw new ApiError(400, 'Logo file is required');
|
||||
|
||||
const existing = await getOrCreateCompany();
|
||||
|
||||
if (existing.logo_path) {
|
||||
const oldPath = path.resolve(process.cwd(), existing.logo_path);
|
||||
if (fs.existsSync(oldPath)) {
|
||||
fs.unlinkSync(oldPath);
|
||||
}
|
||||
}
|
||||
unlinkIfExists(existing.logo_path);
|
||||
|
||||
const logoPath = path.join(env.UPLOAD_DIR, file.filename).replace(/\\/g, '/');
|
||||
|
||||
@ -137,6 +141,35 @@ const updateCompanyLogo = async (file, userId, requestId) => {
|
||||
return sanitizeCompany(updated);
|
||||
};
|
||||
|
||||
const updateCompanyFavicon = async (file, userId, requestId) => {
|
||||
if (!file) throw new ApiError(400, 'Favicon file is required');
|
||||
|
||||
const existing = await getOrCreateCompany();
|
||||
unlinkIfExists(existing.favicon_path);
|
||||
|
||||
const faviconPath = path.join(env.UPLOAD_DIR, file.filename).replace(/\\/g, '/');
|
||||
|
||||
const updated = await prisma.company.update({
|
||||
where: { id: COMPANY_ID },
|
||||
data: {
|
||||
favicon_path: faviconPath,
|
||||
updated_by: userId ? BigInt(userId) : null,
|
||||
},
|
||||
});
|
||||
|
||||
await auditLog({
|
||||
tableName: 'company',
|
||||
recordId: COMPANY_ID,
|
||||
action: 'UPDATE',
|
||||
oldValue: { favicon_path: existing.favicon_path },
|
||||
newValue: { favicon_path: updated.favicon_path },
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
|
||||
return sanitizeCompany(updated);
|
||||
};
|
||||
|
||||
const getEmailSettings = async () => sanitizeEmailSettings(await getOrCreateEmailSettings());
|
||||
|
||||
const updateEmailSettings = async (payload, userId, requestId) => {
|
||||
@ -185,7 +218,8 @@ const getCompanyForDocuments = async () => {
|
||||
phone: row.mobile || '',
|
||||
email: row.email || '',
|
||||
website: row.website || '',
|
||||
logo_url: buildLogoUrl(row.logo_path),
|
||||
logo_url: buildPublicUrl(row.logo_path),
|
||||
favicon_url: buildPublicUrl(row.favicon_path),
|
||||
};
|
||||
};
|
||||
|
||||
@ -206,6 +240,7 @@ module.exports = {
|
||||
getCompany,
|
||||
updateCompany,
|
||||
updateCompanyLogo,
|
||||
updateCompanyFavicon,
|
||||
getEmailSettings,
|
||||
updateEmailSettings,
|
||||
getCompanyForDocuments,
|
||||
|
||||
38
src/utils/csv.js
Normal file
38
src/utils/csv.js
Normal file
@ -0,0 +1,38 @@
|
||||
const escapeCsv = (value) => {
|
||||
if (value === null || value === undefined) return '';
|
||||
const str = String(value);
|
||||
if (/[",\n\r]/.test(str)) return `"${str.replace(/"/g, '""')}"`;
|
||||
return str;
|
||||
};
|
||||
|
||||
const formatCsvCell = (value) => {
|
||||
if (value === null || value === undefined) return '';
|
||||
if (typeof value === 'bigint') return value.toString();
|
||||
if (value instanceof Date) return value.toISOString();
|
||||
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
||||
if (typeof value === 'object') {
|
||||
if (typeof value.toJSON === 'function') return formatCsvCell(value.toJSON());
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Array<{ key: string|Function, header: string }>} columns
|
||||
* @param {object[]} rows
|
||||
* @returns {string}
|
||||
*/
|
||||
const rowsToCsv = (columns, rows) => {
|
||||
const header = columns.map((col) => escapeCsv(col.header)).join(',');
|
||||
const lines = rows.map((row) =>
|
||||
columns
|
||||
.map((col) => {
|
||||
const raw = typeof col.key === 'function' ? col.key(row) : row[col.key];
|
||||
return escapeCsv(formatCsvCell(raw));
|
||||
})
|
||||
.join(',')
|
||||
);
|
||||
return [header, ...lines].join('\n');
|
||||
};
|
||||
|
||||
module.exports = { escapeCsv, formatCsvCell, rowsToCsv };
|
||||
11
src/utils/parseId.js
Normal file
11
src/utils/parseId.js
Normal file
@ -0,0 +1,11 @@
|
||||
const ApiError = require('./ApiError');
|
||||
|
||||
/** @param {string|number|bigint} id */
|
||||
const parseId = (id) => {
|
||||
if (id === undefined || id === null) throw new ApiError(400, 'Invalid id');
|
||||
const str = String(id).trim();
|
||||
if (!/^\d+$/.test(str)) throw new ApiError(400, 'Invalid id');
|
||||
return BigInt(str);
|
||||
};
|
||||
|
||||
module.exports = { parseId };
|
||||
@ -281,6 +281,9 @@ const generateGrnHtml = (inputData = getDummyGrnData()) => {
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
}
|
||||
table.items thead th.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
table.items tbody td {
|
||||
padding: 9px 3px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
|
||||
@ -276,6 +276,9 @@ const generatePoHtml = (inputData = getDummyPoData()) => {
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
}
|
||||
table.items thead th.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
table.items tbody td {
|
||||
padding: 10px 4px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user