← back to NationalPaperHangers
views/partials/call-installer.ejs
222 lines
<%# Shared "Call this installer" section — used by all installer-template
variants. Renders a button that opens a 3-mode modal. The customer picks
one of: hold-for-me, click-to-call bridge, or AI agent. Submitting POSTs
to /installer/:slug/call-installer which hands the call to Butlr.
Renders nothing if the studio has no phone on file or the integration
is not configured (butlrEnabled passed false by the route).
Expects in scope: installer, csrfToken, butlrEnabled (boolean). %>
<% if (typeof butlrEnabled !== 'undefined' && butlrEnabled && installer.phone) { %>
<section id="call-installer-section" class="profile-section"
style="max-width:680px;margin:32px auto;padding:24px;border:1px solid var(--border,#ddd);border-radius:8px">
<h2 style="margin:0 0 4px;font-size:22px;letter-spacing:0.01em">Skip the phone tag</h2>
<p class="muted" style="margin:0 0 16px;font-size:14px;line-height:1.55;color:var(--fg-muted,#6a6a6a)">
Let our calling concierge reach <strong><%= installer.business_name %></strong> for you —
wait on hold, bridge you in live, or send an AI agent with your project brief.
</p>
<button type="button" class="btn btn-primary" data-call-open
style="font-size:15px;padding:11px 20px;border:1px solid var(--accent,#6b1a1a);background:var(--accent,#6b1a1a);color:var(--accent-fg,#f5f1e8);border-radius:6px;cursor:pointer;font-family:var(--sans,inherit)">
📞 Call this installer
</button>
<%# ── Modal ──────────────────────────────────────────────────── %>
<div id="call-installer-modal" role="dialog" aria-modal="true" aria-labelledby="call-modal-title"
style="display:none;position:fixed;inset:0;z-index:1000;background:rgba(14,14,14,0.55);
align-items:flex-start;justify-content:center;overflow-y:auto;padding:40px 16px">
<div style="background:var(--bg,#f5f1e8);color:var(--fg,#0e0e0e);max-width:520px;width:100%;
border:1px solid var(--border-strong,#ccc);border-radius:10px;padding:28px 26px;
box-shadow:0 12px 48px rgba(0,0,0,0.3);font-family:var(--sans,inherit)">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:16px">
<h2 id="call-modal-title" style="margin:0;font-size:22px">Call <%= installer.business_name %></h2>
<button type="button" data-call-cancel aria-label="Close"
style="border:none;background:none;font-size:24px;line-height:1;cursor:pointer;color:var(--fg-muted,#6a6a6a)">×</button>
</div>
<p class="muted" style="font-size:13px;line-height:1.5;margin:8px 0 18px;color:var(--fg-muted,#6a6a6a)">
Choose how you'd like the call placed. You're starting this call to a business you chose —
nothing dials without your tap.
</p>
<form id="call-installer-form" method="POST"
action="/installer/<%= installer.slug %>/call-installer"
style="display:grid;gap:16px">
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<%# Mode picker — three radio cards %>
<fieldset style="border:none;padding:0;margin:0;display:grid;gap:10px">
<legend style="font-size:12px;text-transform:uppercase;letter-spacing:0.08em;color:var(--fg-muted,#999);margin-bottom:4px;padding:0">How should we call?</legend>
<label class="call-mode-card" style="display:block;border:1px solid var(--border,#ddd);border-radius:8px;padding:12px 14px;cursor:pointer">
<span style="display:flex;align-items:center;gap:8px;font-weight:600;font-size:15px">
<input type="radio" name="mode" value="hold" checked> Hold for me
</span>
<span style="display:block;font-size:13px;line-height:1.5;color:var(--fg-muted,#6a6a6a);margin-top:4px;padding-left:24px">
We dial the installer, sit through the IVR and hold music, and call <em>you</em> back the moment a real person picks up.
</span>
</label>
<label class="call-mode-card" style="display:block;border:1px solid var(--border,#ddd);border-radius:8px;padding:12px 14px;cursor:pointer">
<span style="display:flex;align-items:center;gap:8px;font-weight:600;font-size:15px">
<input type="radio" name="mode" value="bridge"> Connect us live
</span>
<span style="display:block;font-size:13px;line-height:1.5;color:var(--fg-muted,#6a6a6a);margin-top:4px;padding-left:24px">
A click-to-call bridge — we ring you and the installer and join you both on one live call.
</span>
</label>
<%# ai_agent mode is HELD BACK at launch (DTD panel verdict 2026-05-19,
2/1): an AI autonomously representing a customer to a business is a
materially different risk class than hold/bridge and needs a
dedicated conversational + compliance review before it is exposed.
The route (routes/call-installer.js) and Butlr's API still accept
'ai_agent' — flip the false below to true to re-enable the picker
once the review is done. %>
<% if (false /* ai_agent: held back pending compliance + UX review */) { %>
<label class="call-mode-card" style="display:block;border:1px solid var(--border,#ddd);border-radius:8px;padding:12px 14px;cursor:pointer">
<span style="display:flex;align-items:center;gap:8px;font-weight:600;font-size:15px">
<input type="radio" name="mode" value="ai_agent"> Send an AI agent
</span>
<span style="display:block;font-size:13px;line-height:1.5;color:var(--fg-muted,#6a6a6a);margin-top:4px;padding-left:24px">
Our AI agent calls on your behalf, delivers your project brief, asks about availability, and reports back. It identifies itself as an automated assistant.
</span>
</label>
<% } %>
</fieldset>
<label style="display:block">
<span style="font-size:13px;font-weight:600">Your name</span>
<input type="text" name="customer_name" maxlength="80" autocomplete="name"
style="width:100%;margin-top:4px;padding:9px 10px;border:1px solid var(--border-strong,#ccc);border-radius:6px;background:var(--bg,#fff);color:var(--fg,#000)">
</label>
<label style="display:block">
<span style="font-size:13px;font-weight:600">Your phone number <span style="color:#dc2626">*</span></span>
<input type="tel" name="customer_phone" required maxlength="40" autocomplete="tel"
placeholder="(310) 555-0123"
style="width:100%;margin-top:4px;padding:9px 10px;border:1px solid var(--border-strong,#ccc);border-radius:6px;background:var(--bg,#fff);color:var(--fg,#000)">
<span style="display:block;font-size:12px;color:var(--fg-muted,#6a6a6a);margin-top:3px">
For hold & bridge we call you on this number; for the AI agent we text you the report here.
</span>
</label>
<%# Brief — only relevant to ai_agent; shown/hidden by JS %>
<label id="call-brief-field" style="display:none">
<span style="font-size:13px;font-weight:600">Project brief <span style="color:#dc2626">*</span></span>
<textarea name="brief" maxlength="1500" rows="3"
placeholder="e.g. ~40 yards of grasscloth in a Beverly Hills dining room, hoping to install next month — what's your availability?"
style="width:100%;margin-top:4px;padding:9px 10px;border:1px solid var(--border-strong,#ccc);border-radius:6px;background:var(--bg,#fff);color:var(--fg,#000);font-family:var(--sans,inherit)"></textarea>
<span style="display:block;font-size:12px;color:var(--fg-muted,#6a6a6a);margin-top:3px">
The AI agent reads this to the installer. Be specific.
</span>
</label>
<div id="call-installer-result" role="status" aria-live="polite" style="display:none;font-size:14px;line-height:1.5;padding:11px 13px;border-radius:6px"></div>
<div style="display:flex;gap:10px;align-items:center">
<button type="submit" class="btn btn-primary" data-call-submit
style="font-size:15px;padding:10px 18px;border:1px solid var(--accent,#6b1a1a);background:var(--accent,#6b1a1a);color:var(--accent-fg,#f5f1e8);border-radius:6px;cursor:pointer;font-family:var(--sans,inherit)">
Place the call →
</button>
<button type="button" class="btn btn-ghost" data-call-cancel
style="font-size:14px;padding:10px 14px;border:1px solid var(--border,#ddd);background:none;color:var(--fg,#000);border-radius:6px;cursor:pointer;font-family:var(--sans,inherit)">
Cancel
</button>
</div>
<p style="font-size:11px;color:var(--fg-muted,#6a6a6a);margin:0;line-height:1.5">
By placing this call you consent to a call between you and the installer. Calls may be recorded; both parties hear a recording notice. Standard carrier rates apply.
</p>
</form>
</div>
</div>
<script>
(function(){
var sec = document.getElementById('call-installer-section');
if (!sec) return;
var modal = document.getElementById('call-installer-modal');
var form = document.getElementById('call-installer-form');
var openBtn = sec.querySelector('[data-call-open]');
var briefField = document.getElementById('call-brief-field');
var resultBox = document.getElementById('call-installer-result');
var submitBtn = form.querySelector('[data-call-submit]');
if (!modal || !form || !openBtn) return;
function openModal(){
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
var first = form.querySelector('input[name="customer_name"]');
if (first) first.focus();
}
function closeModal(){
modal.style.display = 'none';
document.body.style.overflow = '';
}
openBtn.addEventListener('click', openModal);
sec.querySelectorAll('[data-call-cancel]').forEach(function(b){
b.addEventListener('click', closeModal);
});
modal.addEventListener('click', function(e){ if (e.target === modal) closeModal(); });
document.addEventListener('keydown', function(e){
if (e.key === 'Escape' && modal.style.display === 'flex') closeModal();
});
// Show the brief field only for the AI-agent mode.
function syncBrief(){
var mode = (form.querySelector('input[name="mode"]:checked') || {}).value;
briefField.style.display = (mode === 'ai_agent') ? 'block' : 'none';
}
form.querySelectorAll('input[name="mode"]').forEach(function(r){
r.addEventListener('change', syncBrief);
});
syncBrief();
function showResult(ok, msg){
resultBox.style.display = 'block';
resultBox.textContent = msg;
resultBox.style.background = ok ? 'rgba(34,139,34,0.12)' : 'rgba(220,38,38,0.10)';
resultBox.style.border = '1px solid ' + (ok ? 'rgba(34,139,34,0.4)' : 'rgba(220,38,38,0.4)');
resultBox.style.color = ok ? '#1a6b1a' : '#b91c1c';
}
form.addEventListener('submit', function(e){
e.preventDefault();
resultBox.style.display = 'none';
submitBtn.disabled = true;
var orig = submitBtn.textContent;
submitBtn.textContent = 'Placing…';
var body = new URLSearchParams(new FormData(form));
fetch(form.action, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-Token': form.querySelector('input[name="_csrf"]').value,
'Accept': 'application/json'
},
body: body.toString()
})
.then(function(r){ return r.json().catch(function(){ return { ok:false, message:'Unexpected response.' }; }); })
.then(function(j){
if (j.ok) {
showResult(true, j.message || 'Call placed.');
form.querySelectorAll('input,textarea,button').forEach(function(el){
if (el.type !== 'button') el.disabled = true;
});
} else {
var msg = j.message || (j.errors && j.errors.join(' ')) || 'The call could not be placed.';
showResult(false, msg);
submitBtn.disabled = false;
submitBtn.textContent = orig;
}
})
.catch(function(){
showResult(false, 'Network error — please try again.');
submitBtn.disabled = false;
submitBtn.textContent = orig;
});
});
})();
</script>
</section>
<% } %>