diff --git a/controller/documentTemplate/DocumentTemplateController.js b/controller/documentTemplate/DocumentTemplateController.js index 33e2241..13bdcd1 100644 --- a/controller/documentTemplate/DocumentTemplateController.js +++ b/controller/documentTemplate/DocumentTemplateController.js @@ -1277,6 +1277,244 @@ exports.previewPdf = async (req, res) => { } }; +// NEW: Admin-only endpoint to preview latest active contract for a specific user with DB-filled placeholders +// GET /api/admin/contracts/:id/preview?userType=personal|company&type=contract +exports.previewLatestForUser = async (req, res) => { + const targetUserId = parseInt(req.params.id, 10); + const overrideUserType = (req.query.userType || req.query.user_type || '').toString().toLowerCase(); + const templateType = (req.query.type || 'contract').toString(); + + if (!req.user || !['admin', 'super_admin'].includes(req.user.role)) { + return res.status(403).json({ error: 'Forbidden: Admins only' }); + } + if (!Number.isFinite(targetUserId) || targetUserId <= 0) { + return res.status(400).json({ error: 'Invalid user id' }); + } + + try { + // Resolve target user and determine user_type if not overridden + let userRow = null; + try { + const [uRows] = await db.execute('SELECT id, email, user_type, role FROM users WHERE id = ? LIMIT 1', [targetUserId]); + userRow = (uRows && uRows[0]) ? uRows[0] : null; + } catch (e) { + logger.error('[previewLatestForUser] failed to load users row', e && e.message); + } + if (!userRow) { + return res.status(404).json({ error: 'User not found' }); + } + + const userType = (overrideUserType === 'personal' || overrideUserType === 'company') ? overrideUserType : userRow.user_type; + + // Find the latest active template for this user type + const latest = await DocumentTemplateService.getLatestActiveForUserType(userType, templateType); + if (!latest) { + return res.status(404).json({ error: 'No active template found for user type' }); + } + + // Fetch template HTML from S3 + const s3 = new S3Client({ + region: process.env.EXOSCALE_REGION, + endpoint: process.env.EXOSCALE_ENDPOINT, + credentials: { + accessKeyId: process.env.EXOSCALE_ACCESS_KEY, + secretAccessKey: process.env.EXOSCALE_SECRET_KEY + } + }); + const command = new GetObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: latest.storageKey }); + const fileObj = await s3.send(command); + if (!fileObj.Body) { + return res.status(404).json({ error: 'Template file not available' }); + } + let html = await streamToString(fileObj.Body, latest.id); + + // Build variable map from DB for target user + const vars = {}; + // Always include email and currentDate + vars.email = userRow.email || ''; + const now = new Date(); + const pad = (n) => String(n).padStart(2, '0'); + vars.currentDate = `${pad(now.getDate())}.${pad(now.getMonth() + 1)}.${now.getFullYear()} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`; + + // Personal profile fields + if (userType === 'personal') { + try { + const [pRows] = await db.execute('SELECT * FROM personal_profiles WHERE user_id = ? LIMIT 1', [targetUserId]); + const p = (pRows && pRows[0]) ? pRows[0] : null; + if (p) { + const first = p.first_name || ''; + const last = p.last_name || ''; + const fullName = `${first} ${last}`.trim(); + vars.fullName = fullName; + vars.address = p.address || ''; + vars.zip_code = p.zip_code || ''; + vars.city = p.city || ''; + vars.country = p.country || ''; + vars.phone = p.phone || ''; + const fullAddressParts = []; + if (vars.address) fullAddressParts.push(vars.address); + const zipCity = [vars.zip_code, vars.city].filter(Boolean).join(' '); + if (zipCity) fullAddressParts.push(zipCity); + vars.fullAddress = fullAddressParts.join(', '); + } + } catch (e) { + logger.warn('[previewLatestForUser] load personal_profiles failed', e && e.message); + } + } + + // Company profile fields + if (userType === 'company') { + try { + const [cRows] = await db.execute('SELECT * FROM company_profiles WHERE user_id = ? LIMIT 1', [targetUserId]); + const c = (cRows && cRows[0]) ? cRows[0] : null; + if (c) { + vars.companyName = c.company_name || ''; + vars.registrationNumber = c.registration_number || ''; + vars.companyAddress = c.address || ''; + // generic address keys used by some templates + vars.address = vars.companyAddress; + vars.zip_code = c.zip_code || ''; + vars.city = c.city || ''; + vars.country = c.country || ''; + vars.contactPersonName = c.contact_person_name || ''; + vars.contactPersonPhone = c.contact_person_phone || c.phone || ''; + vars.companyEmail = c.email || c.company_email || c.contact_email || userRow.email || ''; + vars.companyPhone = c.phone || c.contact_person_phone || ''; + const addrParts = []; + if (vars.companyAddress) addrParts.push(vars.companyAddress); + const zipCity = [vars.zip_code, vars.city].filter(Boolean).join(' '); + if (zipCity) addrParts.push(zipCity); + vars.companyFullAddress = addrParts.join(', '); + } + } catch (e) { + logger.warn('[previewLatestForUser] load company_profiles failed', e && e.message); + } + } + + // Replace placeholders with resolved variables (simple global replace) + if (vars && typeof vars === 'object') { + Object.entries(vars).forEach(([key, value]) => { + const before = (html.match(new RegExp(`{{\\s*${key}\\s*}}`, 'g')) || []).length; + if (before) { + html = html.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), String(value ?? '')); + } + }); + } + + // Apply stamp/signature placeholders (harmless if placeholders are absent) + try { html = await applyCompanyStampPlaceholders(html, { ...req, user: { ...userRow, user_type: userType } }); } catch (e) { logger.warn('[previewLatestForUser] applyCompanyStampPlaceholders failed', e && e.message); } + try { html = await applyProfitPlanetSignature(html); } catch (e) { logger.warn('[previewLatestForUser] applyProfitPlanetSignature failed', e && e.message); } + + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + return res.send(ensureHtmlDocument(html)); + } catch (err) { + logger.error('[previewLatestForUser] error', err && err.stack ? err.stack : err); + return res.status(500).json({ error: 'Failed to render preview' }); + } +}; + +// NEW: Authenticated user endpoint to preview their own latest active contract with DB-filled placeholders +// GET /api/contracts/preview/latest +exports.previewLatestForMe = async (req, res) => { + if (!req.user) return res.status(401).json({ error: 'Unauthorized' }); + const targetUserId = req.user.id || req.user.userId; + const userType = req.user.user_type || req.user.userType; + if (!targetUserId || !userType) return res.status(400).json({ error: 'Invalid authenticated user' }); + + try { + // Find the latest active template for this user type + const latest = await DocumentTemplateService.getLatestActiveForUserType(userType, 'contract'); + if (!latest) { + return res.status(404).json({ error: 'No active template found for your user type' }); + } + + // Fetch template HTML from storage + const s3 = new S3Client({ + region: process.env.EXOSCALE_REGION, + endpoint: process.env.EXOSCALE_ENDPOINT, + credentials: { + accessKeyId: process.env.EXOSCALE_ACCESS_KEY, + secretAccessKey: process.env.EXOSCALE_SECRET_KEY + } + }); + const command = new GetObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: latest.storageKey }); + const fileObj = await s3.send(command); + if (!fileObj.Body) return res.status(404).json({ error: 'Template file not available' }); + let html = await streamToString(fileObj.Body, latest.id); + + // Build variables from the authenticated user's DB data + const vars = {}; + // base fields + vars.email = req.user.email || ''; + const d = new Date(); + const pad = (n) => String(n).padStart(2, '0'); + vars.currentDate = `${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; + + if (userType === 'personal') { + try { + const [rows] = await db.execute('SELECT * FROM personal_profiles WHERE user_id = ? LIMIT 1', [targetUserId]); + const p = rows && rows[0] ? rows[0] : null; + if (p) { + const fullName = `${p.first_name || ''} ${p.last_name || ''}`.trim(); + vars.fullName = fullName; + vars.address = p.address || ''; + vars.zip_code = p.zip_code || ''; + vars.city = p.city || ''; + vars.country = p.country || ''; + vars.phone = p.phone || ''; + const parts = []; + if (vars.address) parts.push(vars.address); + const zipCity = [vars.zip_code, vars.city].filter(Boolean).join(' '); + if (zipCity) parts.push(zipCity); + vars.fullAddress = parts.join(', '); + } + } catch (e) { + logger.warn('[previewLatestForMe] personal profile lookup failed', e && e.message); + } + } else if (userType === 'company') { + try { + const [rows] = await db.execute('SELECT * FROM company_profiles WHERE user_id = ? LIMIT 1', [targetUserId]); + const c = rows && rows[0] ? rows[0] : null; + if (c) { + vars.companyName = c.company_name || ''; + vars.registrationNumber = c.registration_number || ''; + vars.companyAddress = c.address || ''; + vars.address = vars.companyAddress; + vars.zip_code = c.zip_code || ''; + vars.city = c.city || ''; + vars.country = c.country || ''; + vars.contactPersonName = c.contact_person_name || ''; + vars.contactPersonPhone = c.contact_person_phone || c.phone || ''; + vars.companyEmail = c.email || c.company_email || c.contact_email || req.user.email || ''; + vars.companyPhone = c.phone || c.contact_person_phone || ''; + const parts = []; + if (vars.companyAddress) parts.push(vars.companyAddress); + const zipCity = [vars.zip_code, vars.city].filter(Boolean).join(' '); + if (zipCity) parts.push(zipCity); + vars.companyFullAddress = parts.join(', '); + } + } catch (e) { + logger.warn('[previewLatestForMe] company profile lookup failed', e && e.message); + } + } + + // Replace placeholders + Object.entries(vars).forEach(([k, v]) => { + html = html.replace(new RegExp(`{{\\s*${k}\\s*}}`, 'g'), String(v ?? '')); + }); + + // Apply company stamp and signature where applicable + try { html = await applyCompanyStampPlaceholders(html, req); } catch (e) {} + try { html = await applyProfitPlanetSignature(html); } catch (e) {} + + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + return res.send(ensureHtmlDocument(html)); + } catch (err) { + logger.error('[previewLatestForMe] error', err && err.stack ? err.stack : err); + return res.status(500).json({ error: 'Failed to render preview' }); + } +}; + // NEW controller: download sanitized PDF (variables emptied) // Keeps stamp/signature placeholders so company stamps or profitplanetSignature can still be applied if available. exports.downloadPdf = async (req, res) => { diff --git a/routes/getRoutes.js b/routes/getRoutes.js index 82d3f7f..e3a82e8 100644 --- a/routes/getRoutes.js +++ b/routes/getRoutes.js @@ -59,6 +59,8 @@ router.get('/admin/verification-pending-users', authMiddleware, requireAdmin, Ad router.get('/admin/unverified-users', authMiddleware, requireAdmin, AdminUserController.getUnverifiedUsers); router.get('/admin/user/:id/documents', authMiddleware, requireAdmin, UserDocumentController.getAllDocumentsForUser); router.get('/admin/server-status', authMiddleware, requireAdmin, ServerStatusController.getStatus); +// Contract preview for admin: latest active by user type +router.get('/admin/contracts/:id/preview', authMiddleware, requireAdmin, DocumentTemplateController.previewLatestForUser); // permissions.js GETs router.get('/permissions', authMiddleware, PermissionController.list); @@ -86,6 +88,9 @@ router.get('/contracts/company', authMiddleware, (req, res) => { }); }); +// User: preview latest active contract (HTML) for authenticated user +router.get('/contracts/preview/latest', authMiddleware, DocumentTemplateController.previewLatestForMe); + // documentTemplates.js GETs router.get('/document-templates', authMiddleware, DocumentTemplateController.listTemplates); router.get('/document-templates/:id', authMiddleware, DocumentTemplateController.getTemplate); diff --git a/services/template/DocumentTemplateService.js b/services/template/DocumentTemplateService.js index 7932205..a87d38a 100644 --- a/services/template/DocumentTemplateService.js +++ b/services/template/DocumentTemplateService.js @@ -128,6 +128,20 @@ class DocumentTemplateService { throw error; } } + + // Convenience: return the most recent active template for a user type (by createdAt desc) + async getLatestActiveForUserType(userType, templateType = 'contract') { + logger.info('DocumentTemplateService.getLatestActiveForUserType:start', { userType, templateType }); + try { + const list = await DocumentTemplateRepository.findActiveByUserType(userType, templateType); + const latest = Array.isArray(list) && list.length ? list[0] : null; + logger.info('DocumentTemplateService.getLatestActiveForUserType:result', { found: !!latest, id: latest?.id }); + return latest; + } catch (error) { + logger.error('DocumentTemplateService.getLatestActiveForUserType:error', { error: error.message }); + throw error; + } + } // NEW: Retrieve Profit Planet signature (company stamp) as tag // Fallback order: