[object Object]

← back to NationalPaperHangers

Surface the structured booking brief in the installer notification email

841f9be0c3d70ceec8ad30ac114d57ab1f25351d · 2026-05-18 16:25:45 -0700 · SteveStudio2

UX backlog #6's promise is 'the installer arrives prepped' — but the new
booking email showed only customer/when/type/address/notes. All the brief
data (ceiling height, surface state, access, brand/SKU, rolls, sq ft,
material, who's ordering, wallpaper-selected) was captured and stored but
never delivered where the installer first sees it. Added a 'Project brief'
section via prettyEnum + infoRow helpers (empty fields omitted; handles
both string req.body and boolean DB-row shapes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 841f9be0c3d70ceec8ad30ac114d57ab1f25351d
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Mon May 18 16:25:45 2026 -0700

    Surface the structured booking brief in the installer notification email
    
    UX backlog #6's promise is 'the installer arrives prepped' — but the new
    booking email showed only customer/when/type/address/notes. All the brief
    data (ceiling height, surface state, access, brand/SKU, rolls, sq ft,
    material, who's ordering, wallpaper-selected) was captured and stored but
    never delivered where the installer first sees it. Added a 'Project brief'
    section via prettyEnum + infoRow helpers (empty fields omitted; handles
    both string req.body and boolean DB-row shapes).
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 lib/email.js | 44 +++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 41 insertions(+), 3 deletions(-)

diff --git a/lib/email.js b/lib/email.js
index 8a22809..971b99c 100644
--- a/lib/email.js
+++ b/lib/email.js
@@ -76,22 +76,60 @@ function bookingConfirmationCustomer({ booking, installer, publicUrl }) {
   };
 }
 
+// Enum identifier → human label: 'new_plaster' → 'New plaster'.
+function prettyEnum(v) {
+  if (v === null || v === undefined || v === '') return '';
+  return String(v).replace(/_/g, ' ').replace(/^./, c => c.toUpperCase());
+}
+
+// One label/value <tr>, omitted entirely when the value is empty.
+function infoRow(label, value) {
+  if (value === null || value === undefined || value === '') return '';
+  return `<tr><td style="padding:6px 0;color:#666;width:150px;vertical-align:top">${escapeHtml(label)}</td>` +
+         `<td style="padding:6px 0">${escapeHtml(String(value))}</td></tr>`;
+}
+
 function bookingNotificationInstaller({ booking, installer, publicUrl }) {
   const when = new Date(booking.scheduled_start).toLocaleString('en-US', {
     weekday: 'long', month: 'long', day: 'numeric', hour: 'numeric', minute: '2-digit'
   });
+  // product_sourced arrives as a boolean (DB row) or string (raw req.body).
+  const ps = booking.product_sourced;
+  const productSourced = (ps === true || ps === 'true') ? 'Yes — brand & pattern chosen'
+                       : (ps === false || ps === 'false') ? 'Not yet — wants guidance'
+                       : '';
+  // Structured project brief (UX backlog #6) — surfaced in the email itself so
+  // the installer can scope tools, ladder height, paste type, and roll count
+  // before they ever open the dashboard. The whole point of the brief.
+  const briefRows = [
+    infoRow('Who is ordering', prettyEnum(booking.customer_role)),
+    infoRow('Wallpaper selected', productSourced),
+    infoRow('Material', prettyEnum(booking.material)),
+    infoRow('Brand', booking.brand),
+    infoRow('Pattern / SKU', booking.brand_sku),
+    infoRow('Rooms / surfaces', booking.surfaces),
+    infoRow('Square feet', booking.square_feet ? booking.square_feet + ' sq ft' : ''),
+    infoRow('Ceiling height', booking.ceiling_height_ft ? booking.ceiling_height_ft + ' ft' : ''),
+    infoRow('Estimated rolls', booking.roll_count_estimate),
+    infoRow('Surface state', prettyEnum(booking.surface_state)),
+    infoRow('Access', prettyEnum(booking.access_constraints)),
+    infoRow('Budget band', prettyEnum(booking.budget_band))
+  ].join('');
   return {
     subject: `New booking — ${booking.customer_name} · ${when}`,
     html: `<div style="font-family:Georgia,serif;max-width:560px;margin:0 auto;padding:32px 24px;color:#0e0e0e">
       <h1 style="font-size:22px;margin:0 0 16px">New booking on your calendar</h1>
       <table style="width:100%;border-collapse:collapse;margin:16px 0">
-        <tr><td style="padding:6px 0;color:#666;width:140px">Customer</td><td style="padding:6px 0">${escapeHtml(booking.customer_name)} · ${escapeHtml(booking.customer_email)}${booking.customer_phone ? ' · ' + escapeHtml(booking.customer_phone) : ''}</td></tr>
+        <tr><td style="padding:6px 0;color:#666;width:150px">Customer</td><td style="padding:6px 0">${escapeHtml(booking.customer_name)} · ${escapeHtml(booking.customer_email)}${booking.customer_phone ? ' · ' + escapeHtml(booking.customer_phone) : ''}</td></tr>
         <tr><td style="padding:6px 0;color:#666">When</td><td style="padding:6px 0">${when}</td></tr>
         <tr><td style="padding:6px 0;color:#666">Type</td><td style="padding:6px 0">${escapeHtml(booking.project_type || 'consultation')}</td></tr>
         <tr><td style="padding:6px 0;color:#666">Address</td><td style="padding:6px 0">${[booking.address_line1, booking.city, booking.state, booking.zip].filter(Boolean).map(escapeHtml).join(', ') || '—'}</td></tr>
-        <tr><td style="padding:6px 0;color:#666">Notes</td><td style="padding:6px 0">${escapeHtml(booking.customer_notes || '—')}</td></tr>
       </table>
-      <p><a href="${publicUrl}/admin/bookings" style="display:inline-block;padding:12px 20px;background:#0e0e0e;color:#fff;text-decoration:none;border-radius:2px">Open dashboard</a></p>
+      ${briefRows ? `<h2 style="font-size:14px;text-transform:uppercase;letter-spacing:0.08em;color:#999;margin:24px 0 4px">Project brief</h2>
+      <table style="width:100%;border-collapse:collapse">${briefRows}</table>` : ''}
+      <h2 style="font-size:14px;text-transform:uppercase;letter-spacing:0.08em;color:#999;margin:24px 0 4px">Customer notes</h2>
+      <p style="margin:4px 0 0;line-height:1.55">${escapeHtml(booking.customer_notes || '—')}</p>
+      <p style="margin-top:24px"><a href="${publicUrl}/admin/bookings" style="display:inline-block;padding:12px 20px;background:#0e0e0e;color:#fff;text-decoration:none;border-radius:2px">Open dashboard</a></p>
     </div>`
   };
 }

← 2e14b8b Fix /api/near-me: accept county/town/zip filters without coo  ·  back to NationalPaperHangers  ·  Migration 022: room_captures JSONB on bookings 81398eb →