Fix full customer flow integration gaps
Registration: - New signup triggers Google Sheet row + email to Cgabijendra@gmail.com - Subject: 🆕 New Account Registration with name/email/phone Course saves: - 💾 Save now notifies team via Google Sheet + email (high-intent signal) - Deduplication: won't save same course twice for same user - Save redirects back to search results (not dashboard) with green ✅ toast - Button shows "✅ Saved" for the just-saved course Dashboard: - Saved courses split into own card with "Book Consultation" CTA per course - Actual inquiries shown separately with Processing/Received status - Empty state guides user to course search - Quick-stat card shows saved courses count + consultation count separately Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e773335034
commit
99b7b68ea9
3 changed files with 104 additions and 24 deletions
47
src/index.js
47
src/index.js
|
|
@ -61,6 +61,13 @@ app.post('/register', async c => {
|
|||
'INSERT INTO users (email, password_hash, full_name, phone) VALUES (?, ?, ?, ?)'
|
||||
).bind(email, hash, fullName, phone).run();
|
||||
|
||||
// Notify team of new registration — high-intent signal
|
||||
c.executionCtx.waitUntil(pushLeadToKondesk(c.env, {
|
||||
fullName, email, phone,
|
||||
service: '🆕 New Account Registration',
|
||||
message: `New user registered on the portal. Name: ${fullName}, Phone: ${phone}`,
|
||||
}));
|
||||
|
||||
const { token, expires } = await createSession(c.env, result.meta.last_row_id);
|
||||
c.header('Set-Cookie', `session=${token}; Path=/; HttpOnly; SameSite=Lax; Expires=${expires.toUTCString()}`);
|
||||
return c.redirect('/dashboard');
|
||||
|
|
@ -116,7 +123,7 @@ app.get('/courses', async c => {
|
|||
error = r.error || null;
|
||||
}
|
||||
|
||||
return c.html(coursesPage(c.get('user'), results, params, fromCache, error));
|
||||
return c.html(coursesPage(c.get('user'), results, params, fromCache, error, c.req.query('saved')||''));
|
||||
});
|
||||
|
||||
// ── DASHBOARD ───────────────────────────────────────────────────────────────
|
||||
|
|
@ -124,7 +131,7 @@ app.get('/dashboard', async c => {
|
|||
const user = c.get('user');
|
||||
if (!user) return c.redirect('/login?redirect=/dashboard');
|
||||
const [inquiriesRes, profileRes, docsRes, timelineRes] = await Promise.all([
|
||||
c.env.DB.prepare('SELECT * FROM inquiries WHERE user_id=? ORDER BY created_at DESC LIMIT 10').bind(user.id).all(),
|
||||
c.env.DB.prepare('SELECT * FROM inquiries WHERE user_id=? ORDER BY created_at DESC LIMIT 30').bind(user.id).all(),
|
||||
c.env.DB.prepare('SELECT * FROM visa_profiles WHERE user_id=?').bind(user.id).first(),
|
||||
c.env.DB.prepare('SELECT * FROM document_expiries WHERE user_id=? ORDER BY expiry_date ASC LIMIT 5').bind(user.id).all(),
|
||||
c.env.DB.prepare('SELECT * FROM case_timelines WHERE user_id=?').bind(user.id).all(),
|
||||
|
|
@ -150,11 +157,37 @@ app.get('/dashboard', async c => {
|
|||
app.get('/dashboard/save', async c => {
|
||||
const user = c.get('user');
|
||||
if (!user) return c.redirect('/login?redirect=/courses');
|
||||
const { code, name, provider } = { code: c.req.query('code'), name: c.req.query('name'), provider: c.req.query('provider') };
|
||||
await c.env.DB.prepare(
|
||||
'INSERT INTO inquiries (user_id, full_name, email, service, cricos_course_code, cricos_course_name, cricos_provider) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
).bind(user.id, user.full_name, user.email, 'Saved Course', code||'', name||'', provider||'').run();
|
||||
return c.redirect('/dashboard');
|
||||
const code = c.req.query('code') || '';
|
||||
const name = c.req.query('name') || '';
|
||||
const provider = c.req.query('provider') || '';
|
||||
const back = c.req.query('back') || ''; // search URL to return to
|
||||
|
||||
// Check if already saved
|
||||
const existing = await c.env.DB.prepare(
|
||||
'SELECT id FROM inquiries WHERE user_id=? AND cricos_course_code=? AND service=?'
|
||||
).bind(user.id, code, 'Saved Course').first();
|
||||
|
||||
if (!existing) {
|
||||
await c.env.DB.prepare(
|
||||
'INSERT INTO inquiries (user_id, full_name, email, service, cricos_course_code, cricos_course_name, cricos_provider) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
).bind(user.id, user.full_name, user.email, 'Saved Course', code, name, provider).run();
|
||||
|
||||
// Notify team — course save is high-intent
|
||||
c.executionCtx.waitUntil(pushLeadToKondesk(c.env, {
|
||||
fullName: user.full_name,
|
||||
email: user.email,
|
||||
phone: user.phone || '',
|
||||
service: '💾 Course Saved',
|
||||
cricosCode: code,
|
||||
courseName: name,
|
||||
provider,
|
||||
message: `${user.full_name} saved a course to their dashboard: ${name} (${code}) at ${provider}`,
|
||||
}));
|
||||
}
|
||||
|
||||
// Return to courses page with saved=1 flag so we can show a confirmation
|
||||
const returnUrl = back ? `${back}&saved=${encodeURIComponent(code)}` : `/courses?saved=${encodeURIComponent(code)}`;
|
||||
return c.redirect(returnUrl);
|
||||
});
|
||||
|
||||
// ── VISA TRACKER: POINTS CALCULATOR ─────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -489,11 +489,14 @@ function homePage(user) {
|
|||
`, user, '', { url: '/', description: 'Careers Gateway Australia — your trusted partner for education, migration, recruitment, RPL, and visa services. Search 26,000+ CRICOS courses and book a free consultation.', image: 'https://careersgateway.com.au/wp-content/uploads/2025/06/pexels-photo-1236421-1236421-scaled.jpg' });
|
||||
}
|
||||
|
||||
function coursesPage(user, results, params, fromCache, error) {
|
||||
function coursesPage(user, results, params, fromCache, error, savedCode = '') {
|
||||
const hasSearch = Object.values(params).some(v => v);
|
||||
const stateOptions = ['NSW','VIC','QLD','WA','SA','TAS','ACT','NT'];
|
||||
const levelOptions = [['1','Bachelor Degree'],['2','Master Degree'],['3','Doctoral Degree'],['4','Diploma'],['5','Certificate'],['6','Advanced Diploma'],['7','Graduate Certificate'],['8','Graduate Diploma']];
|
||||
|
||||
// Build the back URL (current search) so save redirects back here
|
||||
const backUrl = '/courses?' + Object.entries(params).filter(([,v])=>v).map(([k,v])=>`${k}=${encodeURIComponent(v)}`).join('&');
|
||||
|
||||
const courseMeta = { url: '/courses', description: 'Search 26,000+ CRICOS-registered courses from Australian universities and colleges. Filter by state, level, and field of study. Free account required to view results.', image: 'https://careersgateway.com.au/wp-content/uploads/2025/06/pexels-photo-1236421-1236421-scaled.jpg' };
|
||||
return layout('CRICOS Course Search', `
|
||||
<section style="background:linear-gradient(135deg,#1a2744,#1a5bb8);padding:40px 24px;color:#fff;text-align:center">
|
||||
|
|
@ -504,6 +507,14 @@ function coursesPage(user, results, params, fromCache, error) {
|
|||
</section>
|
||||
<section>
|
||||
<div class="container">
|
||||
${savedCode ? `
|
||||
<div style="background:#dcfce7;border:1px solid #bbf7d0;border-radius:10px;padding:14px 18px;margin-bottom:20px;display:flex;align-items:center;gap:12px">
|
||||
<span style="font-size:1.3rem">✅</span>
|
||||
<div>
|
||||
<strong style="color:#166534">Course saved to your dashboard!</strong>
|
||||
<span style="color:#166534;font-size:.9rem;margin-left:8px">View all saved courses in your <a href="/dashboard" style="color:#15803d;font-weight:700">dashboard</a>.</span>
|
||||
</div>
|
||||
</div>` : ''}
|
||||
<form action="/courses" method="GET" class="search-bar">
|
||||
<div class="search-grid">
|
||||
<div class="form-group" style="margin:0">
|
||||
|
|
@ -606,7 +617,10 @@ function coursesPage(user, results, params, fromCache, error) {
|
|||
</div>
|
||||
<div class="course-actions">
|
||||
<a href="/contact?course=${encodeURIComponent(c.courseName||'')}&code=${encodeURIComponent(c.cricosCode||'')}&provider=${encodeURIComponent(c.provider||'')}" class="btn btn-primary btn-sm">Book Consultation</a>
|
||||
<a href="/dashboard/save?code=${encodeURIComponent(c.cricosCode||'')}&name=${encodeURIComponent(c.courseName||'')}&provider=${encodeURIComponent(c.provider||'')}" class="btn btn-sm" style="background:#f0f6ff;color:#1a5bb8;text-align:center">💾 Save Course</a>
|
||||
${c.cricosCode === savedCode
|
||||
? `<span class="btn btn-sm" style="background:#dcfce7;color:#166534;text-align:center;cursor:default">✅ Saved</span>`
|
||||
: `<a href="/dashboard/save?code=${encodeURIComponent(c.cricosCode||'')}&name=${encodeURIComponent(c.courseName||'')}&provider=${encodeURIComponent(c.provider||'')}&back=${encodeURIComponent(backUrl)}" class="btn btn-sm" style="background:#f0f6ff;color:#1a5bb8;text-align:center">💾 Save</a>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
|
|
|
|||
|
|
@ -646,9 +646,9 @@ export function dashboardOverview(user, { inquiries, profile, points, completedS
|
|||
icon: '📄',
|
||||
},
|
||||
{
|
||||
label: 'Inquiries',
|
||||
value: inquiries.length,
|
||||
sub: 'Total submitted',
|
||||
label: 'Saved Courses',
|
||||
value: inquiries.filter(i => i.service === 'Saved Course').length || '—',
|
||||
sub: inquiries.filter(i => i.service !== 'Saved Course').length + ' consultation request' + (inquiries.filter(i => i.service !== 'Saved Course').length !== 1 ? 's' : ''),
|
||||
color: '#1a5bb8',
|
||||
href: '/contact',
|
||||
icon: '📝',
|
||||
|
|
@ -715,18 +715,51 @@ export function dashboardOverview(user, { inquiries, profile, points, completedS
|
|||
</div>
|
||||
</div>
|
||||
|
||||
${inquiries.length > 0 ? `
|
||||
<div style="background:#fff;border-radius:12px;padding:24px;box-shadow:0 2px 8px rgba(0,0,0,.05)">
|
||||
<div style="font-weight:700;color:#1a2744;font-size:1rem;margin-bottom:16px">📋 Recent Inquiries</div>
|
||||
${inquiries.slice(0,5).map(i => `
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px solid #f1f5f9;flex-wrap:wrap;gap:8px">
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:.9rem;color:#1a2744">${esc(i.service||'General Inquiry')}${i.cricos_course_name ? ` — ${esc(i.cricos_course_name)}` : ''}</div>
|
||||
<div style="font-size:.8rem;color:#94a3b8">${new Date(i.created_at).toLocaleDateString('en-AU',{day:'numeric',month:'short',year:'numeric'})}</div>
|
||||
</div>
|
||||
<span style="background:${i.kondesk_sent?'#dcfce7':'#f1f5f9'};color:${i.kondesk_sent?'#166534':'#374151'};padding:3px 10px;border-radius:12px;font-size:.8rem;font-weight:600">${i.kondesk_sent?'✓ Processing':'Received'}</span>
|
||||
</div>`).join('')}
|
||||
</div>` : ''}`;
|
||||
${(() => {
|
||||
const savedCourses = inquiries.filter(i => i.service === 'Saved Course');
|
||||
const actualInquiries = inquiries.filter(i => i.service !== 'Saved Course');
|
||||
return `
|
||||
${savedCourses.length > 0 ? `
|
||||
<div style="background:#fff;border-radius:12px;padding:24px;box-shadow:0 2px 8px rgba(0,0,0,.05)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
||||
<div style="font-weight:700;color:#1a2744;font-size:1rem">💾 Saved Courses (${savedCourses.length})</div>
|
||||
<a href="/courses" style="font-size:.85rem;color:#1a5bb8;font-weight:600">Search more →</a>
|
||||
</div>
|
||||
${savedCourses.slice(0,5).map(i => `
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px solid #f1f5f9;flex-wrap:wrap;gap:8px">
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:.9rem;color:#1a2744">${esc(i.cricos_course_name||'Course')}</div>
|
||||
<div style="font-size:.8rem;color:#64748b">${esc(i.cricos_provider||'')}${i.cricos_course_code ? ` · CRICOS: ${esc(i.cricos_course_code)}` : ''}</div>
|
||||
<div style="font-size:.78rem;color:#94a3b8">${new Date(i.created_at).toLocaleDateString('en-AU',{day:'numeric',month:'short',year:'numeric'})}</div>
|
||||
</div>
|
||||
<a href="/contact?course=${encodeURIComponent(i.cricos_course_name||'')}&code=${encodeURIComponent(i.cricos_course_code||'')}&provider=${encodeURIComponent(i.cricos_provider||'')}"
|
||||
style="background:#1a5bb8;color:#fff;padding:6px 14px;border-radius:6px;font-size:.82rem;font-weight:600;text-decoration:none;white-space:nowrap">
|
||||
Book Consultation
|
||||
</a>
|
||||
</div>`).join('')}
|
||||
</div>` : ''}
|
||||
|
||||
${actualInquiries.length > 0 ? `
|
||||
<div style="background:#fff;border-radius:12px;padding:24px;box-shadow:0 2px 8px rgba(0,0,0,.05)">
|
||||
<div style="font-weight:700;color:#1a2744;font-size:1rem;margin-bottom:16px">📋 My Inquiries (${actualInquiries.length})</div>
|
||||
${actualInquiries.slice(0,5).map(i => `
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px solid #f1f5f9;flex-wrap:wrap;gap:8px">
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:.9rem;color:#1a2744">${esc(i.service||'General Inquiry')}${i.cricos_course_name ? ` — ${esc(i.cricos_course_name)}` : ''}</div>
|
||||
<div style="font-size:.8rem;color:#94a3b8">${new Date(i.created_at).toLocaleDateString('en-AU',{day:'numeric',month:'short',year:'numeric'})}</div>
|
||||
</div>
|
||||
<span style="background:${i.kondesk_sent?'#dcfce7':'#f1f5f9'};color:${i.kondesk_sent?'#166534':'#374151'};padding:3px 10px;border-radius:12px;font-size:.8rem;font-weight:600">${i.kondesk_sent?'✓ Processing':'Received'}</span>
|
||||
</div>`).join('')}
|
||||
</div>` : ''}
|
||||
|
||||
${savedCourses.length === 0 && actualInquiries.length === 0 ? `
|
||||
<div style="background:#fff;border-radius:12px;padding:32px;box-shadow:0 2px 8px rgba(0,0,0,.05);text-align:center;color:#94a3b8">
|
||||
<div style="font-size:2rem;margin-bottom:10px">🎓</div>
|
||||
<div style="font-weight:600;color:#1a2744;margin-bottom:6px">You haven't saved any courses yet</div>
|
||||
<p style="font-size:.9rem;margin-bottom:16px">Search CRICOS courses and save the ones you're interested in.</p>
|
||||
<a href="/courses" class="btn btn-primary" style="padding:10px 24px;font-size:.9rem">Search Courses</a>
|
||||
</div>` : ''}`;
|
||||
})()}`;
|
||||
|
||||
return dashWrap(user, 'overview', body);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue