brand identity
Build and Push Docker Image / build (push) Successful in 2m21s

This commit is contained in:
Jason Stedwell
2026-07-03 10:09:25 -05:00
parent abb4c740e6
commit 497d54de3c
6 changed files with 147 additions and 54 deletions
+62 -21
View File
@@ -1,8 +1,28 @@
import { google, Auth } from 'googleapis';
import { Readable } from 'stream';
import { readFileSync } from 'fs';
import path from 'path';
import { prisma } from './prisma';
import type { Prisma } from '@prisma/client';
// The MPM logo is embedded directly into the report HTML as a base64 data URI
// so it renders regardless of network reachability (e.g. a LAN-only deployment
// where Google's importer can't fetch an external URL). Read from disk once and
// cache; `null` means the asset was missing and the report renders without it.
let cachedLogoDataUri: string | null | undefined;
function getLogoDataUri(): string | null {
if (cachedLogoDataUri !== undefined) return cachedLogoDataUri;
try {
const logoPath = path.join(process.cwd(), 'public', 'mpm-logo-doc.png');
const b64 = readFileSync(logoPath).toString('base64');
cachedLogoDataUri = `data:image/png;base64,${b64}`;
} catch (error) {
console.error('MPM report logo could not be loaded; rendering without it:', error);
cachedLogoDataUri = null;
}
return cachedLogoDataUri;
}
type ReportWithRelations = Prisma.ReportGetPayload<{
include: { tasks: true; user: true };
}>;
@@ -127,21 +147,41 @@ export function generateReportHTML(report: ReportWithRelations) {
const plannedTasks = report.tasks.filter((t) => t.type === 'PLANNED');
const completedTasks = report.tasks.filter((t) => t.type === 'COMPLETED');
const cellStyle = "padding: 10px; border-bottom: 1px solid #e2e8f0; font-family: Arial, sans-serif; font-size: 11pt;";
const headerStyle = "padding: 12px 10px; background-color: #f1f5f9; border-bottom: 2px solid #cbd5e1; font-family: Arial, sans-serif; font-size: 11pt; font-weight: bold; text-align: left; color: #334155;";
// MPM brand palette
const GOLD_DARK = '#998643'; // titles on light backgrounds
const GOLD_MID = '#DCBB4F'; // accent lines, dividers
const SHADE_DARK = '#232022'; // body text / table header background
const SHADE_LIGHT = '#F5F1EC'; // section panels / table header text
const OFF_WHITE = '#FAF7F2'; // alternating table rows
const ACCENT = '#849698'; // secondary / muted text
const bodyFont = "'Open Sans', Arial, sans-serif";
const headingFont = "'Montserrat', 'Open Sans', Arial, sans-serif";
const cellStyle = `padding: 10px; border-bottom: 1px solid #E7E0D5; font-family: ${bodyFont}; font-size: 11pt; color: ${SHADE_DARK};`;
const headerStyle = `padding: 12px 10px; background-color: ${SHADE_DARK}; font-family: ${headingFont}; font-size: 11pt; font-weight: 600; text-align: left; color: ${SHADE_LIGHT};`;
// Embed the logo inline so it survives conversion without a reachable host.
const logoDataUri = getLogoDataUri();
const logoHtml = logoDataUri
? `<img src="${logoDataUri}" alt="Message Point Media" style="height: 44px; margin-bottom: 16px;" />`
: '';
const rowBg = (i: number) => (i % 2 === 1 ? ` background-color: ${OFF_WHITE};` : '');
return `
<html>
<body style="font-family: Arial, sans-serif; color: #334155; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px;">
<h1 style="color: #0f172a; border-bottom: 3px solid #3b82f6; padding-bottom: 10px; font-family: Arial, sans-serif; margin-bottom: 20px;">WFH Daily Report</h1>
<div style="background-color: #f8fafc; padding: 20px; border-left: 4px solid #3b82f6; border-radius: 4px; margin-bottom: 30px; font-family: Arial, sans-serif;">
<body style="font-family: ${bodyFont}; color: ${SHADE_DARK}; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px;">
${logoHtml}
<h1 style="color: ${GOLD_DARK}; border-bottom: 3px solid ${GOLD_MID}; padding-bottom: 10px; font-family: ${headingFont}; font-weight: 700; margin-bottom: 20px;">WFH Daily Report</h1>
<div style="background-color: ${SHADE_LIGHT}; padding: 20px; border-left: 4px solid ${GOLD_MID}; border-radius: 4px; margin-bottom: 30px; font-family: ${bodyFont};">
<p style="margin: 0 0 8px 0; font-size: 11pt;"><strong>Date:</strong> ${dateStr}</p>
<p style="margin: 0 0 8px 0; font-size: 11pt;"><strong>Employee:</strong> ${escapeHtml(report.user.name)}</p>
<p style="margin: 0; font-size: 11pt;"><strong>Manager:</strong> ${escapeHtml(report.managerName || 'N/A')}</p>
</div>
<h2 style="color: #1e293b; margin-top: 30px; margin-bottom: 15px; font-family: Arial, sans-serif;">Planned Tasks</h2>
<h2 style="color: ${GOLD_DARK}; margin-top: 30px; margin-bottom: 15px; font-family: ${headingFont}; font-weight: 600;">Planned Tasks</h2>
${plannedTasks.length > 0 ? `
<table style="width: 100%; border-collapse: collapse; margin-bottom: 30px;">
<tr>
@@ -149,17 +189,17 @@ export function generateReportHTML(report: ReportWithRelations) {
<th style="${headerStyle} width: 20%;">Estimate</th>
<th style="${headerStyle} width: 35%;">Notes</th>
</tr>
${plannedTasks.map((t) => `
${plannedTasks.map((t, i) => `
<tr>
<td style="${cellStyle}">${escapeHtml(t.description)}</td>
<td style="${cellStyle} color: #64748b;">${escapeHtml(t.timeEstimate || '-')}</td>
<td style="${cellStyle} color: #64748b;">${escapeHtml(t.notes || '-')}</td>
<td style="${cellStyle}${rowBg(i)}">${escapeHtml(t.description)}</td>
<td style="${cellStyle}${rowBg(i)} color: ${ACCENT};">${escapeHtml(t.timeEstimate || '-')}</td>
<td style="${cellStyle}${rowBg(i)} color: ${ACCENT};">${escapeHtml(t.notes || '-')}</td>
</tr>
`).join('')}
</table>
` : `<p style="font-style: italic; color: #94a3b8; font-family: Arial, sans-serif; margin-bottom: 30px;">No planned tasks for today.</p>`}
` : `<p style="font-style: italic; color: ${ACCENT}; font-family: ${bodyFont}; margin-bottom: 30px;">No planned tasks for today.</p>`}
<h2 style="color: #1e293b; margin-top: 30px; margin-bottom: 15px; font-family: Arial, sans-serif;">Completed Tasks</h2>
<h2 style="color: ${GOLD_DARK}; margin-top: 30px; margin-bottom: 15px; font-family: ${headingFont}; font-weight: 600;">Completed Tasks</h2>
${completedTasks.length > 0 ? `
<table style="width: 100%; border-collapse: collapse; margin-bottom: 30px;">
<tr>
@@ -167,21 +207,22 @@ export function generateReportHTML(report: ReportWithRelations) {
<th style="${headerStyle} width: 20%;">Status</th>
<th style="${headerStyle} width: 40%;">Work Link</th>
</tr>
${completedTasks.map((t) => {
${completedTasks.map((t, i) => {
const link = safeLink(t.link);
return `
<tr>
<td style="${cellStyle}">${escapeHtml(t.description)}</td>
<td style="${cellStyle} font-weight: bold; color: #059669;">${escapeHtml(t.status || 'Done')}</td>
<td style="${cellStyle}">${link ? `<a href="${escapeHtml(link)}" style="color: #2563eb; text-decoration: none;">${escapeHtml(link)}</a>` : '-'}</td>
<td style="${cellStyle}${rowBg(i)}">${escapeHtml(t.description)}</td>
<td style="${cellStyle}${rowBg(i)} font-weight: bold; color: ${GOLD_DARK};">${escapeHtml(t.status || 'Done')}</td>
<td style="${cellStyle}${rowBg(i)}">${link ? `<a href="${escapeHtml(link)}" style="color: ${GOLD_DARK}; text-decoration: none;">${escapeHtml(link)}</a>` : '-'}</td>
</tr>
`;
}).join('')}
</table>
` : `<p style="font-style: italic; color: #94a3b8; font-family: Arial, sans-serif; margin-bottom: 30px;">No completed tasks reported today.</p>`}
<div style="margin-top: 50px; font-size: 9pt; color: #cbd5e1; text-align: center; border-top: 1px solid #e2e8f0; padding-top: 20px; font-family: Arial, sans-serif;">
Generated automatically by WFH App
` : `<p style="font-style: italic; color: ${ACCENT}; font-family: ${bodyFont}; margin-bottom: 30px;">No completed tasks reported today.</p>`}
<div style="margin-top: 50px; font-size: 9pt; color: ${ACCENT}; text-align: center; border-top: 1px solid ${GOLD_MID}; padding-top: 20px; font-family: ${bodyFont};">
<p style="margin: 0 0 4px 0; font-family: ${headingFont}; font-weight: 600; color: ${GOLD_DARK}; letter-spacing: 0.5px;">Born to Innovate. Built to Last.</p>
<p style="margin: 0;">Message Point Media &middot; Generated automatically by the WFH Daily Report app</p>
</div>
</body>
</html>