pdf,excell related changes

This commit is contained in:
Gowtham M 2026-07-15 15:00:38 +05:30
parent c37ff28833
commit 95cbdcf7c5
19 changed files with 347 additions and 64 deletions

View File

@ -18,6 +18,13 @@ const list = asyncHandler(async (req, res) => {
res.json(new ApiResponse(200, result.data, 'Assets fetched', result.meta)); 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 getOne = asyncHandler(async (req, res) => {
const data = await service.getAssetById(req.params.id); const data = await service.getAssetById(req.params.id);
res.json(new ApiResponse(200, data, 'Asset fetched')); res.json(new ApiResponse(200, data, 'Asset fetched'));
@ -276,6 +283,7 @@ const removeAttachment = asyncHandler(async (req, res) => {
module.exports = { module.exports = {
create, create,
list, list,
exportCsv,
getOne, getOne,
update, update,
remove, remove,

View File

@ -8,6 +8,7 @@ const {
createAssetSchema, createAssetSchema,
updateAssetSchema, updateAssetSchema,
listAssetsQuerySchema, listAssetsQuerySchema,
exportAssetsQuerySchema,
transferAssetSchema, transferAssetSchema,
amcContractSchema, amcContractSchema,
updateAmcContractSchema, updateAmcContractSchema,
@ -28,6 +29,12 @@ const router = express.Router();
router.use(authenticate); router.use(authenticate);
router.get(
'/export',
authorize('ASSET', 'export'),
validate(exportAssetsQuerySchema, 'query'),
controller.exportCsv
);
router.get( router.get(
'/', '/',
authorize('ASSET', 'view'), authorize('ASSET', 'view'),

View File

@ -3,6 +3,7 @@ const ApiError = require('../../utils/ApiError');
const auditLog = require('../../utils/auditLog'); const auditLog = require('../../utils/auditLog');
const { getPagination } = require('../../utils/pagination'); const { getPagination } = require('../../utils/pagination');
const { nextDocumentNumber } = require('../../utils/generateCode'); const { nextDocumentNumber } = require('../../utils/generateCode');
const { rowsToCsv } = require('../../utils/csv');
const { DISPOSAL_STATUSES, getAssetDropdownOptions } = require('./assets.constants'); const { DISPOSAL_STATUSES, getAssetDropdownOptions } = require('./assets.constants');
const { assertPlant, assertWarehouse } = require('../../utils/locations'); const { assertPlant, assertWarehouse } = require('../../utils/locations');
const repository = require('./assets.repository'); const repository = require('./assets.repository');
@ -307,30 +308,32 @@ const createAsset = async (payload, userId, requestId) => {
return sanitizeAsset(created); 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 listAssets = async (query) => {
const { page, limit, skip } = getPagination(query); const { page, limit, skip } = getPagination(query);
const where = { const where = 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 [rows, total] = await Promise.all([ const [rows, total] = await Promise.all([
prisma.assets.findMany({ prisma.assets.findMany({
@ -346,6 +349,39 @@ const listAssets = async (query) => {
return { data: rows.map(sanitizeAsset), meta: { page, limit, total } }; 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 getAssetById = async (id) => sanitizeAsset(await getAssetOrThrow(id));
const updateAsset = async (id, payload, userId, requestId) => { const updateAsset = async (id, payload, userId, requestId) => {
@ -566,6 +602,7 @@ const previewDepreciation = (payload) => {
module.exports = { module.exports = {
createAsset, createAsset,
listAssets, listAssets,
exportAssets,
getAssetById, getAssetById,
updateAsset, updateAsset,
deleteAsset, deleteAsset,

View File

@ -108,6 +108,11 @@ const listAssetsQuerySchema = Joi.object({
is_active: Joi.boolean().optional(), is_active: Joi.boolean().optional(),
}); });
const exportAssetsQuerySchema = listAssetsQuerySchema.keys({
page: Joi.strip(),
limit: Joi.strip(),
});
const transferAssetSchema = Joi.object({ const transferAssetSchema = Joi.object({
transfer_date: Joi.date().iso().required(), transfer_date: Joi.date().iso().required(),
to_plant_id: Joi.number().integer().positive().allow(null).optional(), to_plant_id: Joi.number().integer().positive().allow(null).optional(),
@ -305,6 +310,7 @@ module.exports = {
createAssetSchema, createAssetSchema,
updateAssetSchema, updateAssetSchema,
listAssetsQuerySchema, listAssetsQuerySchema,
exportAssetsQuerySchema,
transferAssetSchema, transferAssetSchema,
amcContractSchema, amcContractSchema,
updateAmcContractSchema, updateAmcContractSchema,

View File

@ -14,6 +14,13 @@ const list = asyncHandler(async (req, res) => {
res.json(new ApiResponse(200, result.data, 'GRNs fetched', result.meta)); 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 getOne = asyncHandler(async (req, res) => {
const data = await service.getGrnById(req.params.id); const data = await service.getGrnById(req.params.id);
res.json(new ApiResponse(200, data, 'GRN fetched')); res.json(new ApiResponse(200, data, 'GRN fetched'));
@ -85,6 +92,7 @@ const removeAttachment = asyncHandler(async (req, res) => {
module.exports = { module.exports = {
create, create,
list, list,
exportCsv,
getOne, getOne,
update, update,
cancel, cancel,

View File

@ -8,6 +8,7 @@ const {
createGrnSchema, createGrnSchema,
updateGrnSchema, updateGrnSchema,
listGrnQuerySchema, listGrnQuerySchema,
exportGrnQuerySchema,
cancelGrnSchema, cancelGrnSchema,
} = require('./grn.validation'); } = require('./grn.validation');
@ -15,6 +16,12 @@ const router = express.Router();
router.use(authenticate); 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.get('/', authorize('GRN', 'view'), validate(listGrnQuerySchema, 'query'), controller.list);
router.post('/', authorize('GRN', 'create'), validate(createGrnSchema), controller.create); router.post('/', authorize('GRN', 'create'), validate(createGrnSchema), controller.create);

View File

@ -7,6 +7,7 @@ const { generatePdf } = require('../../utils/pdf/pdfGenerator');
const { generateGrnHtml } = require('../../utils/pdf/templates/grn.template'); const { generateGrnHtml } = require('../../utils/pdf/templates/grn.template');
const { formatDate } = require('../../utils/pdf/helpers/formatDate'); const { formatDate } = require('../../utils/pdf/helpers/formatDate');
const { getCompanyForDocuments } = require('../settings/settings.service'); const { getCompanyForDocuments } = require('../settings/settings.service');
const { rowsToCsv } = require('../../utils/csv');
const repository = require('./grn.repository'); const repository = require('./grn.repository');
const { assertWarehouse } = require('../../utils/locations'); const { assertWarehouse } = require('../../utils/locations');
const { sanitizeAttachment } = require('./grn.attachments.service'); const { sanitizeAttachment } = require('./grn.attachments.service');
@ -445,24 +446,26 @@ const createGrn = async (payload, userId, requestId) => {
return sanitizeGrn(detail); 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 listGrns = async (query) => {
const { page, limit, skip } = getPagination(query); const { page, limit, skip } = getPagination(query);
const where = { const where = 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 [rows, total] = await Promise.all([ const [rows, total] = await Promise.all([
prisma.grn.findMany({ prisma.grn.findMany({
@ -478,6 +481,35 @@ const listGrns = async (query) => {
return { data: rows.map(sanitizeGrn), meta: { page, limit, total } }; 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 getGrnById = async (id) => sanitizeGrn(await getGrnOrThrow(id, { includeItems: true }));
const updateGrn = async (id, payload, userId, requestId) => { const updateGrn = async (id, payload, userId, requestId) => {
@ -582,6 +614,7 @@ const getGrnPdf = async (id) => {
module.exports = { module.exports = {
createGrn, createGrn,
listGrns, listGrns,
exportGrns,
getGrnById, getGrnById,
updateGrn, updateGrn,
cancelGrn, cancelGrn,

View File

@ -62,6 +62,11 @@ const listGrnQuerySchema = Joi.object({
date_to: Joi.date().iso().optional(), date_to: Joi.date().iso().optional(),
}); });
const exportGrnQuerySchema = listGrnQuerySchema.keys({
page: Joi.strip(),
limit: Joi.strip(),
});
const cancelGrnSchema = Joi.object({ const cancelGrnSchema = Joi.object({
cancellation_reason: Joi.string().trim().min(1).required(), cancellation_reason: Joi.string().trim().min(1).required(),
}); });
@ -70,5 +75,6 @@ module.exports = {
createGrnSchema, createGrnSchema,
updateGrnSchema, updateGrnSchema,
listGrnQuerySchema, listGrnQuerySchema,
exportGrnQuerySchema,
cancelGrnSchema, cancelGrnSchema,
}; };

View File

@ -14,6 +14,13 @@ const list = asyncHandler(async (req, res) => {
res.json(new ApiResponse(200, result.data, 'Purchase orders fetched', result.meta)); 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 getOne = asyncHandler(async (req, res) => {
const data = await service.getPurchaseOrderById(req.params.id); const data = await service.getPurchaseOrderById(req.params.id);
res.json(new ApiResponse(200, data, 'Purchase order fetched')); res.json(new ApiResponse(200, data, 'Purchase order fetched'));
@ -110,6 +117,7 @@ const removeAttachment = asyncHandler(async (req, res) => {
module.exports = { module.exports = {
create, create,
list, list,
exportCsv,
getOne, getOne,
update, update,
remove, remove,

View File

@ -9,6 +9,7 @@ const {
updatePurchaseOrderSchema, updatePurchaseOrderSchema,
amendPurchaseOrderSchema, amendPurchaseOrderSchema,
listPurchaseOrdersQuerySchema, listPurchaseOrdersQuerySchema,
exportPurchaseOrdersQuerySchema,
workflowRemarksSchema, workflowRemarksSchema,
rejectPurchaseOrderSchema, rejectPurchaseOrderSchema,
} = require('./purchase-orders.validation'); } = require('./purchase-orders.validation');
@ -17,6 +18,12 @@ const router = express.Router();
router.use(authenticate); router.use(authenticate);
router.get(
'/export',
authorize('PURCHASE_ORDER', 'export'),
validate(exportPurchaseOrdersQuerySchema, 'query'),
controller.exportCsv
);
router.get( router.get(
'/', '/',
authorize('PURCHASE_ORDER', 'view'), authorize('PURCHASE_ORDER', 'view'),

View File

@ -7,6 +7,7 @@ const { generatePdf } = require('../../utils/pdf/pdfGenerator');
const { generatePoHtml } = require('../../utils/pdf/templates/po.template'); const { generatePoHtml } = require('../../utils/pdf/templates/po.template');
const { formatDate } = require('../../utils/pdf/helpers/formatDate'); const { formatDate } = require('../../utils/pdf/helpers/formatDate');
const { getCompanyForDocuments } = require('../settings/settings.service'); const { getCompanyForDocuments } = require('../settings/settings.service');
const { rowsToCsv } = require('../../utils/csv');
const { const {
EDITABLE_STATUSES, EDITABLE_STATUSES,
SUBMITTABLE_STATUSES, SUBMITTABLE_STATUSES,
@ -429,24 +430,26 @@ const createPurchaseOrder = async (payload, userId, requestId) => {
return sanitizePo(created); 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 listPurchaseOrders = async (query) => {
const { page, limit, skip } = getPagination(query); const { page, limit, skip } = getPagination(query);
const where = { const where = 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 [rows, total] = await Promise.all([ const [rows, total] = await Promise.all([
prisma.purchase_orders.findMany({ prisma.purchase_orders.findMany({
@ -462,6 +465,36 @@ const listPurchaseOrders = async (query) => {
return { data: rows.map(sanitizePo), meta: { page, limit, total } }; 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) => const getPurchaseOrderById = async (id) =>
sanitizePo(await getPoOrThrow(id, { includeItems: true })); sanitizePo(await getPoOrThrow(id, { includeItems: true }));
@ -828,6 +861,7 @@ const getPurchaseOrderPdf = async (id) => {
module.exports = { module.exports = {
createPurchaseOrder, createPurchaseOrder,
listPurchaseOrders, listPurchaseOrders,
exportPurchaseOrders,
getPurchaseOrderById, getPurchaseOrderById,
updatePurchaseOrder, updatePurchaseOrder,
deletePurchaseOrder, deletePurchaseOrder,

View File

@ -107,6 +107,11 @@ const listPurchaseOrdersQuerySchema = Joi.object({
date_to: Joi.date().iso().optional(), date_to: Joi.date().iso().optional(),
}); });
const exportPurchaseOrdersQuerySchema = listPurchaseOrdersQuerySchema.keys({
page: Joi.strip(),
limit: Joi.strip(),
});
const workflowRemarksSchema = Joi.object({ const workflowRemarksSchema = Joi.object({
remarks: Joi.string().allow(null, '').optional(), remarks: Joi.string().allow(null, '').optional(),
}); });
@ -120,6 +125,7 @@ module.exports = {
updatePurchaseOrderSchema, updatePurchaseOrderSchema,
amendPurchaseOrderSchema, amendPurchaseOrderSchema,
listPurchaseOrdersQuerySchema, listPurchaseOrdersQuerySchema,
exportPurchaseOrdersQuerySchema,
workflowRemarksSchema, workflowRemarksSchema,
rejectPurchaseOrderSchema, rejectPurchaseOrderSchema,
}; };

View File

@ -15,6 +15,30 @@ const buildPublicUrl = (filePath) => {
return normalized.startsWith('/') ? normalized : `/${normalized}`; 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) => { const unlinkIfExists = (relativePath) => {
if (!relativePath) return; if (!relativePath) return;
const absolute = path.resolve(process.cwd(), relativePath); const absolute = path.resolve(process.cwd(), relativePath);
@ -218,7 +242,9 @@ const getCompanyForDocuments = async () => {
phone: row.mobile || '', phone: row.mobile || '',
email: row.email || '', email: row.email || '',
website: row.website || '', website: row.website || '',
logo_path: row.logo_path || null,
logo_url: buildPublicUrl(row.logo_path), logo_url: buildPublicUrl(row.logo_path),
logo_src: buildLogoDataUri(row.logo_path),
favicon_url: buildPublicUrl(row.favicon_path), favicon_url: buildPublicUrl(row.favicon_path),
}; };
}; };

View File

@ -12,6 +12,13 @@ const list = asyncHandler(async (req, res) => {
res.json(new ApiResponse(200, result.data, 'Vendors fetched', result.meta)); 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 listGstTreatments = asyncHandler(async (_req, res) => {
const data = service.listGstTreatments(); const data = service.listGstTreatments();
res.json(new ApiResponse(200, data, 'GST treatment options fetched')); res.json(new ApiResponse(200, data, 'GST treatment options fetched'));
@ -184,6 +191,7 @@ const removeItemMapping = asyncHandler(async (req, res) => {
module.exports = { module.exports = {
create, create,
list, list,
exportCsv,
listGstTreatments, listGstTreatments,
listSourceOfSupplyOptions, listSourceOfSupplyOptions,
getOne, getOne,

View File

@ -8,6 +8,7 @@ const {
updateVendorSchema, updateVendorSchema,
vendorStatusSchema, vendorStatusSchema,
listVendorsQuerySchema, listVendorsQuerySchema,
exportVendorsQuerySchema,
createAddressSchema, createAddressSchema,
updateAddressSchema, updateAddressSchema,
createContactSchema, createContactSchema,
@ -22,6 +23,12 @@ const router = express.Router();
router.use(authenticate); router.use(authenticate);
router.get(
'/export',
authorize('VENDOR', 'export'),
validate(exportVendorsQuerySchema, 'query'),
controller.exportCsv
);
router.get( router.get(
'/', '/',
authorize('VENDOR', 'view'), authorize('VENDOR', 'view'),

View File

@ -4,6 +4,7 @@ const auditLog = require('../../utils/auditLog');
const { getPagination } = require('../../utils/pagination'); const { getPagination } = require('../../utils/pagination');
const { nextDocumentNumber } = require('../../utils/generateCode'); const { nextDocumentNumber } = require('../../utils/generateCode');
const { encrypt, decrypt, blindIndex } = require('../../utils/encryption'); const { encrypt, decrypt, blindIndex } = require('../../utils/encryption');
const { rowsToCsv } = require('../../utils/csv');
const { const {
GST_TREATMENTS, GST_TREATMENTS,
SOURCE_OF_SUPPLY_OPTIONS, SOURCE_OF_SUPPLY_OPTIONS,
@ -166,6 +167,33 @@ const listVendors = async (query) => {
return { data: rows.map((row) => sanitizeVendor(row)), meta: { page, limit, total } }; 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 listGstTreatments = () => GST_TREATMENTS;
const listSourceOfSupplyOptions = () => SOURCE_OF_SUPPLY_OPTIONS; const listSourceOfSupplyOptions = () => SOURCE_OF_SUPPLY_OPTIONS;
@ -657,6 +685,7 @@ const deleteItemMapping = async (vendorId, mappingId, userId, requestId) => {
module.exports = { module.exports = {
createVendor, createVendor,
listVendors, listVendors,
exportVendors,
listGstTreatments, listGstTreatments,
listSourceOfSupplyOptions, listSourceOfSupplyOptions,
getVendorById, getVendorById,

View File

@ -70,6 +70,11 @@ const listVendorsQuerySchema = Joi.object({
is_active: Joi.boolean().optional(), is_active: Joi.boolean().optional(),
}); });
const exportVendorsQuerySchema = listVendorsQuerySchema.keys({
page: Joi.strip(),
limit: Joi.strip(),
});
const createAddressSchema = Joi.object({ const createAddressSchema = Joi.object({
address_type: Joi.string() address_type: Joi.string()
.valid(...addressTypes) .valid(...addressTypes)
@ -145,6 +150,7 @@ module.exports = {
updateVendorSchema, updateVendorSchema,
vendorStatusSchema, vendorStatusSchema,
listVendorsQuerySchema, listVendorsQuerySchema,
exportVendorsQuerySchema,
createAddressSchema, createAddressSchema,
updateAddressSchema, updateAddressSchema,
createContactSchema, createContactSchema,

View File

@ -207,6 +207,19 @@ const generateGrnHtml = (inputData = getDummyGrnData()) => {
align-items: flex-start; align-items: flex-start;
gap: 24px; 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 { .company-name {
margin: 0; margin: 0;
font-size: 20px; font-size: 20px;
@ -383,11 +396,18 @@ const generateGrnHtml = (inputData = getDummyGrnData()) => {
<body> <body>
<div class="document"> <div class="document">
<div class="header"> <div class="header">
<div> <div class="brand">
<h1 class="company-name">${escapeHtml(company.name || 'Company')}</h1> ${
<div class="company-meta"> company.logo_src
${escapeHtml(companyAddressLine(company))}<br /> ? `<img class="company-logo" src="${company.logo_src}" alt="Company logo" />`
${escapeHtml(companyContactLine(company))} : ''
}
<div>
<h1 class="company-name">${escapeHtml(company.name || 'Company')}</h1>
<div class="company-meta">
${escapeHtml(companyAddressLine(company))}<br />
${escapeHtml(companyContactLine(company))}
</div>
</div> </div>
</div> </div>
<div class="title-block"> <div class="title-block">

View File

@ -205,6 +205,19 @@ const generatePoHtml = (inputData = getDummyPoData()) => {
align-items: flex-start; align-items: flex-start;
gap: 24px; 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 { .company-name {
margin: 0; margin: 0;
font-size: 20px; font-size: 20px;
@ -391,11 +404,18 @@ const generatePoHtml = (inputData = getDummyPoData()) => {
<body> <body>
<div class="document"> <div class="document">
<div class="header"> <div class="header">
<div> <div class="brand">
<h1 class="company-name">${escapeHtml(company.name || 'Company')}</h1> ${
<div class="company-meta"> company.logo_src
${escapeHtml(companyAddressLine(company))}<br /> ? `<img class="company-logo" src="${company.logo_src}" alt="Company logo" />`
${escapeHtml(companyContactLine(company))} : ''
}
<div>
<h1 class="company-name">${escapeHtml(company.name || 'Company')}</h1>
<div class="company-meta">
${escapeHtml(companyAddressLine(company))}<br />
${escapeHtml(companyContactLine(company))}
</div>
</div> </div>
</div> </div>
<div class="title-block"> <div class="title-block">