[object Object]

← back to NationalPaperHangers

Surface room captures in the installer brief — admin page + email

a1dfc05577809daceee55b1b290be55cca930d92 · 2026-05-18 16:49:55 -0700 · SteveStudio2

Admin booking-detail gets a 'Room captures' section: per room the wall
photo, walkthrough-video link, confirmed dimensions, and — when the
customer supplied their own wallpaper — its specs plus a true-scale tiled
preview on a wall of the measured proportions. The installer notification
email gets a matching 'Room captures' summary with absolute photo/video
links. Installer quotes from real footage instead of a typed guess.

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

Files touched

Diff

commit a1dfc05577809daceee55b1b290be55cca930d92
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Mon May 18 16:49:55 2026 -0700

    Surface room captures in the installer brief — admin page + email
    
    Admin booking-detail gets a 'Room captures' section: per room the wall
    photo, walkthrough-video link, confirmed dimensions, and — when the
    customer supplied their own wallpaper — its specs plus a true-scale tiled
    preview on a wall of the measured proportions. The installer notification
    email gets a matching 'Room captures' summary with absolute photo/video
    links. Installer quotes from real footage instead of a typed guess.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 lib/email.js                   | 20 ++++++++++++++++
 views/admin/booking-detail.ejs | 54 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 74 insertions(+)

diff --git a/lib/email.js b/lib/email.js
index 971b99c..c082837 100644
--- a/lib/email.js
+++ b/lib/email.js
@@ -115,6 +115,24 @@ function bookingNotificationInstaller({ booking, installer, publicUrl }) {
     infoRow('Access', prettyEnum(booking.access_constraints)),
     infoRow('Budget band', prettyEnum(booking.budget_band))
   ].join('');
+  // Camera-captured rooms (UX #6) — photo/video links + confirmed dimensions
+  // + any customer-supplied wallpaper. URLs are sanitiser-validated upstream.
+  const captures = Array.isArray(booking.room_captures) ? booking.room_captures : [];
+  const captureRows = captures.map(function (rc) {
+    const dims = (rc.wall_width_ft && rc.wall_height_ft)
+      ? rc.wall_width_ft + ' × ' + rc.wall_height_ft + ' ft' : 'not measured';
+    const links = ['<a href="' + publicUrl + rc.photo_url + '">wall photo</a>'];
+    if (rc.video_url) links.push('<a href="' + publicUrl + rc.video_url + '">walkthrough</a>');
+    const wp = rc.wallpaper
+      ? '<br><span style="color:#666">Wallpaper — ' + escapeHtml(String(rc.wallpaper.width_in))
+        + '" panel · ' + escapeHtml(String(rc.wallpaper.repeat_in)) + '" repeat · '
+        + escapeHtml(prettyEnum(rc.wallpaper.match_type)) + ' match · '
+        + '<a href="' + publicUrl + rc.wallpaper.image_url + '">image</a></span>'
+      : '';
+    return '<tr><td style="padding:6px 0;color:#666;width:150px;vertical-align:top">'
+      + escapeHtml(rc.room || 'Room') + '</td><td style="padding:6px 0">'
+      + dims + ' · ' + links.join(' · ') + wp + '</td></tr>';
+  }).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">
@@ -127,6 +145,8 @@ function bookingNotificationInstaller({ booking, installer, publicUrl }) {
       </table>
       ${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>` : ''}
+      ${captureRows ? `<h2 style="font-size:14px;text-transform:uppercase;letter-spacing:0.08em;color:#999;margin:24px 0 4px">Room captures (${captures.length})</h2>
+      <table style="width:100%;border-collapse:collapse">${captureRows}</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>
diff --git a/views/admin/booking-detail.ejs b/views/admin/booking-detail.ejs
index 3e93fe1..29b833e 100644
--- a/views/admin/booking-detail.ejs
+++ b/views/admin/booking-detail.ejs
@@ -1,6 +1,17 @@
 <%- include('../partials/head', { title, admin: true }) %>
 <%- include('partials/admin-header') %>
 
+<style>
+  .rc-admin-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:18px}
+  .rc-admin-card{border:1px solid var(--border,#d4d2c8);border-radius:10px;padding:14px;background:var(--card-bg,#fff)}
+  .rc-admin-card h3{font-size:15px;margin:0 0 8px}
+  .rc-admin-photo{display:block;max-width:100%;border-radius:6px}
+  .rc-admin-wp{margin-top:10px;border-top:1px solid var(--border,#eee);padding-top:10px}
+  .rc-admin-wp-spec{font-size:13px;margin:0 0 8px}
+  .rc-admin-preview{width:100%;border-radius:6px;border:1px solid var(--border,#ddd);background-repeat:repeat}
+  .rc-admin-card .muted{color:var(--muted,#888);font-weight:400;font-size:12px}
+</style>
+
 <%
   function fmt(v) { return (v === null || v === undefined || v === '') ? '—' : v; }
   function pretty(v) { return v ? String(v).replace(/_/g, ' ') : '—'; }
@@ -64,6 +75,49 @@
       </dl>
     </section>
 
+    <% if (booking.room_captures && booking.room_captures.length) { %>
+      <section class="admin-section" style="grid-column:1/-1">
+        <h2>Room captures <span class="muted">(<%= booking.room_captures.length %>)</span></h2>
+        <p class="muted" style="font-size:12px;margin:0 0 14px">Customer-photographed walls with confirmed dimensions — quote from these instead of the typed estimate.</p>
+        <div class="rc-admin-grid">
+          <% booking.room_captures.forEach(function (rc) {
+               var measured = rc.wall_width_ft && rc.wall_height_ft;
+               var dims = measured ? (rc.wall_width_ft + ' × ' + rc.wall_height_ft + ' ft') : 'not measured';
+               var wp = rc.wallpaper;
+          %>
+            <div class="rc-admin-card">
+              <h3><%= rc.room || 'Room' %> <span class="muted">· <%= dims %></span></h3>
+              <% if (rc.photo_url) { %>
+                <a href="<%= rc.photo_url %>" target="_blank" rel="noopener">
+                  <img src="<%= rc.photo_url %>" alt="Wall photo" class="rc-admin-photo">
+                </a>
+              <% } %>
+              <% if (rc.video_url) { %>
+                <p style="margin:8px 0 0"><a href="<%= rc.video_url %>" target="_blank" rel="noopener">▶ Room walkthrough video</a></p>
+              <% } %>
+              <% if (wp) { %>
+                <div class="rc-admin-wp">
+                  <p class="rc-admin-wp-spec"><strong>Customer's wallpaper</strong><br>
+                    <%= wp.width_in %>″ panel · <%= wp.repeat_in %>″ repeat · <%= pretty(wp.match_type) %> match</p>
+                  <% if (measured) {
+                       var tw = ((wp.width_in / 12) / rc.wall_width_ft * 100).toFixed(2);
+                       var th = ((wp.repeat_in / 12) / rc.wall_height_ft * 100).toFixed(2);
+                  %>
+                    <div class="rc-admin-preview"
+                         style="aspect-ratio:<%= rc.wall_width_ft %>/<%= rc.wall_height_ft %>;background-image:url('<%= wp.image_url %>');background-size:<%= tw %>% <%= th %>%"></div>
+                    <p class="muted">Shown at true scale on a <%= dims %> wall.</p>
+                  <% } else { %>
+                    <a href="<%= wp.image_url %>" target="_blank" rel="noopener">
+                      <img src="<%= wp.image_url %>" alt="Wallpaper" class="rc-admin-photo"></a>
+                  <% } %>
+                </div>
+              <% } %>
+            </div>
+          <% }); %>
+        </div>
+      </section>
+    <% } %>
+
     <% if (booking.customer_notes) { %>
       <section class="admin-section">
         <h2>Customer notes</h2>

← 6557a78 Persist room_captures on booking POST + derive sq ft from me  ·  back to NationalPaperHangers  ·  e2e-room-capture: 14-assertion test for the camera quote flo 5c5c9df →