Add calculator UX enhancements and submission tracking

- Button loading spinner + disable on submit to prevent duplicate AI calls
- Email field and opt-in consent checkbox on calculator form (checked by default)
- Privacy assurance blurb below opt-in
- Refinement blurb on results screen explaining live cash-flow optimization
- calc_submissions SQLite table stores form inputs, computed results, AI text, email, opt-in
- GET /api/calc-submissions endpoint (x-admin-key protected) to retrieve submissions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 10:58:16 -04:00
parent f195c6082d
commit 8a369f6a57
4 changed files with 156 additions and 3 deletions

View File

@@ -53,6 +53,25 @@ db.exec(`
);
`);
db.exec(`
CREATE TABLE IF NOT EXISTS calc_submissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT,
opt_in INTEGER DEFAULT 1,
homesites REAL,
property_type TEXT,
annual_income REAL,
payment_freq TEXT,
reserve_funds REAL,
interest_2025 REAL,
total_potential REAL,
op_interest REAL,
res_interest REAL,
ai_recommendation TEXT,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
`);
// Migrate existing DBs: add new columns if they don't exist yet
const cols = db.pragma('table_info(leads)').map(c => c.name);
if (!cols.includes('org_name')) db.exec('ALTER TABLE leads ADD COLUMN org_name TEXT');
@@ -67,6 +86,19 @@ const insertLead = db.prepare(`
const findByEmail = db.prepare(`SELECT id FROM leads WHERE email = ? LIMIT 1`);
const insertCalcSubmission = db.prepare(`
INSERT INTO calc_submissions
(email, opt_in, homesites, property_type, annual_income, payment_freq,
reserve_funds, interest_2025, total_potential, op_interest, res_interest, ai_recommendation)
VALUES
(@email, @optIn, @homesites, @propertyType, @annualIncome, @paymentFreq,
@reserveFunds, @interest2025, @totalPotential, @opInterest, @resInterest, @aiRecommendation)
`);
const getAllCalcSubmissions = db.prepare(`
SELECT * FROM calc_submissions ORDER BY created_at DESC
`);
const getAllLeads = db.prepare(`
SELECT id, first_name, last_name, email, org_name, state, role, unit_count, beta_interest, source, created_at
FROM leads
@@ -140,11 +172,36 @@ app.get('/api/leads', (req, res) => {
// POST /api/calculate — AI-powered investment recommendation
app.post('/api/calculate', async (req, res) => {
function saveCalcSubmission(aiRecommendation) {
try {
insertCalcSubmission.run({
email: email?.trim() || null,
optIn: optIn ? 1 : 0,
homesites: homesites || null,
propertyType: propertyType || null,
annualIncome: annualIncome || null,
paymentFreq: paymentFreq || null,
reserveFunds: reserveFunds || null,
interest2025: interest2025 || null,
totalPotential: totalPotential || null,
opInterest: opInterest || null,
resInterest: resInterest || null,
aiRecommendation: aiRecommendation || null,
});
} catch (err) {
console.error('Failed to save calc submission:', err.message);
}
}
if (!aiClient) {
saveCalcSubmission(null);
return res.status(503).json({ error: 'AI service not configured.' });
}
const { homesites, propertyType, annualIncome, paymentFreq, reserveFunds, interest2025 } = req.body ?? {};
const {
homesites, propertyType, annualIncome, paymentFreq, reserveFunds, interest2025,
email, optIn, totalPotential, opInterest, resInterest,
} = req.body ?? {};
if (!homesites || !annualIncome) {
return res.status(400).json({ error: 'homesites and annualIncome are required.' });
@@ -185,13 +242,25 @@ Keep the tone professional and factual. No bullet points — flowing paragraph o
if (AI_DEBUG) console.log('[AI_DEBUG] response:', text);
saveCalcSubmission(text);
res.json({ recommendation: text });
} catch (err) {
console.error('AI API error:', err.message);
saveCalcSubmission(null);
res.status(502).json({ error: 'AI service unavailable. Showing estimated result.' });
}
});
// GET /api/calc-submissions — internal: list all calculator submissions
app.get('/api/calc-submissions', (req, res) => {
const secret = req.headers['x-admin-key'];
if (!secret || secret !== process.env.ADMIN_KEY) {
return res.status(401).json({ error: 'Unauthorized.' });
}
const rows = getAllCalcSubmissions.all();
res.json({ count: rows.length, submissions: rows });
});
// Health check
app.get('/api/health', (_req, res) => res.json({ status: 'ok', ts: new Date().toISOString() }));