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:
71
server.js
71
server.js
@@ -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() }));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user