diff --git a/src/modules/assets/assets.controller.js b/src/modules/assets/assets.controller.js index cd865b5..12a5f9c 100644 --- a/src/modules/assets/assets.controller.js +++ b/src/modules/assets/assets.controller.js @@ -18,6 +18,13 @@ const list = asyncHandler(async (req, res) => { res.json(new ApiResponse(200, result.data, 'Assets fetched', result.meta)); }); +const exportCsv = asyncHandler(async (req, res) => { + const csv = await service.exportAssets(req.query); + res.setHeader('Content-Type', 'text/csv; charset=utf-8'); + res.setHeader('Content-Disposition', 'attachment; filename="assets-export.csv"'); + res.send(csv); +}); + const getOne = asyncHandler(async (req, res) => { const data = await service.getAssetById(req.params.id); res.json(new ApiResponse(200, data, 'Asset fetched')); @@ -276,6 +283,7 @@ const removeAttachment = asyncHandler(async (req, res) => { module.exports = { create, list, + exportCsv, getOne, update, remove, diff --git a/src/modules/assets/assets.routes.js b/src/modules/assets/assets.routes.js index e6c6b81..c4d5496 100644 --- a/src/modules/assets/assets.routes.js +++ b/src/modules/assets/assets.routes.js @@ -8,6 +8,7 @@ const { createAssetSchema, updateAssetSchema, listAssetsQuerySchema, + exportAssetsQuerySchema, transferAssetSchema, amcContractSchema, updateAmcContractSchema, @@ -28,6 +29,12 @@ const router = express.Router(); router.use(authenticate); +router.get( + '/export', + authorize('ASSET', 'export'), + validate(exportAssetsQuerySchema, 'query'), + controller.exportCsv +); router.get( '/', authorize('ASSET', 'view'), diff --git a/src/modules/assets/assets.service.js b/src/modules/assets/assets.service.js index f6dbe42..8646c61 100644 --- a/src/modules/assets/assets.service.js +++ b/src/modules/assets/assets.service.js @@ -3,6 +3,7 @@ const ApiError = require('../../utils/ApiError'); const auditLog = require('../../utils/auditLog'); const { getPagination } = require('../../utils/pagination'); const { nextDocumentNumber } = require('../../utils/generateCode'); +const { rowsToCsv } = require('../../utils/csv'); const { DISPOSAL_STATUSES, getAssetDropdownOptions } = require('./assets.constants'); const { assertPlant, assertWarehouse } = require('../../utils/locations'); const repository = require('./assets.repository'); @@ -307,30 +308,32 @@ const createAsset = async (payload, userId, requestId) => { return sanitizeAsset(created); }; +const buildAssetsWhere = (query) => ({ + deleted_at: null, + ...(query.status ? { status: query.status } : {}), + ...(query.condition ? { condition: query.condition } : {}), + ...(query.item_category_id ? { item_category_id: BigInt(query.item_category_id) } : {}), + ...(query.item_subcategory_id + ? { item_subcategory_id: BigInt(query.item_subcategory_id) } + : {}), + ...(query.plant_id ? { plant_id: BigInt(query.plant_id) } : {}), + ...(query.department_id ? { department_id: BigInt(query.department_id) } : {}), + ...(query.is_active !== undefined ? { is_active: query.is_active } : {}), + ...(query.search + ? { + OR: [ + { asset_code: { contains: query.search, mode: 'insensitive' } }, + { asset_name: { contains: query.search, mode: 'insensitive' } }, + { serial_number: { contains: query.search, mode: 'insensitive' } }, + { qr_code_value: { contains: query.search, mode: 'insensitive' } }, + ], + } + : {}), +}); + const listAssets = async (query) => { const { page, limit, skip } = getPagination(query); - const where = { - deleted_at: null, - ...(query.status ? { status: query.status } : {}), - ...(query.condition ? { condition: query.condition } : {}), - ...(query.item_category_id ? { item_category_id: BigInt(query.item_category_id) } : {}), - ...(query.item_subcategory_id - ? { item_subcategory_id: BigInt(query.item_subcategory_id) } - : {}), - ...(query.plant_id ? { plant_id: BigInt(query.plant_id) } : {}), - ...(query.department_id ? { department_id: BigInt(query.department_id) } : {}), - ...(query.is_active !== undefined ? { is_active: query.is_active } : {}), - ...(query.search - ? { - OR: [ - { asset_code: { contains: query.search, mode: 'insensitive' } }, - { asset_name: { contains: query.search, mode: 'insensitive' } }, - { serial_number: { contains: query.search, mode: 'insensitive' } }, - { qr_code_value: { contains: query.search, mode: 'insensitive' } }, - ], - } - : {}), - }; + const where = buildAssetsWhere(query); const [rows, total] = await Promise.all([ prisma.assets.findMany({ @@ -346,6 +349,39 @@ const listAssets = async (query) => { return { data: rows.map(sanitizeAsset), meta: { page, limit, total } }; }; +const exportAssets = async (query) => { + const rows = await prisma.assets.findMany({ + where: buildAssetsWhere(query), + include: assetInclude, + orderBy: { created_at: 'desc' }, + }); + + return rowsToCsv( + [ + { key: 'asset_code', header: 'Asset Code' }, + { key: 'asset_name', header: 'Asset Name' }, + { key: (row) => row.item_category?.name || '', header: 'Category' }, + { key: (row) => row.item_subcategory?.name || '', header: 'Subcategory' }, + { key: 'serial_number', header: 'Serial Number' }, + { key: 'condition', header: 'Condition' }, + { key: 'status', header: 'Status' }, + { key: (row) => row.plant?.name || '', header: 'Plant' }, + { key: (row) => row.department?.name || '', header: 'Department' }, + { key: (row) => row.warehouse?.name || '', header: 'Warehouse' }, + { key: (row) => row.assigned_to_user?.full_name || '', header: 'Assigned To' }, + { key: (row) => row.vendor?.vendor_name || '', header: 'Vendor' }, + { key: (row) => row.purchase_order?.po_number || '', header: 'PO Number' }, + { key: (row) => row.grn?.grn_number || '', header: 'GRN Number' }, + { key: 'purchase_date', header: 'Purchase Date', type: 'date' }, + { key: 'purchase_cost', header: 'Purchase Cost' }, + { key: 'warranty_expiry_date', header: 'Warranty Expiry', type: 'date' }, + { key: 'is_active', header: 'Active' }, + { key: 'created_at', header: 'Created At', type: 'datetime' }, + ], + rows.map(sanitizeAsset) + ); +}; + const getAssetById = async (id) => sanitizeAsset(await getAssetOrThrow(id)); const updateAsset = async (id, payload, userId, requestId) => { @@ -566,6 +602,7 @@ const previewDepreciation = (payload) => { module.exports = { createAsset, listAssets, + exportAssets, getAssetById, updateAsset, deleteAsset, diff --git a/src/modules/assets/assets.validation.js b/src/modules/assets/assets.validation.js index 9574386..55c6a21 100644 --- a/src/modules/assets/assets.validation.js +++ b/src/modules/assets/assets.validation.js @@ -108,6 +108,11 @@ const listAssetsQuerySchema = Joi.object({ is_active: Joi.boolean().optional(), }); +const exportAssetsQuerySchema = listAssetsQuerySchema.keys({ + page: Joi.strip(), + limit: Joi.strip(), +}); + const transferAssetSchema = Joi.object({ transfer_date: Joi.date().iso().required(), to_plant_id: Joi.number().integer().positive().allow(null).optional(), @@ -305,6 +310,7 @@ module.exports = { createAssetSchema, updateAssetSchema, listAssetsQuerySchema, + exportAssetsQuerySchema, transferAssetSchema, amcContractSchema, updateAmcContractSchema, diff --git a/src/modules/grn/grn.controller.js b/src/modules/grn/grn.controller.js index 3fc888a..8fb0dd5 100644 --- a/src/modules/grn/grn.controller.js +++ b/src/modules/grn/grn.controller.js @@ -14,6 +14,13 @@ const list = asyncHandler(async (req, res) => { res.json(new ApiResponse(200, result.data, 'GRNs fetched', result.meta)); }); +const exportCsv = asyncHandler(async (req, res) => { + const csv = await service.exportGrns(req.query); + res.setHeader('Content-Type', 'text/csv; charset=utf-8'); + res.setHeader('Content-Disposition', 'attachment; filename="grn-export.csv"'); + res.send(csv); +}); + const getOne = asyncHandler(async (req, res) => { const data = await service.getGrnById(req.params.id); res.json(new ApiResponse(200, data, 'GRN fetched')); @@ -85,6 +92,7 @@ const removeAttachment = asyncHandler(async (req, res) => { module.exports = { create, list, + exportCsv, getOne, update, cancel, diff --git a/src/modules/grn/grn.routes.js b/src/modules/grn/grn.routes.js index 7736025..a81f6fa 100644 --- a/src/modules/grn/grn.routes.js +++ b/src/modules/grn/grn.routes.js @@ -8,6 +8,7 @@ const { createGrnSchema, updateGrnSchema, listGrnQuerySchema, + exportGrnQuerySchema, cancelGrnSchema, } = require('./grn.validation'); @@ -15,6 +16,12 @@ const router = express.Router(); router.use(authenticate); +router.get( + '/export', + authorize('GRN', 'export'), + validate(exportGrnQuerySchema, 'query'), + controller.exportCsv +); router.get('/', authorize('GRN', 'view'), validate(listGrnQuerySchema, 'query'), controller.list); router.post('/', authorize('GRN', 'create'), validate(createGrnSchema), controller.create); diff --git a/src/modules/grn/grn.service.js b/src/modules/grn/grn.service.js index a1cb390..95e162b 100644 --- a/src/modules/grn/grn.service.js +++ b/src/modules/grn/grn.service.js @@ -7,6 +7,7 @@ const { generatePdf } = require('../../utils/pdf/pdfGenerator'); const { generateGrnHtml } = require('../../utils/pdf/templates/grn.template'); const { formatDate } = require('../../utils/pdf/helpers/formatDate'); const { getCompanyForDocuments } = require('../settings/settings.service'); +const { rowsToCsv } = require('../../utils/csv'); const repository = require('./grn.repository'); const { assertWarehouse } = require('../../utils/locations'); const { sanitizeAttachment } = require('./grn.attachments.service'); @@ -445,24 +446,26 @@ const createGrn = async (payload, userId, requestId) => { return sanitizeGrn(detail); }; +const buildGrnsWhere = (query) => ({ + deleted_at: null, + ...(query.status ? { status: query.status } : {}), + ...(query.po_id ? { po_id: BigInt(query.po_id) } : {}), + ...(query.vendor_id ? { vendor_id: BigInt(query.vendor_id) } : {}), + ...(query.warehouse_id ? { warehouse_id: BigInt(query.warehouse_id) } : {}), + ...(query.search ? { grn_number: { contains: query.search, mode: 'insensitive' } } : {}), + ...(query.date_from || query.date_to + ? { + grn_date: { + ...(query.date_from ? { gte: toDateOnly(query.date_from) } : {}), + ...(query.date_to ? { lte: toDateOnly(query.date_to) } : {}), + }, + } + : {}), +}); + const listGrns = async (query) => { const { page, limit, skip } = getPagination(query); - const where = { - deleted_at: null, - ...(query.status ? { status: query.status } : {}), - ...(query.po_id ? { po_id: BigInt(query.po_id) } : {}), - ...(query.vendor_id ? { vendor_id: BigInt(query.vendor_id) } : {}), - ...(query.warehouse_id ? { warehouse_id: BigInt(query.warehouse_id) } : {}), - ...(query.search ? { grn_number: { contains: query.search, mode: 'insensitive' } } : {}), - ...(query.date_from || query.date_to - ? { - grn_date: { - ...(query.date_from ? { gte: toDateOnly(query.date_from) } : {}), - ...(query.date_to ? { lte: toDateOnly(query.date_to) } : {}), - }, - } - : {}), - }; + const where = buildGrnsWhere(query); const [rows, total] = await Promise.all([ prisma.grn.findMany({ @@ -478,6 +481,35 @@ const listGrns = async (query) => { return { data: rows.map(sanitizeGrn), meta: { page, limit, total } }; }; +const exportGrns = async (query) => { + const rows = await prisma.grn.findMany({ + where: buildGrnsWhere(query), + include: grnListInclude, + orderBy: { created_at: 'desc' }, + }); + + return rowsToCsv( + [ + { key: 'grn_number', header: 'GRN Number' }, + { key: 'grn_date', header: 'GRN Date', type: 'date' }, + { key: 'status', header: 'Status' }, + { key: (row) => row.purchase_order?.po_number || '', header: 'PO Number' }, + { key: (row) => row.vendor?.vendor_code || '', header: 'Vendor Code' }, + { key: (row) => row.vendor?.vendor_name || '', header: 'Vendor Name' }, + { key: (row) => row.warehouse?.name || '', header: 'Warehouse' }, + { key: 'vendor_invoice_no', header: 'Vendor Invoice No' }, + { key: 'vendor_invoice_date', header: 'Vendor Invoice Date', type: 'date' }, + { key: 'vendor_invoice_amount', header: 'Vendor Invoice Amount' }, + { key: 'vehicle_no', header: 'Vehicle No' }, + { key: 'lr_no', header: 'LR No' }, + { key: (row) => row.received_by_user?.full_name || '', header: 'Received By' }, + { key: (row) => row.created_by_user?.full_name || '', header: 'Created By' }, + { key: 'created_at', header: 'Created At', type: 'datetime' }, + ], + rows.map(sanitizeGrn) + ); +}; + const getGrnById = async (id) => sanitizeGrn(await getGrnOrThrow(id, { includeItems: true })); const updateGrn = async (id, payload, userId, requestId) => { @@ -582,6 +614,7 @@ const getGrnPdf = async (id) => { module.exports = { createGrn, listGrns, + exportGrns, getGrnById, updateGrn, cancelGrn, diff --git a/src/modules/grn/grn.validation.js b/src/modules/grn/grn.validation.js index 3f7db54..ec6d6ba 100644 --- a/src/modules/grn/grn.validation.js +++ b/src/modules/grn/grn.validation.js @@ -62,6 +62,11 @@ const listGrnQuerySchema = Joi.object({ date_to: Joi.date().iso().optional(), }); +const exportGrnQuerySchema = listGrnQuerySchema.keys({ + page: Joi.strip(), + limit: Joi.strip(), +}); + const cancelGrnSchema = Joi.object({ cancellation_reason: Joi.string().trim().min(1).required(), }); @@ -70,5 +75,6 @@ module.exports = { createGrnSchema, updateGrnSchema, listGrnQuerySchema, + exportGrnQuerySchema, cancelGrnSchema, }; diff --git a/src/modules/purchase-orders/purchase-orders.controller.js b/src/modules/purchase-orders/purchase-orders.controller.js index 5b15a78..e7a1169 100644 --- a/src/modules/purchase-orders/purchase-orders.controller.js +++ b/src/modules/purchase-orders/purchase-orders.controller.js @@ -14,6 +14,13 @@ const list = asyncHandler(async (req, res) => { res.json(new ApiResponse(200, result.data, 'Purchase orders fetched', result.meta)); }); +const exportCsv = asyncHandler(async (req, res) => { + const csv = await service.exportPurchaseOrders(req.query); + res.setHeader('Content-Type', 'text/csv; charset=utf-8'); + res.setHeader('Content-Disposition', 'attachment; filename="purchase-orders-export.csv"'); + res.send(csv); +}); + const getOne = asyncHandler(async (req, res) => { const data = await service.getPurchaseOrderById(req.params.id); res.json(new ApiResponse(200, data, 'Purchase order fetched')); @@ -110,6 +117,7 @@ const removeAttachment = asyncHandler(async (req, res) => { module.exports = { create, list, + exportCsv, getOne, update, remove, diff --git a/src/modules/purchase-orders/purchase-orders.routes.js b/src/modules/purchase-orders/purchase-orders.routes.js index a43e3d5..2c581f5 100644 --- a/src/modules/purchase-orders/purchase-orders.routes.js +++ b/src/modules/purchase-orders/purchase-orders.routes.js @@ -9,6 +9,7 @@ const { updatePurchaseOrderSchema, amendPurchaseOrderSchema, listPurchaseOrdersQuerySchema, + exportPurchaseOrdersQuerySchema, workflowRemarksSchema, rejectPurchaseOrderSchema, } = require('./purchase-orders.validation'); @@ -17,6 +18,12 @@ const router = express.Router(); router.use(authenticate); +router.get( + '/export', + authorize('PURCHASE_ORDER', 'export'), + validate(exportPurchaseOrdersQuerySchema, 'query'), + controller.exportCsv +); router.get( '/', authorize('PURCHASE_ORDER', 'view'), diff --git a/src/modules/purchase-orders/purchase-orders.service.js b/src/modules/purchase-orders/purchase-orders.service.js index 048d651..6bef021 100644 --- a/src/modules/purchase-orders/purchase-orders.service.js +++ b/src/modules/purchase-orders/purchase-orders.service.js @@ -7,6 +7,7 @@ const { generatePdf } = require('../../utils/pdf/pdfGenerator'); const { generatePoHtml } = require('../../utils/pdf/templates/po.template'); const { formatDate } = require('../../utils/pdf/helpers/formatDate'); const { getCompanyForDocuments } = require('../settings/settings.service'); +const { rowsToCsv } = require('../../utils/csv'); const { EDITABLE_STATUSES, SUBMITTABLE_STATUSES, @@ -429,24 +430,26 @@ const createPurchaseOrder = async (payload, userId, requestId) => { return sanitizePo(created); }; +const buildPurchaseOrdersWhere = (query) => ({ + deleted_at: null, + ...(query.status ? { status: query.status } : {}), + ...(query.po_type ? { po_type: query.po_type } : {}), + ...(query.vendor_id ? { vendor_id: BigInt(query.vendor_id) } : {}), + ...(query.plant_id ? { plant_id: BigInt(query.plant_id) } : {}), + ...(query.search ? { po_number: { contains: query.search, mode: 'insensitive' } } : {}), + ...(query.date_from || query.date_to + ? { + po_date: { + ...(query.date_from ? { gte: toDateOnly(query.date_from) } : {}), + ...(query.date_to ? { lte: toDateOnly(query.date_to) } : {}), + }, + } + : {}), +}); + const listPurchaseOrders = async (query) => { const { page, limit, skip } = getPagination(query); - const where = { - deleted_at: null, - ...(query.status ? { status: query.status } : {}), - ...(query.po_type ? { po_type: query.po_type } : {}), - ...(query.vendor_id ? { vendor_id: BigInt(query.vendor_id) } : {}), - ...(query.plant_id ? { plant_id: BigInt(query.plant_id) } : {}), - ...(query.search ? { po_number: { contains: query.search, mode: 'insensitive' } } : {}), - ...(query.date_from || query.date_to - ? { - po_date: { - ...(query.date_from ? { gte: toDateOnly(query.date_from) } : {}), - ...(query.date_to ? { lte: toDateOnly(query.date_to) } : {}), - }, - } - : {}), - }; + const where = buildPurchaseOrdersWhere(query); const [rows, total] = await Promise.all([ prisma.purchase_orders.findMany({ @@ -462,6 +465,36 @@ const listPurchaseOrders = async (query) => { return { data: rows.map(sanitizePo), meta: { page, limit, total } }; }; +const exportPurchaseOrders = async (query) => { + const rows = await prisma.purchase_orders.findMany({ + where: buildPurchaseOrdersWhere(query), + include: poListInclude, + orderBy: { created_at: 'desc' }, + }); + + return rowsToCsv( + [ + { key: 'po_number', header: 'PO Number' }, + { key: 'po_date', header: 'PO Date', type: 'date' }, + { key: 'po_type', header: 'Type' }, + { key: 'status', header: 'Status' }, + { key: 'revision_no', header: 'Revision' }, + { key: (row) => row.vendor?.vendor_code || '', header: 'Vendor Code' }, + { key: (row) => row.vendor?.vendor_name || '', header: 'Vendor Name' }, + { key: (row) => row.plant?.name || '', header: 'Plant' }, + { key: (row) => row.warehouse?.name || '', header: 'Warehouse' }, + { key: (row) => row.brand?.name || '', header: 'Brand' }, + { key: 'expected_delivery_date', header: 'Expected Delivery', type: 'date' }, + { key: 'sub_total', header: 'Sub Total' }, + { key: 'tax_total', header: 'Tax Total' }, + { key: 'grand_total', header: 'Grand Total' }, + { key: (row) => row.created_by_user?.full_name || '', header: 'Created By' }, + { key: 'created_at', header: 'Created At', type: 'datetime' }, + ], + rows.map(sanitizePo) + ); +}; + const getPurchaseOrderById = async (id) => sanitizePo(await getPoOrThrow(id, { includeItems: true })); @@ -828,6 +861,7 @@ const getPurchaseOrderPdf = async (id) => { module.exports = { createPurchaseOrder, listPurchaseOrders, + exportPurchaseOrders, getPurchaseOrderById, updatePurchaseOrder, deletePurchaseOrder, diff --git a/src/modules/purchase-orders/purchase-orders.validation.js b/src/modules/purchase-orders/purchase-orders.validation.js index 327d70a..ceaf54a 100644 --- a/src/modules/purchase-orders/purchase-orders.validation.js +++ b/src/modules/purchase-orders/purchase-orders.validation.js @@ -107,6 +107,11 @@ const listPurchaseOrdersQuerySchema = Joi.object({ date_to: Joi.date().iso().optional(), }); +const exportPurchaseOrdersQuerySchema = listPurchaseOrdersQuerySchema.keys({ + page: Joi.strip(), + limit: Joi.strip(), +}); + const workflowRemarksSchema = Joi.object({ remarks: Joi.string().allow(null, '').optional(), }); @@ -120,6 +125,7 @@ module.exports = { updatePurchaseOrderSchema, amendPurchaseOrderSchema, listPurchaseOrdersQuerySchema, + exportPurchaseOrdersQuerySchema, workflowRemarksSchema, rejectPurchaseOrderSchema, }; diff --git a/src/modules/settings/settings.service.js b/src/modules/settings/settings.service.js index 6e6ce66..a61c099 100644 --- a/src/modules/settings/settings.service.js +++ b/src/modules/settings/settings.service.js @@ -15,6 +15,30 @@ const buildPublicUrl = (filePath) => { return normalized.startsWith('/') ? normalized : `/${normalized}`; }; +const MIME_BY_EXT = { + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.webp': 'image/webp', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', +}; + +const buildLogoDataUri = (logoPath) => { + if (!logoPath) return null; + + const absolute = path.isAbsolute(logoPath) + ? logoPath + : path.resolve(process.cwd(), logoPath); + + if (!fs.existsSync(absolute)) return null; + + const ext = path.extname(absolute).toLowerCase(); + const mime = MIME_BY_EXT[ext] || 'image/png'; + const base64 = fs.readFileSync(absolute).toString('base64'); + return `data:${mime};base64,${base64}`; +}; + const unlinkIfExists = (relativePath) => { if (!relativePath) return; const absolute = path.resolve(process.cwd(), relativePath); @@ -218,7 +242,9 @@ const getCompanyForDocuments = async () => { phone: row.mobile || '', email: row.email || '', website: row.website || '', + logo_path: row.logo_path || null, logo_url: buildPublicUrl(row.logo_path), + logo_src: buildLogoDataUri(row.logo_path), favicon_url: buildPublicUrl(row.favicon_path), }; }; diff --git a/src/modules/vendors/vendors.controller.js b/src/modules/vendors/vendors.controller.js index e51659d..d81ef07 100644 --- a/src/modules/vendors/vendors.controller.js +++ b/src/modules/vendors/vendors.controller.js @@ -12,6 +12,13 @@ const list = asyncHandler(async (req, res) => { res.json(new ApiResponse(200, result.data, 'Vendors fetched', result.meta)); }); +const exportCsv = asyncHandler(async (req, res) => { + const csv = await service.exportVendors(req.query); + res.setHeader('Content-Type', 'text/csv; charset=utf-8'); + res.setHeader('Content-Disposition', 'attachment; filename="vendors-export.csv"'); + res.send(csv); +}); + const listGstTreatments = asyncHandler(async (_req, res) => { const data = service.listGstTreatments(); res.json(new ApiResponse(200, data, 'GST treatment options fetched')); @@ -184,6 +191,7 @@ const removeItemMapping = asyncHandler(async (req, res) => { module.exports = { create, list, + exportCsv, listGstTreatments, listSourceOfSupplyOptions, getOne, diff --git a/src/modules/vendors/vendors.routes.js b/src/modules/vendors/vendors.routes.js index b694eb1..79e28b5 100644 --- a/src/modules/vendors/vendors.routes.js +++ b/src/modules/vendors/vendors.routes.js @@ -8,6 +8,7 @@ const { updateVendorSchema, vendorStatusSchema, listVendorsQuerySchema, + exportVendorsQuerySchema, createAddressSchema, updateAddressSchema, createContactSchema, @@ -22,6 +23,12 @@ const router = express.Router(); router.use(authenticate); +router.get( + '/export', + authorize('VENDOR', 'export'), + validate(exportVendorsQuerySchema, 'query'), + controller.exportCsv +); router.get( '/', authorize('VENDOR', 'view'), diff --git a/src/modules/vendors/vendors.service.js b/src/modules/vendors/vendors.service.js index 31aff54..25683b6 100644 --- a/src/modules/vendors/vendors.service.js +++ b/src/modules/vendors/vendors.service.js @@ -4,6 +4,7 @@ const auditLog = require('../../utils/auditLog'); const { getPagination } = require('../../utils/pagination'); const { nextDocumentNumber } = require('../../utils/generateCode'); const { encrypt, decrypt, blindIndex } = require('../../utils/encryption'); +const { rowsToCsv } = require('../../utils/csv'); const { GST_TREATMENTS, SOURCE_OF_SUPPLY_OPTIONS, @@ -166,6 +167,33 @@ const listVendors = async (query) => { return { data: rows.map((row) => sanitizeVendor(row)), meta: { page, limit, total } }; }; +const exportVendors = async (query) => { + const where = buildVendorsWhere(query); + const rows = await prisma.vendors.findMany({ + where, + include: vendorInclude, + orderBy: { created_at: 'desc' }, + }); + + return rowsToCsv( + [ + { key: 'vendor_code', header: 'Vendor Code' }, + { key: 'vendor_name', header: 'Vendor Name' }, + { key: 'vendor_type', header: 'Vendor Type' }, + { key: 'gstin', header: 'GSTIN' }, + { key: 'pan', header: 'PAN' }, + { key: 'gst_treatment', header: 'GST Treatment' }, + { key: 'source_of_supply', header: 'Source of Supply' }, + { key: (row) => row.payment_terms?.name || '', header: 'Payment Term' }, + { key: 'credit_period_days', header: 'Credit Days' }, + { key: 'status', header: 'Status' }, + { key: 'is_active', header: 'Active' }, + { key: 'created_at', header: 'Created At', type: 'datetime' }, + ], + rows.map((row) => sanitizeVendor(row)) + ); +}; + const listGstTreatments = () => GST_TREATMENTS; const listSourceOfSupplyOptions = () => SOURCE_OF_SUPPLY_OPTIONS; @@ -657,6 +685,7 @@ const deleteItemMapping = async (vendorId, mappingId, userId, requestId) => { module.exports = { createVendor, listVendors, + exportVendors, listGstTreatments, listSourceOfSupplyOptions, getVendorById, diff --git a/src/modules/vendors/vendors.validation.js b/src/modules/vendors/vendors.validation.js index a936a43..ab3b48e 100644 --- a/src/modules/vendors/vendors.validation.js +++ b/src/modules/vendors/vendors.validation.js @@ -70,6 +70,11 @@ const listVendorsQuerySchema = Joi.object({ is_active: Joi.boolean().optional(), }); +const exportVendorsQuerySchema = listVendorsQuerySchema.keys({ + page: Joi.strip(), + limit: Joi.strip(), +}); + const createAddressSchema = Joi.object({ address_type: Joi.string() .valid(...addressTypes) @@ -145,6 +150,7 @@ module.exports = { updateVendorSchema, vendorStatusSchema, listVendorsQuerySchema, + exportVendorsQuerySchema, createAddressSchema, updateAddressSchema, createContactSchema, diff --git a/src/utils/pdf/templates/grn.template.js b/src/utils/pdf/templates/grn.template.js index 3653d38..32e5e64 100644 --- a/src/utils/pdf/templates/grn.template.js +++ b/src/utils/pdf/templates/grn.template.js @@ -207,6 +207,19 @@ const generateGrnHtml = (inputData = getDummyGrnData()) => { align-items: flex-start; gap: 24px; } + .brand { + display: flex; + gap: 12px; + align-items: flex-start; + flex: 1; + min-width: 0; + } + .company-logo { + width: 56px; + height: 56px; + object-fit: contain; + flex-shrink: 0; + } .company-name { margin: 0; font-size: 20px; @@ -383,11 +396,18 @@ const generateGrnHtml = (inputData = getDummyGrnData()) => {
-
-

${escapeHtml(company.name || 'Company')}

-
- ${escapeHtml(companyAddressLine(company))}
- ${escapeHtml(companyContactLine(company))} +
+ ${ + company.logo_src + ? `` + : '' + } +
+

${escapeHtml(company.name || 'Company')}

+
+ ${escapeHtml(companyAddressLine(company))}
+ ${escapeHtml(companyContactLine(company))} +
diff --git a/src/utils/pdf/templates/po.template.js b/src/utils/pdf/templates/po.template.js index 84d8848..434b7ff 100644 --- a/src/utils/pdf/templates/po.template.js +++ b/src/utils/pdf/templates/po.template.js @@ -205,6 +205,19 @@ const generatePoHtml = (inputData = getDummyPoData()) => { align-items: flex-start; gap: 24px; } + .brand { + display: flex; + gap: 12px; + align-items: flex-start; + flex: 1; + min-width: 0; + } + .company-logo { + width: 56px; + height: 56px; + object-fit: contain; + flex-shrink: 0; + } .company-name { margin: 0; font-size: 20px; @@ -391,11 +404,18 @@ const generatePoHtml = (inputData = getDummyPoData()) => {
-
-

${escapeHtml(company.name || 'Company')}

-
- ${escapeHtml(companyAddressLine(company))}
- ${escapeHtml(companyContactLine(company))} +
+ ${ + company.logo_src + ? `` + : '' + } +
+

${escapeHtml(company.name || 'Company')}

+
+ ${escapeHtml(companyAddressLine(company))}
+ ${escapeHtml(companyContactLine(company))} +