GWM : favicon upload , gsitn update in company , excell export api error fixed

This commit is contained in:
Gowtham M 2026-07-14 10:55:48 +05:30
parent 969647333f
commit 710b0b33c6
74 changed files with 691 additions and 162 deletions

View File

@ -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`

View File

@ -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 {

View File

@ -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),

View 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);

View File

@ -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]

View File

@ -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 };

View File

@ -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,
};

View File

@ -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 };

View File

@ -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);

View File

@ -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,

View File

@ -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 };

View File

@ -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 };

View File

@ -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);

View File

@ -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,

View File

@ -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 };

View File

@ -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 };

View File

@ -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);

View File

@ -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,

View File

@ -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 };

View File

@ -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 };

View File

@ -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);

View File

@ -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,

View File

@ -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 };

View File

@ -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 };

View File

@ -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);

View File

@ -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,

View File

@ -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 };

View File

@ -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 };

View File

@ -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);

View File

@ -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,

View File

@ -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 };

View File

@ -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 };

View File

@ -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);

View File

@ -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,

View File

@ -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 };

View File

@ -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 };

View File

@ -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);

View File

@ -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),

View File

@ -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 };

View File

@ -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 };

View File

@ -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);

View File

@ -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,

View File

@ -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 };

View File

@ -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 };

View File

@ -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);

View File

@ -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,

View File

@ -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 };

View File

@ -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 };

View File

@ -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);

View File

@ -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,

View File

@ -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,
};

View File

@ -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 };

View File

@ -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);

View File

@ -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,

View File

@ -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 };

View File

@ -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 };

View File

@ -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);

View File

@ -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,

View File

@ -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 };

View File

@ -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 };

View File

@ -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);

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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);

View File

@ -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,

View File

@ -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 };

View File

@ -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,
};

View File

@ -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);

View File

@ -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
View 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
View 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 };

View File

@ -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;

View File

@ -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;