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:
King Omar 2026-07-10 14:15:03 +10:00
parent e773335034
commit 99b7b68ea9
3 changed files with 104 additions and 24 deletions

View file

@ -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 ─────────────────────────────────────────

View file

@ -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('')}

View file

@ -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);
}