diff --git a/BACKEND_TASKS.md b/BACKEND_TASKS.md index f5cb37a..61865d2 100644 --- a/BACKEND_TASKS.md +++ b/BACKEND_TASKS.md @@ -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` diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6d6c1bd..ad2bb72 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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 { diff --git a/scripts/patch-company-email-settings.sql b/scripts/patch-company-email-settings.sql index f31458e..b784a74 100644 --- a/scripts/patch-company-email-settings.sql +++ b/scripts/patch-company-email-settings.sql @@ -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), diff --git a/scripts/patch-company-favicon.sql b/scripts/patch-company-favicon.sql new file mode 100644 index 0000000..6bb44eb --- /dev/null +++ b/scripts/patch-company-favicon.sql @@ -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); diff --git a/src/docs/settings-routes.yaml b/src/docs/settings-routes.yaml index 95d0cde..146f756 100644 --- a/src/docs/settings-routes.yaml +++ b/src/docs/settings-routes.yaml @@ -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] diff --git a/src/modules/masters/_shared/master.factory.js b/src/modules/masters/_shared/master.factory.js index ae5e3fc..713869c 100644 --- a/src/modules/masters/_shared/master.factory.js +++ b/src/modules/masters/_shared/master.factory.js @@ -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 }; diff --git a/src/modules/masters/_shared/masters.validation.js b/src/modules/masters/_shared/masters.validation.js index 8d00752..4787093 100644 --- a/src/modules/masters/_shared/masters.validation.js +++ b/src/modules/masters/_shared/masters.validation.js @@ -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, }; diff --git a/src/modules/masters/brands/brands.controller.js b/src/modules/masters/brands/brands.controller.js index a21f053..c5a4117 100644 --- a/src/modules/masters/brands/brands.controller.js +++ b/src/modules/masters/brands/brands.controller.js @@ -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 }; + diff --git a/src/modules/masters/brands/brands.routes.js b/src/modules/masters/brands/brands.routes.js index 151d1ae..ff4cbab 100644 --- a/src/modules/masters/brands/brands.routes.js +++ b/src/modules/masters/brands/brands.routes.js @@ -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); diff --git a/src/modules/masters/brands/brands.service.js b/src/modules/masters/brands/brands.service.js index 9443898..ce839a1 100644 --- a/src/modules/masters/brands/brands.service.js +++ b/src/modules/masters/brands/brands.service.js @@ -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, diff --git a/src/modules/masters/brands/brands.validation.js b/src/modules/masters/brands/brands.validation.js index 2b102b4..a0aafc5 100644 --- a/src/modules/masters/brands/brands.validation.js +++ b/src/modules/masters/brands/brands.validation.js @@ -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 }; diff --git a/src/modules/masters/delivery-terms/delivery-terms.controller.js b/src/modules/masters/delivery-terms/delivery-terms.controller.js index 4522c9e..b0c486c 100644 --- a/src/modules/masters/delivery-terms/delivery-terms.controller.js +++ b/src/modules/masters/delivery-terms/delivery-terms.controller.js @@ -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 }; + diff --git a/src/modules/masters/delivery-terms/delivery-terms.routes.js b/src/modules/masters/delivery-terms/delivery-terms.routes.js index 3b9374c..b0c77d8 100644 --- a/src/modules/masters/delivery-terms/delivery-terms.routes.js +++ b/src/modules/masters/delivery-terms/delivery-terms.routes.js @@ -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); diff --git a/src/modules/masters/delivery-terms/delivery-terms.service.js b/src/modules/masters/delivery-terms/delivery-terms.service.js index 80c8d11..1b5db76 100644 --- a/src/modules/masters/delivery-terms/delivery-terms.service.js +++ b/src/modules/masters/delivery-terms/delivery-terms.service.js @@ -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, diff --git a/src/modules/masters/delivery-terms/delivery-terms.validation.js b/src/modules/masters/delivery-terms/delivery-terms.validation.js index f680f7a..e8cd9b9 100644 --- a/src/modules/masters/delivery-terms/delivery-terms.validation.js +++ b/src/modules/masters/delivery-terms/delivery-terms.validation.js @@ -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 }; diff --git a/src/modules/masters/departments/departments.controller.js b/src/modules/masters/departments/departments.controller.js index 2d99418..cbeca86 100644 --- a/src/modules/masters/departments/departments.controller.js +++ b/src/modules/masters/departments/departments.controller.js @@ -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 }; + diff --git a/src/modules/masters/departments/departments.routes.js b/src/modules/masters/departments/departments.routes.js index 2d50fc1..8f1faca 100644 --- a/src/modules/masters/departments/departments.routes.js +++ b/src/modules/masters/departments/departments.routes.js @@ -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); diff --git a/src/modules/masters/departments/departments.service.js b/src/modules/masters/departments/departments.service.js index 5cb2f24..64971a6 100644 --- a/src/modules/masters/departments/departments.service.js +++ b/src/modules/masters/departments/departments.service.js @@ -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, diff --git a/src/modules/masters/departments/departments.validation.js b/src/modules/masters/departments/departments.validation.js index 0829b11..b89250d 100644 --- a/src/modules/masters/departments/departments.validation.js +++ b/src/modules/masters/departments/departments.validation.js @@ -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 }; diff --git a/src/modules/masters/designations/designations.controller.js b/src/modules/masters/designations/designations.controller.js index a425fad..739b33d 100644 --- a/src/modules/masters/designations/designations.controller.js +++ b/src/modules/masters/designations/designations.controller.js @@ -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 }; + diff --git a/src/modules/masters/designations/designations.routes.js b/src/modules/masters/designations/designations.routes.js index 76fc6e2..62ec4e8 100644 --- a/src/modules/masters/designations/designations.routes.js +++ b/src/modules/masters/designations/designations.routes.js @@ -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); diff --git a/src/modules/masters/designations/designations.service.js b/src/modules/masters/designations/designations.service.js index b188ca1..1b9a524 100644 --- a/src/modules/masters/designations/designations.service.js +++ b/src/modules/masters/designations/designations.service.js @@ -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, diff --git a/src/modules/masters/designations/designations.validation.js b/src/modules/masters/designations/designations.validation.js index 0829b11..b89250d 100644 --- a/src/modules/masters/designations/designations.validation.js +++ b/src/modules/masters/designations/designations.validation.js @@ -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 }; diff --git a/src/modules/masters/document-series/document-series.controller.js b/src/modules/masters/document-series/document-series.controller.js index 208a753..76b97b9 100644 --- a/src/modules/masters/document-series/document-series.controller.js +++ b/src/modules/masters/document-series/document-series.controller.js @@ -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 }; + diff --git a/src/modules/masters/document-series/document-series.routes.js b/src/modules/masters/document-series/document-series.routes.js index 6afde95..5ee7fbf 100644 --- a/src/modules/masters/document-series/document-series.routes.js +++ b/src/modules/masters/document-series/document-series.routes.js @@ -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); diff --git a/src/modules/masters/document-series/document-series.service.js b/src/modules/masters/document-series/document-series.service.js index 1730686..c614a5d 100644 --- a/src/modules/masters/document-series/document-series.service.js +++ b/src/modules/masters/document-series/document-series.service.js @@ -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, diff --git a/src/modules/masters/document-series/document-series.validation.js b/src/modules/masters/document-series/document-series.validation.js index e0b9bfc..5374c51 100644 --- a/src/modules/masters/document-series/document-series.validation.js +++ b/src/modules/masters/document-series/document-series.validation.js @@ -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 }; diff --git a/src/modules/masters/gst-rates/gst-rates.controller.js b/src/modules/masters/gst-rates/gst-rates.controller.js index d36f7f0..cfe0ba5 100644 --- a/src/modules/masters/gst-rates/gst-rates.controller.js +++ b/src/modules/masters/gst-rates/gst-rates.controller.js @@ -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 }; + diff --git a/src/modules/masters/gst-rates/gst-rates.routes.js b/src/modules/masters/gst-rates/gst-rates.routes.js index f56b51d..ad0d346 100644 --- a/src/modules/masters/gst-rates/gst-rates.routes.js +++ b/src/modules/masters/gst-rates/gst-rates.routes.js @@ -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); diff --git a/src/modules/masters/gst-rates/gst-rates.service.js b/src/modules/masters/gst-rates/gst-rates.service.js index b617957..ad5c7bf 100644 --- a/src/modules/masters/gst-rates/gst-rates.service.js +++ b/src/modules/masters/gst-rates/gst-rates.service.js @@ -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, diff --git a/src/modules/masters/gst-rates/gst-rates.validation.js b/src/modules/masters/gst-rates/gst-rates.validation.js index 27a3b0c..bac41d9 100644 --- a/src/modules/masters/gst-rates/gst-rates.validation.js +++ b/src/modules/masters/gst-rates/gst-rates.validation.js @@ -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 }; diff --git a/src/modules/masters/hsn-codes/hsn-codes.controller.js b/src/modules/masters/hsn-codes/hsn-codes.controller.js index fabf61a..e967597 100644 --- a/src/modules/masters/hsn-codes/hsn-codes.controller.js +++ b/src/modules/masters/hsn-codes/hsn-codes.controller.js @@ -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 }; + diff --git a/src/modules/masters/hsn-codes/hsn-codes.routes.js b/src/modules/masters/hsn-codes/hsn-codes.routes.js index 5ca1246..4166044 100644 --- a/src/modules/masters/hsn-codes/hsn-codes.routes.js +++ b/src/modules/masters/hsn-codes/hsn-codes.routes.js @@ -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); diff --git a/src/modules/masters/hsn-codes/hsn-codes.service.js b/src/modules/masters/hsn-codes/hsn-codes.service.js index 0aea82b..6054f03 100644 --- a/src/modules/masters/hsn-codes/hsn-codes.service.js +++ b/src/modules/masters/hsn-codes/hsn-codes.service.js @@ -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, diff --git a/src/modules/masters/hsn-codes/hsn-codes.validation.js b/src/modules/masters/hsn-codes/hsn-codes.validation.js index b53d7fc..ff2b57c 100644 --- a/src/modules/masters/hsn-codes/hsn-codes.validation.js +++ b/src/modules/masters/hsn-codes/hsn-codes.validation.js @@ -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 }; diff --git a/src/modules/masters/item-categories/item-categories.controller.js b/src/modules/masters/item-categories/item-categories.controller.js index 01ebf72..c4ee34c 100644 --- a/src/modules/masters/item-categories/item-categories.controller.js +++ b/src/modules/masters/item-categories/item-categories.controller.js @@ -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 }; + diff --git a/src/modules/masters/item-categories/item-categories.routes.js b/src/modules/masters/item-categories/item-categories.routes.js index 5e09cde..4ff294a 100644 --- a/src/modules/masters/item-categories/item-categories.routes.js +++ b/src/modules/masters/item-categories/item-categories.routes.js @@ -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); diff --git a/src/modules/masters/item-categories/item-categories.service.js b/src/modules/masters/item-categories/item-categories.service.js index daa0318..56e4717 100644 --- a/src/modules/masters/item-categories/item-categories.service.js +++ b/src/modules/masters/item-categories/item-categories.service.js @@ -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), diff --git a/src/modules/masters/item-categories/item-categories.validation.js b/src/modules/masters/item-categories/item-categories.validation.js index 84f3e48..b9e5005 100644 --- a/src/modules/masters/item-categories/item-categories.validation.js +++ b/src/modules/masters/item-categories/item-categories.validation.js @@ -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 }; diff --git a/src/modules/masters/item-subcategories/item-subcategories.controller.js b/src/modules/masters/item-subcategories/item-subcategories.controller.js index b831061..bd1a642 100644 --- a/src/modules/masters/item-subcategories/item-subcategories.controller.js +++ b/src/modules/masters/item-subcategories/item-subcategories.controller.js @@ -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 }; + diff --git a/src/modules/masters/item-subcategories/item-subcategories.routes.js b/src/modules/masters/item-subcategories/item-subcategories.routes.js index f811e46..79e1853 100644 --- a/src/modules/masters/item-subcategories/item-subcategories.routes.js +++ b/src/modules/masters/item-subcategories/item-subcategories.routes.js @@ -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); diff --git a/src/modules/masters/item-subcategories/item-subcategories.service.js b/src/modules/masters/item-subcategories/item-subcategories.service.js index 57ab653..0efdf47 100644 --- a/src/modules/masters/item-subcategories/item-subcategories.service.js +++ b/src/modules/masters/item-subcategories/item-subcategories.service.js @@ -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, diff --git a/src/modules/masters/item-subcategories/item-subcategories.validation.js b/src/modules/masters/item-subcategories/item-subcategories.validation.js index 01658aa..f995102 100644 --- a/src/modules/masters/item-subcategories/item-subcategories.validation.js +++ b/src/modules/masters/item-subcategories/item-subcategories.validation.js @@ -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 }; diff --git a/src/modules/masters/items/items.controller.js b/src/modules/masters/items/items.controller.js index 127fb8b..60bb67d 100644 --- a/src/modules/masters/items/items.controller.js +++ b/src/modules/masters/items/items.controller.js @@ -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 }; + diff --git a/src/modules/masters/items/items.routes.js b/src/modules/masters/items/items.routes.js index a1cd828..d4cb860 100644 --- a/src/modules/masters/items/items.routes.js +++ b/src/modules/masters/items/items.routes.js @@ -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); diff --git a/src/modules/masters/items/items.service.js b/src/modules/masters/items/items.service.js index e0bf70c..3c8524f 100644 --- a/src/modules/masters/items/items.service.js +++ b/src/modules/masters/items/items.service.js @@ -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, diff --git a/src/modules/masters/items/items.validation.js b/src/modules/masters/items/items.validation.js index daf9534..b5dbb6e 100644 --- a/src/modules/masters/items/items.validation.js +++ b/src/modules/masters/items/items.validation.js @@ -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 }; diff --git a/src/modules/masters/locations/locations.controller.js b/src/modules/masters/locations/locations.controller.js index 3d9ca22..427d3ee 100644 --- a/src/modules/masters/locations/locations.controller.js +++ b/src/modules/masters/locations/locations.controller.js @@ -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 }; + diff --git a/src/modules/masters/locations/locations.routes.js b/src/modules/masters/locations/locations.routes.js index 1b95afb..05a6d53 100644 --- a/src/modules/masters/locations/locations.routes.js +++ b/src/modules/masters/locations/locations.routes.js @@ -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); diff --git a/src/modules/masters/locations/locations.service.js b/src/modules/masters/locations/locations.service.js index 4c1be1a..f7ba091 100644 --- a/src/modules/masters/locations/locations.service.js +++ b/src/modules/masters/locations/locations.service.js @@ -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, diff --git a/src/modules/masters/locations/locations.validation.js b/src/modules/masters/locations/locations.validation.js index bb0e113..09a2385 100644 --- a/src/modules/masters/locations/locations.validation.js +++ b/src/modules/masters/locations/locations.validation.js @@ -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, }; diff --git a/src/modules/masters/payment-terms/payment-terms.controller.js b/src/modules/masters/payment-terms/payment-terms.controller.js index 6db8d51..2bf5e16 100644 --- a/src/modules/masters/payment-terms/payment-terms.controller.js +++ b/src/modules/masters/payment-terms/payment-terms.controller.js @@ -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 }; + diff --git a/src/modules/masters/payment-terms/payment-terms.routes.js b/src/modules/masters/payment-terms/payment-terms.routes.js index 0e84025..6254303 100644 --- a/src/modules/masters/payment-terms/payment-terms.routes.js +++ b/src/modules/masters/payment-terms/payment-terms.routes.js @@ -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); diff --git a/src/modules/masters/payment-terms/payment-terms.service.js b/src/modules/masters/payment-terms/payment-terms.service.js index 78f3a84..c343132 100644 --- a/src/modules/masters/payment-terms/payment-terms.service.js +++ b/src/modules/masters/payment-terms/payment-terms.service.js @@ -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, diff --git a/src/modules/masters/payment-terms/payment-terms.validation.js b/src/modules/masters/payment-terms/payment-terms.validation.js index 0615f07..f44fb46 100644 --- a/src/modules/masters/payment-terms/payment-terms.validation.js +++ b/src/modules/masters/payment-terms/payment-terms.validation.js @@ -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 }; diff --git a/src/modules/masters/plants/plants.controller.js b/src/modules/masters/plants/plants.controller.js index b66e623..cdff6cf 100644 --- a/src/modules/masters/plants/plants.controller.js +++ b/src/modules/masters/plants/plants.controller.js @@ -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 }; + diff --git a/src/modules/masters/plants/plants.routes.js b/src/modules/masters/plants/plants.routes.js index 58197b2..3f30030 100644 --- a/src/modules/masters/plants/plants.routes.js +++ b/src/modules/masters/plants/plants.routes.js @@ -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); diff --git a/src/modules/masters/plants/plants.service.js b/src/modules/masters/plants/plants.service.js index 2a01c75..43ce417 100644 --- a/src/modules/masters/plants/plants.service.js +++ b/src/modules/masters/plants/plants.service.js @@ -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, diff --git a/src/modules/masters/plants/plants.validation.js b/src/modules/masters/plants/plants.validation.js index ab851b1..89824ab 100644 --- a/src/modules/masters/plants/plants.validation.js +++ b/src/modules/masters/plants/plants.validation.js @@ -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 }; diff --git a/src/modules/masters/uom/uom.controller.js b/src/modules/masters/uom/uom.controller.js index 53ed9f5..3535a97 100644 --- a/src/modules/masters/uom/uom.controller.js +++ b/src/modules/masters/uom/uom.controller.js @@ -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 }; + diff --git a/src/modules/masters/uom/uom.routes.js b/src/modules/masters/uom/uom.routes.js index cfd6642..c0e7ea3 100644 --- a/src/modules/masters/uom/uom.routes.js +++ b/src/modules/masters/uom/uom.routes.js @@ -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); diff --git a/src/modules/masters/uom/uom.service.js b/src/modules/masters/uom/uom.service.js index 7b4df77..b369bec 100644 --- a/src/modules/masters/uom/uom.service.js +++ b/src/modules/masters/uom/uom.service.js @@ -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 }; diff --git a/src/modules/masters/uom/uom.validation.js b/src/modules/masters/uom/uom.validation.js index 1e3196f..369e4c7 100644 --- a/src/modules/masters/uom/uom.validation.js +++ b/src/modules/masters/uom/uom.validation.js @@ -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 }; diff --git a/src/modules/masters/warehouses/warehouses.controller.js b/src/modules/masters/warehouses/warehouses.controller.js index 71e2b4c..ce6c0fe 100644 --- a/src/modules/masters/warehouses/warehouses.controller.js +++ b/src/modules/masters/warehouses/warehouses.controller.js @@ -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 }; + diff --git a/src/modules/masters/warehouses/warehouses.routes.js b/src/modules/masters/warehouses/warehouses.routes.js index b9f1149..c0ca7e9 100644 --- a/src/modules/masters/warehouses/warehouses.routes.js +++ b/src/modules/masters/warehouses/warehouses.routes.js @@ -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); diff --git a/src/modules/masters/warehouses/warehouses.service.js b/src/modules/masters/warehouses/warehouses.service.js index 625114e..3445dca 100644 --- a/src/modules/masters/warehouses/warehouses.service.js +++ b/src/modules/masters/warehouses/warehouses.service.js @@ -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, diff --git a/src/modules/masters/warehouses/warehouses.validation.js b/src/modules/masters/warehouses/warehouses.validation.js index 5fdf27d..713374b 100644 --- a/src/modules/masters/warehouses/warehouses.validation.js +++ b/src/modules/masters/warehouses/warehouses.validation.js @@ -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 }; diff --git a/src/modules/settings/settings.controller.js b/src/modules/settings/settings.controller.js index 471393b..ab4aa01 100644 --- a/src/modules/settings/settings.controller.js +++ b/src/modules/settings/settings.controller.js @@ -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, }; diff --git a/src/modules/settings/settings.routes.js b/src/modules/settings/settings.routes.js index bce685b..bf89015 100644 --- a/src/modules/settings/settings.routes.js +++ b/src/modules/settings/settings.routes.js @@ -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); diff --git a/src/modules/settings/settings.service.js b/src/modules/settings/settings.service.js index 029675d..6e6ce66 100644 --- a/src/modules/settings/settings.service.js +++ b/src/modules/settings/settings.service.js @@ -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, diff --git a/src/utils/csv.js b/src/utils/csv.js new file mode 100644 index 0000000..e90c1ec --- /dev/null +++ b/src/utils/csv.js @@ -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 }; diff --git a/src/utils/parseId.js b/src/utils/parseId.js new file mode 100644 index 0000000..a2ac582 --- /dev/null +++ b/src/utils/parseId.js @@ -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 }; diff --git a/src/utils/pdf/templates/grn.template.js b/src/utils/pdf/templates/grn.template.js index 743782a..3653d38 100644 --- a/src/utils/pdf/templates/grn.template.js +++ b/src/utils/pdf/templates/grn.template.js @@ -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; diff --git a/src/utils/pdf/templates/po.template.js b/src/utils/pdf/templates/po.template.js index 6a26526..a23c0b1 100644 --- a/src/utils/pdf/templates/po.template.js +++ b/src/utils/pdf/templates/po.template.js @@ -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;