diff --git a/migrations/003_visa_tracker.sql b/migrations/003_visa_tracker.sql new file mode 100644 index 0000000..670ef9f --- /dev/null +++ b/migrations/003_visa_tracker.sql @@ -0,0 +1,46 @@ +CREATE TABLE IF NOT EXISTS visa_profiles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER UNIQUE NOT NULL, + occupation_anzsco TEXT, + occupation_name TEXT, + visa_subclass TEXT, + age INTEGER, + english_level TEXT, + education_level TEXT, + aus_study_years REAL, + professional_year INTEGER DEFAULT 0, + overseas_work_years REAL, + aus_work_years REAL, + partner_skills INTEGER DEFAULT 0, + naati INTEGER DEFAULT 0, + regional_study INTEGER DEFAULT 0, + state_nomination TEXT, + updated_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (user_id) REFERENCES users(id) +); + +CREATE TABLE IF NOT EXISTS case_timelines ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + stage TEXT NOT NULL, + milestone_date TEXT, + notes TEXT, + updated_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (user_id) REFERENCES users(id) +); + +CREATE TABLE IF NOT EXISTS document_expiries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + doc_type TEXT NOT NULL, + doc_label TEXT NOT NULL, + expiry_date TEXT NOT NULL, + reminder_days INTEGER DEFAULT 180, + created_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (user_id) REFERENCES users(id) +); + +CREATE INDEX IF NOT EXISTS idx_visa_profiles_user ON visa_profiles(user_id); +CREATE INDEX IF NOT EXISTS idx_timelines_user ON case_timelines(user_id); +CREATE INDEX IF NOT EXISTS idx_docs_user ON document_expiries(user_id); +CREATE INDEX IF NOT EXISTS idx_docs_expiry ON document_expiries(expiry_date); diff --git a/src/index.js b/src/index.js index 03b8bdb..1f8d7ad 100644 --- a/src/index.js +++ b/src/index.js @@ -4,8 +4,12 @@ import { searchCricos } from './cricos.js'; import { pushLeadToKondesk } from './kondesk.js'; import { homePage, coursesPage, registerPage, loginPage, - dashboardPage, contactPage, healthInsurancePage, servicesPage, esc + dashboardPage, contactPage, healthInsurancePage, servicesPage, layout, esc } from './templates.js'; +import { + pointsPage, timelinePage, documentsPage, feesPage, + occupationsPage, stateCriteriaPage, processingTimesPage, englishPage, studentFundPage +} from './visa-tracker.js'; const app = new Hono(); @@ -123,10 +127,140 @@ app.get('/dashboard/save', async c => { return c.redirect('/dashboard'); }); +// ── VISA TRACKER: POINTS CALCULATOR ───────────────────────────────────────── +app.get('/dashboard/points', async c => { + const user = c.get('user'); + if (!user) return c.redirect('/login?redirect=/dashboard/points'); + const profile = await c.env.DB.prepare('SELECT * FROM visa_profiles WHERE user_id=?').bind(user.id).first(); + return c.html(layout('Points Calculator', pointsPage(user, profile, null), user)); +}); + +app.post('/dashboard/points', async c => { + const user = c.get('user'); + if (!user) return c.redirect('/login'); + const form = await c.req.formData(); + const p = { + occupation_anzsco: form.get('occupation_anzsco')||'', + occupation_name: form.get('occupation_name')||'', + visa_subclass: form.get('visa_subclass')||'', + age: parseInt(form.get('age'))||0, + english_level: form.get('english_level')||'', + education_level: form.get('education_level')||'', + aus_study_years: form.get('aus_study_years')||'none', + professional_year: form.get('professional_year')==='1'?1:0, + overseas_work_years: form.get('overseas_work_years')||'lt3', + aus_work_years: form.get('aus_work_years')||'lt1', + partner_skills: parseInt(form.get('partner_skills'))||0, + naati: form.get('naati')==='1'?1:0, + regional_study: form.get('regional_study')==='1'?1:0, + state_nomination: form.get('state_nomination')||'', + }; + await c.env.DB.prepare(`INSERT INTO visa_profiles (user_id,occupation_anzsco,occupation_name,visa_subclass,age,english_level,education_level,aus_study_years,professional_year,overseas_work_years,aus_work_years,partner_skills,naati,regional_study,state_nomination,updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now')) + ON CONFLICT(user_id) DO UPDATE SET occupation_anzsco=excluded.occupation_anzsco,occupation_name=excluded.occupation_name,visa_subclass=excluded.visa_subclass,age=excluded.age,english_level=excluded.english_level,education_level=excluded.education_level,aus_study_years=excluded.aus_study_years,professional_year=excluded.professional_year,overseas_work_years=excluded.overseas_work_years,aus_work_years=excluded.aus_work_years,partner_skills=excluded.partner_skills,naati=excluded.naati,regional_study=excluded.regional_study,state_nomination=excluded.state_nomination,updated_at=datetime('now')`) + .bind(user.id,p.occupation_anzsco,p.occupation_name,p.visa_subclass,p.age,p.english_level,p.education_level,p.aus_study_years,p.professional_year,p.overseas_work_years,p.aus_work_years,p.partner_skills,p.naati,p.regional_study,p.state_nomination).run(); + return c.html(layout('Points Calculator', pointsPage(user, p, 'Points saved!'), user)); +}); + +// ── VISA TRACKER: TIMELINE ─────────────────────────────────────────────────── +app.get('/dashboard/timeline', async c => { + const user = c.get('user'); + if (!user) return c.redirect('/login?redirect=/dashboard/timeline'); + const { results } = await c.env.DB.prepare('SELECT * FROM case_timelines WHERE user_id=?').bind(user.id).all(); + return c.html(layout('Visa Timeline', timelinePage(user, results, null), user)); +}); + +app.post('/dashboard/timeline', async c => { + const user = c.get('user'); + if (!user) return c.redirect('/login'); + const form = await c.req.formData(); + const { TIMELINE_STAGES } = await import('./visa-data.js'); + for (const stage of TIMELINE_STAGES) { + const date = form.get(`date_${stage.key}`) || null; + const notes = form.get(`notes_${stage.key}`) || null; + if (date || notes) { + await c.env.DB.prepare(`INSERT INTO case_timelines (user_id,stage,milestone_date,notes,updated_at) VALUES (?,?,?,?,datetime('now')) + ON CONFLICT(user_id,stage) DO UPDATE SET milestone_date=excluded.milestone_date,notes=excluded.notes,updated_at=datetime('now')`) + .bind(user.id, stage.key, date||null, notes||null).run().catch(()=>{}); + } + } + const { results } = await c.env.DB.prepare('SELECT * FROM case_timelines WHERE user_id=?').bind(user.id).all(); + return c.html(layout('Visa Timeline', timelinePage(user, results, 'Timeline saved!'), user)); +}); + +// ── VISA TRACKER: DOCUMENT EXPIRY ──────────────────────────────────────────── +app.get('/dashboard/documents', async c => { + const user = c.get('user'); + if (!user) return c.redirect('/login?redirect=/dashboard/documents'); + const { results } = await c.env.DB.prepare('SELECT * FROM document_expiries WHERE user_id=? ORDER BY expiry_date ASC').bind(user.id).all(); + return c.html(layout('Document Expiry', documentsPage(user, results, null), user)); +}); + +app.post('/dashboard/documents', async c => { + const user = c.get('user'); + if (!user) return c.redirect('/login'); + const form = await c.req.formData(); + const preset = form.get('doc_label') || ''; + const custom = (form.get('custom_label') || '').trim(); + const label = custom || preset; + const expiry = form.get('expiry_date') || ''; + if (label && expiry) { + await c.env.DB.prepare('INSERT INTO document_expiries (user_id,doc_type,doc_label,expiry_date) VALUES (?,?,?,?)') + .bind(user.id, preset, label, expiry).run(); + } + return c.redirect('/dashboard/documents'); +}); + +app.post('/dashboard/documents/delete', async c => { + const user = c.get('user'); + if (!user) return c.redirect('/login'); + const form = await c.req.formData(); + const id = parseInt(form.get('id')); + if (id) await c.env.DB.prepare('DELETE FROM document_expiries WHERE id=? AND user_id=?').bind(id, user.id).run(); + return c.redirect('/dashboard/documents'); +}); + +// ── VISA TRACKER: STATIC TOOLS ─────────────────────────────────────────────── +app.get('/dashboard/fees', c => { + const user = c.get('user'); + if (!user) return c.redirect('/login?redirect=/dashboard/fees'); + return c.html(layout('VAC Fee Calculator', feesPage(user), user)); +}); + +app.get('/dashboard/occupations', c => { + const user = c.get('user'); + if (!user) return c.redirect('/login?redirect=/dashboard/occupations'); + return c.html(layout('Occupation Search', occupationsPage(user, c.req.query('q')||''), user)); +}); + +app.get('/dashboard/state-criteria', c => { + const user = c.get('user'); + if (!user) return c.redirect('/login?redirect=/dashboard/state-criteria'); + return c.html(layout('State Nomination Criteria', stateCriteriaPage(user), user)); +}); + +app.get('/dashboard/processing-times', c => { + const user = c.get('user'); + if (!user) return c.redirect('/login?redirect=/dashboard/processing-times'); + return c.html(layout('Processing Times', processingTimesPage(user), user)); +}); + +app.get('/dashboard/english', c => { + const user = c.get('user'); + if (!user) return c.redirect('/login?redirect=/dashboard/english'); + return c.html(layout('English Requirements', englishPage(user), user)); +}); + +app.get('/dashboard/student-fund', c => { + const user = c.get('user'); + if (!user) return c.redirect('/login?redirect=/dashboard/student-fund'); + return c.html(layout('Student Fund Calculator', studentFundPage(user), user)); +}); + +// ── PROFILE ────────────────────────────────────────────────────────────────── app.get('/dashboard/profile', async c => { const user = c.get('user'); if (!user) return c.redirect('/login'); - const { layout, esc } = await import('./templates.js'); return c.html(layout('My Profile', `
diff --git a/src/visa-data.js b/src/visa-data.js new file mode 100644 index 0000000..d8d85e7 --- /dev/null +++ b/src/visa-data.js @@ -0,0 +1,326 @@ +// ── POINTS CALCULATOR ─────────────────────────────────────────────────────── +export const POINTS_TABLE = { + age: [ + { label: '18–24', min: 18, max: 24, points: 25 }, + { label: '25–32', min: 25, max: 32, points: 30 }, + { label: '33–39', min: 33, max: 39, points: 25 }, + { label: '40–44', min: 40, max: 44, points: 15 }, + { label: '45+', min: 45, max: 99, points: 0 }, + ], + english: [ + { label: 'Competent (IELTS 6 / PTE 50)', value: 'competent', points: 0 }, + { label: 'Proficient (IELTS 7 / PTE 65)', value: 'proficient', points: 10 }, + { label: 'Superior (IELTS 8 / PTE 79)', value: 'superior', points: 20 }, + ], + education: [ + { label: 'Doctorate (PhD)', value: 'phd', points: 20 }, + { label: 'Bachelor / Masters / Diploma', value: 'bachelor', points: 15 }, + { label: 'None of the above', value: 'none', points: 0 }, + ], + ausStudy: [ + { label: 'No Australian study', value: 'none', points: 0 }, + { label: '1–2 years in Australia', value: '1-2', points: 5 }, + { label: '2+ years in Australia', value: '2+', points: 5 }, + ], + professionalYear: { points: 5, label: 'Completed Professional Year program (+5)' }, + overseasWork: [ + { label: 'Less than 3 years', value: 'lt3', points: 0 }, + { label: '3–4 years', value: '3-4', points: 5 }, + { label: '5–7 years', value: '5-7', points: 10 }, + { label: '8+ years', value: '8+', points: 15 }, + ], + ausWork: [ + { label: 'Less than 1 year', value: 'lt1', points: 0 }, + { label: '1–2 years', value: '1-2', points: 5 }, + { label: '3–4 years', value: '3-4', points: 10 }, + { label: '5–7 years', value: '5-7', points: 15 }, + { label: '8+ years', value: '8+', points: 20 }, + ], + partnerSkills: { points: 10, label: 'Partner has competent English + nominated occupation / AQF Cert IV or above (+10)' }, + singleOrAusCitizen: { points: 10, label: 'Single / partner is Australian citizen or PR (+10)' }, + naati: { points: 5, label: 'NAATI community language credential (+5)' }, + nomination190: { points: 5, label: 'State/Territory nomination — Subclass 190 (+5)' }, + nomination491: { points: 15, label: 'State/Territory nomination — Subclass 491 (+15)' }, + regionalStudy: { points: 5, label: 'Study in regional Australia (+5)' }, +}; + +export function calcPoints(profile) { + let pts = 0; + const breakdown = []; + + // Age + const age = parseInt(profile.age) || 0; + const ageBand = POINTS_TABLE.age.find(b => age >= b.min && age <= b.max); + if (ageBand) { pts += ageBand.points; breakdown.push({ label: `Age (${ageBand.label})`, points: ageBand.points }); } + + // English + const eng = POINTS_TABLE.english.find(e => e.value === profile.english_level); + if (eng) { pts += eng.points; breakdown.push({ label: `English (${eng.label})`, points: eng.points }); } + + // Education + const edu = POINTS_TABLE.education.find(e => e.value === profile.education_level); + if (edu) { pts += edu.points; breakdown.push({ label: `Education (${edu.label})`, points: edu.points }); } + + // Australian study + const ausStudy = POINTS_TABLE.ausStudy.find(e => e.value === profile.aus_study_years); + if (ausStudy && ausStudy.points) { pts += ausStudy.points; breakdown.push({ label: `Australian Study (${ausStudy.label})`, points: ausStudy.points }); } + + // Professional year + if (profile.professional_year == 1) { pts += 5; breakdown.push({ label: 'Professional Year', points: 5 }); } + + // Overseas work + const owork = POINTS_TABLE.overseasWork.find(e => e.value === profile.overseas_work_years); + if (owork && owork.points) { pts += owork.points; breakdown.push({ label: `Overseas Work (${owork.label})`, points: owork.points }); } + + // Australian work + const awork = POINTS_TABLE.ausWork.find(e => e.value === profile.aus_work_years); + if (awork && awork.points) { pts += awork.points; breakdown.push({ label: `Australian Work (${awork.label})`, points: awork.points }); } + + // Partner / single + if (profile.partner_skills == 1) { pts += 10; breakdown.push({ label: 'Partner Skills', points: 10 }); } + else if (profile.partner_skills == 2) { pts += 10; breakdown.push({ label: 'Single / AUS Citizen Partner', points: 10 }); } + + // NAATI + if (profile.naati == 1) { pts += 5; breakdown.push({ label: 'NAATI Community Language', points: 5 }); } + + // Regional study + if (profile.regional_study == 1) { pts += 5; breakdown.push({ label: 'Regional Australian Study', points: 5 }); } + + // State nomination + if (profile.state_nomination === '190') { pts += 5; breakdown.push({ label: 'State Nomination (190)', points: 5 }); } + else if (profile.state_nomination === '491') { pts += 15; breakdown.push({ label: 'State Nomination (491)', points: 15 }); } + + return { total: pts, breakdown }; +} + +// ── VAC FEES ───────────────────────────────────────────────────────────────── +export const VAC_FEES = [ + { subclass: '189', name: 'Skilled Independent', primary: 4770, secondary: 2385, child: 1195 }, + { subclass: '190', name: 'Skilled Nominated', primary: 4770, secondary: 2385, child: 1195 }, + { subclass: '491', name: 'Skilled Work Regional', primary: 4770, secondary: 2385, child: 1195 }, + { subclass: '186', name: 'Employer Nomination (ENS)', primary: 4770, secondary: 2385, child: 1195 }, + { subclass: '187', name: 'Regional Sponsored (RSMS)', primary: 4770, secondary: 2385, child: 1195 }, + { subclass: '482', name: 'Temporary Skill Shortage', primary: 3115, secondary: 1560, child: 1560 }, + { subclass: '494', name: 'Skilled Employer Sponsored Regional', primary: 4770, secondary: 2385, child: 1195 }, + { subclass: '500', name: 'Student Visa', primary: 710, secondary: 355, child: 355 }, + { subclass: '485', name: 'Graduate Temporary', primary: 1895, secondary: 950, child: 475 }, + { subclass: '820', name: 'Partner (onshore)', primary: 8850, secondary: 4425, child: 0 }, + { subclass: '801', name: 'Partner (permanent onshore)',primary: 0, secondary: 0, child: 0, note: 'No additional fee if applying with 820' }, + { subclass: '309', name: 'Partner (offshore)', primary: 8850, secondary: 4425, child: 0 }, + { subclass: '100', name: 'Partner (permanent offshore)',primary: 0, secondary: 0, child: 0, note: 'No additional fee if applying with 309' }, + { subclass: '600', name: 'Visitor Visa', primary: 190, secondary: 190, child: 190 }, + { subclass: '407', name: 'Training Visa', primary: 335, secondary: 170, child: 170 }, +]; + +// ── PROCESSING TIMES ────────────────────────────────────────────────────────── +export const PROCESSING_TIMES = [ + { subclass: '189', name: 'Skilled Independent', p25: 12, p50: 20, p75: 30, p90: 48, note: 'Invitation required via SkillSelect EOI' }, + { subclass: '190', name: 'Skilled Nominated', p25: 8, p50: 14, p75: 22, p90: 36, note: 'State nomination required first' }, + { subclass: '491', name: 'Skilled Work Regional',p25: 8, p50: 14, p75: 22, p90: 36, note: 'State/family nomination required' }, + { subclass: '482', name: 'TSS (employer)', p25: 1, p50: 3, p75: 5, p90: 9, note: 'Short stream approx.' }, + { subclass: '186', name: 'ENS (employer nom.)', p25: 8, p50: 14, p75: 24, p90: 36, note: 'Direct entry stream' }, + { subclass: '494', name: 'Employer Sponsored Regional', p25: 8, p50: 14, p75: 22, p90: 36, note: '' }, + { subclass: '500', name: 'Student Visa', p25: 1, p50: 2, p75: 3, p90: 5, note: 'Varies by nationality and provider' }, + { subclass: '485', name: 'Graduate Temporary', p25: 2, p50: 4, p75: 7, p90: 12, note: '2-year or 4-year stream' }, + { subclass: '820', name: 'Partner (onshore)', p25: 12, p50: 24, p75: 36, p90: 48, note: 'Bridging visa granted while waiting' }, + { subclass: '309', name: 'Partner (offshore)', p25: 12, p50: 24, p75: 36, p90: 48, note: '' }, + { subclass: '600', name: 'Visitor Visa', p25: 1, p50: 1, p75: 2, p90: 3, note: 'Online applications' }, +]; + +// ── STATE NOMINATION CRITERIA ───────────────────────────────────────────────── +export const STATE_CRITERIA = [ + { + state: 'NSW', fullName: 'New South Wales', + sc190: { minPoints: 90, notes: 'Must have employment offer or strong ties to NSW. High demand occupations only.' }, + sc491: { minPoints: 85, notes: 'Regional NSW only. Must live/work in designated regional areas.' }, + website: 'https://www.nsw.gov.au/working-in-nsw/visa-and-migration', + }, + { + state: 'VIC', fullName: 'Victoria', + sc190: { minPoints: 90, notes: 'Expression of Interest submitted via Victoria\'s Skilled Visa portal. Occupations from Victorian list.' }, + sc491: { minPoints: 85, notes: 'Must live and work in regional Victoria.' }, + website: 'https://business.vic.gov.au/visa-and-migration', + }, + { + state: 'QLD', fullName: 'Queensland', + sc190: { minPoints: 85, notes: 'QLD Skills in Demand list. Must intend to live and work in QLD.' }, + sc491: { minPoints: 80, notes: 'Regional QLD. Must live in designated regions.' }, + website: 'https://migration.qld.gov.au/', + }, + { + state: 'WA', fullName: 'Western Australia', + sc190: { minPoints: 80, notes: 'WA Skilled Migration Occupation List. Strong preference for in-demand trades/healthcare.' }, + sc491: { minPoints: 75, notes: 'Regional WA. Must reside in regional WA.' }, + website: 'https://www.wa.gov.au/service/employment/migration/migrate-western-australia', + }, + { + state: 'SA', fullName: 'South Australia', + sc190: { minPoints: 75, notes: 'SA Critical Skills List. Lower threshold but strong occupation requirement.' }, + sc491: { minPoints: 65, notes: 'Regional SA. Must live and work in SA.' }, + website: 'https://www.migration.sa.gov.au/', + }, + { + state: 'TAS', fullName: 'Tasmania', + sc190: { minPoints: 65, notes: 'Open to most eligible occupations. Must intend to live and work in Tasmania.' }, + sc491: { minPoints: 60, notes: 'All of Tasmania is classified as regional.' }, + website: 'https://www.migration.tas.gov.au/', + }, + { + state: 'ACT', fullName: 'Australian Capital Territory', + sc190: { minPoints: 90, notes: 'ACT Critical Skills List. Must have an ACT-based job offer in most cases.' }, + sc491: { minPoints: null, notes: 'ACT does not participate in SC 491 (not a regional state).' }, + website: 'https://www.act.gov.au/migration', + }, + { + state: 'NT', fullName: 'Northern Territory', + sc190: { minPoints: 65, notes: 'NT occupation list. Lower points threshold. Strong demand for trades, healthcare, agriculture.' }, + sc491: { minPoints: 60, notes: 'All of NT is classified as regional.' }, + website: 'https://migration.nt.gov.au/', + }, +]; + +// ── ENGLISH REQUIREMENTS ────────────────────────────────────────────────────── +export const ENGLISH_REQUIREMENTS = [ + { + visa: '189 / 190 / 491', + name: 'Skilled Migration', + level: 'Competent English (minimum)', + ielts: 'L6 / R6 / W6 / S6', + pte: 'L50 / R50 / W50 / S50', + toefl: 'L12 / R13 / W21 / S18', + oet: 'B in all components', + notes: 'Proficient (IELTS 7) earns +10 pts; Superior (IELTS 8) earns +20 pts', + }, + { + visa: '482 (TSS)', + name: 'Temporary Skill Shortage', + level: 'Competent English', + ielts: 'L4.5 / R4.5 / W4.5 / S4.5 (avg 5.0)', + pte: '36 in each component (avg 42)', + toefl: 'L3 / R3 / W14 / S12 (total 24)', + oet: 'B in each component', + notes: 'Some exemptions for certain nationalities and assessment pathways', + }, + { + visa: '186 (ENS)', + name: 'Employer Nomination', + level: 'Competent English', + ielts: 'L6 / R6 / W6 / S6', + pte: 'L50 / R50 / W50 / S50', + toefl: 'L12 / R13 / W21 / S18', + oet: 'B in all components', + notes: 'Direct entry stream', + }, + { + visa: '500', + name: 'Student Visa', + level: 'Provider-dependent', + ielts: 'Typically 5.5–6.5 overall', + pte: 'Typically 42–58', + toefl: 'Typically 46–79', + oet: 'Typically C+ in all', + notes: 'Each institution sets its own minimum; confirm with your provider', + }, + { + visa: '485', + name: 'Graduate Temporary', + level: 'Competent English', + ielts: 'L6 / R6 / W6 / S6', + pte: 'L50 / R50 / W50 / S50', + toefl: 'L12 / R13 / W21 / S18', + oet: 'B in all components', + notes: 'Must have met English requirement for the SC 500 student visa', + }, + { + visa: '820 / 801 / 309 / 100', + name: 'Partner Visa', + level: 'No minimum required', + ielts: 'N/A', + pte: 'N/A', + toefl: 'N/A', + oet: 'N/A', + notes: 'English is not mandatory for partner visas; affects some citizenship pathways', + }, +]; + +// ── STUDENT FUND CALCULATOR ─────────────────────────────────────────────────── +export const STUDENT_FUND = { + livingCostPerYear: 29710, // AUD (2025 DHA figure) + partnerPerYear: 10394, + childPerYear: 4449, + note: 'Based on DHA 2024-25 financial capacity requirements. Does not include tuition fees, travel, or health cover.', +}; + +// ── OCCUPATIONS (MLTSSL + key STSOL) ───────────────────────────────────────── +export const OCCUPATIONS = [ + // ICT + { anzsco: '261111', title: 'ICT Business Analyst', list: 'MLTSSL', authority: 'ACS', visas: ['189','190','491','482','186'] }, + { anzsco: '261112', title: 'Systems Analyst', list: 'MLTSSL', authority: 'ACS', visas: ['189','190','491','482','186'] }, + { anzsco: '261311', title: 'Analyst Programmer', list: 'MLTSSL', authority: 'ACS', visas: ['189','190','491','482','186'] }, + { anzsco: '261312', title: 'Developer Programmer', list: 'MLTSSL', authority: 'ACS', visas: ['189','190','491','482','186'] }, + { anzsco: '261313', title: 'Software Engineer', list: 'MLTSSL', authority: 'ACS', visas: ['189','190','491','482','186'] }, + { anzsco: '261314', title: 'Software Tester', list: 'MLTSSL', authority: 'ACS', visas: ['189','190','491','482','186'] }, + { anzsco: '261399', title: 'Software/Apps Programmers NEC', list: 'MLTSSL', authority: 'ACS', visas: ['189','190','491','482','186'] }, + { anzsco: '262111', title: 'Database Administrator', list: 'MLTSSL', authority: 'ACS', visas: ['189','190','491','482','186'] }, + { anzsco: '262112', title: 'ICT Security Specialist', list: 'MLTSSL', authority: 'ACS', visas: ['189','190','491','482','186'] }, + { anzsco: '263111', title: 'Computer Network Engineer', list: 'MLTSSL', authority: 'ACS', visas: ['189','190','491','482','186'] }, + { anzsco: '263112', title: 'Network Administrator', list: 'MLTSSL', authority: 'ACS', visas: ['189','190','491','482','186'] }, + { anzsco: '263211', title: 'ICT Quality Assurance Engineer', list: 'MLTSSL', authority: 'ACS', visas: ['189','190','491','482','186'] }, + { anzsco: '263311', title: 'Telecommunications Engineer', list: 'MLTSSL', authority: 'ANZSCO', visas: ['189','190','491','482','186'] }, + // Nursing / Healthcare + { anzsco: '254111', title: 'Occupational Therapist', list: 'MLTSSL', authority: 'AOTC', visas: ['189','190','491','482','186'] }, + { anzsco: '252411', title: 'Physiotherapist', list: 'MLTSSL', authority: 'APC', visas: ['189','190','491','482','186'] }, + { anzsco: '252511', title: 'Podiatrist', list: 'MLTSSL', authority: 'PODAC', visas: ['189','190','491','482','186'] }, + { anzsco: '252711', title: 'Radiographer', list: 'MLTSSL', authority: 'AIR', visas: ['189','190','491','482','186'] }, + { anzsco: '252211', title: 'Medical Laboratory Scientist', list: 'MLTSSL', authority: 'AIMS', visas: ['189','190','491','482','186'] }, + { anzsco: '272311', title: 'Registered Nurse (Medical)', list: 'MLTSSL', authority: 'ANMAC', visas: ['189','190','491','482','186'] }, + { anzsco: '272312', title: 'Registered Nurse (Surgical)', list: 'MLTSSL', authority: 'ANMAC', visas: ['189','190','491','482','186'] }, + { anzsco: '272317', title: 'Registered Nurse (Mental Health)', list: 'MLTSSL', authority: 'ANMAC', visas: ['189','190','491','482','186'] }, + { anzsco: '272399', title: 'Registered Nurses NEC', list: 'MLTSSL', authority: 'ANMAC', visas: ['189','190','491','482','186'] }, + { anzsco: '251211', title: 'Medical Practitioner (General)', list: 'MLTSSL', authority: 'AMC', visas: ['189','190','491','482','186'] }, + { anzsco: '253111', title: 'General Practitioner', list: 'MLTSSL', authority: 'AMC', visas: ['189','190','491','482','186'] }, + { anzsco: '253321', title: 'Psychiatrist', list: 'MLTSSL', authority: 'AMC', visas: ['189','190','491','482','186'] }, + { anzsco: '253999', title: 'Medical Specialists NEC', list: 'MLTSSL', authority: 'AMC', visas: ['189','190','491','482','186'] }, + // Engineering + { anzsco: '233111', title: 'Chemical Engineer', list: 'MLTSSL', authority: 'Engineers Australia', visas: ['189','190','491','482','186'] }, + { anzsco: '233211', title: 'Civil Engineer', list: 'MLTSSL', authority: 'Engineers Australia', visas: ['189','190','491','482','186'] }, + { anzsco: '233214', title: 'Structural Engineer', list: 'MLTSSL', authority: 'Engineers Australia', visas: ['189','190','491','482','186'] }, + { anzsco: '233512', title: 'Mechanical Engineer', list: 'MLTSSL', authority: 'Engineers Australia', visas: ['189','190','491','482','186'] }, + { anzsco: '233611', title: 'Mining Engineer', list: 'MLTSSL', authority: 'Engineers Australia', visas: ['189','190','491','482','186'] }, + { anzsco: '233999', title: 'Engineering Professionals NEC', list: 'MLTSSL', authority: 'Engineers Australia', visas: ['189','190','491','482','186'] }, + { anzsco: '232111', title: 'Architect', list: 'MLTSSL', authority: 'AACA', visas: ['189','190','491','482','186'] }, + { anzsco: '232212', title: 'Landscape Architect', list: 'MLTSSL', authority: 'AILA', visas: ['189','190','491','482','186'] }, + // Accounting / Finance + { anzsco: '221111', title: 'Accountant (General)', list: 'MLTSSL', authority: 'CPAA/CAANZ/IPA', visas: ['189','190','491','482','186'] }, + { anzsco: '221112', title: 'Management Accountant', list: 'MLTSSL', authority: 'CPAA/CAANZ/IPA', visas: ['189','190','491','482','186'] }, + { anzsco: '221113', title: 'Taxation Accountant', list: 'MLTSSL', authority: 'CPAA/CAANZ/IPA', visas: ['189','190','491','482','186'] }, + { anzsco: '221114', title: 'External Auditor', list: 'MLTSSL', authority: 'CPAA/CAANZ', visas: ['189','190','491','482','186'] }, + { anzsco: '222111', title: 'Financial Investment Adviser', list: 'STSOL', authority: 'FINSIA', visas: ['190','491','482'] }, + // Education + { anzsco: '241111', title: 'Early Childhood Teacher', list: 'MLTSSL', authority: 'ACECQA', visas: ['189','190','491','482','186'] }, + { anzsco: '241411', title: 'Special Education Teacher', list: 'MLTSSL', authority: 'AITSL', visas: ['189','190','491','482','186'] }, + { anzsco: '241213', title: 'Secondary School Teacher', list: 'STSOL', authority: 'AITSL', visas: ['190','491','482'] }, + // Trades + { anzsco: '323211', title: 'Fitter (general)', list: 'MLTSSL', authority: 'TRA', visas: ['189','190','491','482','186'] }, + { anzsco: '342111', title: 'Electrician (general)', list: 'MLTSSL', authority: 'TRA', visas: ['189','190','491','482','186'] }, + { anzsco: '334111', title: 'Plumber (general)', list: 'MLTSSL', authority: 'TRA', visas: ['189','190','491','482','186'] }, + { anzsco: '331112', title: 'Bricklayer', list: 'MLTSSL', authority: 'TRA', visas: ['189','190','491','482','186'] }, + { anzsco: '331211', title: 'Carpenter and Joiner', list: 'MLTSSL', authority: 'TRA', visas: ['189','190','491','482','186'] }, + { anzsco: '333111', title: 'Painting Tradesperson', list: 'MLTSSL', authority: 'TRA', visas: ['189','190','491','482','186'] }, + { anzsco: '361111', title: 'Veterinarian', list: 'MLTSSL', authority: 'AVA', visas: ['189','190','491','482','186'] }, + { anzsco: '251411', title: 'Optometrist', list: 'MLTSSL', authority: 'OCANZ', visas: ['189','190','491','482','186'] }, + { anzsco: '251611', title: 'Dental Therapist', list: 'MLTSSL', authority: 'ADC', visas: ['189','190','491','482','186'] }, + { anzsco: '252311', title: 'Dental Specialist', list: 'MLTSSL', authority: 'ADC', visas: ['189','190','491','482','186'] }, + { anzsco: '271311', title: 'Social Worker', list: 'MLTSSL', authority: 'AASW', visas: ['189','190','491','482','186'] }, + { anzsco: '272111', title: 'Counsellor', list: 'STSOL', authority: 'VETASSESS',visas: ['190','491','482'] }, +]; + +export const TIMELINE_STAGES = [ + { key: 'skill_assessment', label: 'Skills Assessment', icon: '📋', desc: 'Skills assessed by relevant authority (TRA, ACS, ANMAC, Engineers Australia, etc.)' }, + { key: 'english_test', label: 'English Test', icon: '🗣️', desc: 'IELTS, PTE, TOEFL, or OET completed' }, + { key: 'eoi_lodged', label: 'EOI Submitted', icon: '📝', desc: 'Expression of Interest submitted in SkillSelect' }, + { key: 'invitation', label: 'Invitation Received', icon: '✉️', desc: 'Invitation to Apply (ITA) received from DHA or state authority' }, + { key: 'visa_lodged', label: 'Visa Application Lodged', icon: '🚀', desc: 'Visa application submitted to the Department of Home Affairs' }, + { key: 'decision', label: 'Decision Made', icon: '⚖️', desc: 'DHA has made a decision on the application' }, + { key: 'visa_granted', label: 'Visa Granted! 🎉', icon: '🎉', desc: 'Visa approved — welcome to Australia!' }, +]; diff --git a/src/visa-tracker.js b/src/visa-tracker.js new file mode 100644 index 0000000..243fb8e --- /dev/null +++ b/src/visa-tracker.js @@ -0,0 +1,617 @@ +import { POINTS_TABLE, calcPoints, VAC_FEES, PROCESSING_TIMES, STATE_CRITERIA, ENGLISH_REQUIREMENTS, STUDENT_FUND, OCCUPATIONS, TIMELINE_STAGES } from './visa-data.js'; +import { esc } from './templates.js'; + +const BASE_CSS = ` + .vt-card{background:#fff;border-radius:12px;padding:28px;box-shadow:0 2px 8px rgba(0,0,0,.05);margin-bottom:24px} + .vt-card h3{font-size:1.1rem;font-weight:700;color:#1a2744;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid #e8f0fe} + .vt-section-title{font-size:1.6rem;font-weight:800;color:#1a2744;margin-bottom:6px} + .vt-section-sub{color:#64748b;margin-bottom:28px} + .pts-total{background:linear-gradient(135deg,#1a2744,#1a5bb8);color:#fff;border-radius:16px;padding:28px;text-align:center;margin-bottom:24px} + .pts-num{font-size:4rem;font-weight:900;line-height:1} + .pts-label{font-size:1rem;opacity:.85;margin-top:6px} + .pts-bar{height:8px;background:#e8f0fe;border-radius:4px;overflow:hidden;margin:6px 0 4px} + .pts-fill{height:100%;background:linear-gradient(90deg,#1a5bb8,#38bdf8);border-radius:4px;transition:.4s} + .breakdown-row{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid #f1f5f9;font-size:.9rem} + .breakdown-row:last-child{border-bottom:none} + .breakdown-pts{font-weight:700;color:#1a5bb8} + + .timeline-wrap{position:relative;padding-left:44px} + .timeline-wrap::before{content:'';position:absolute;left:18px;top:0;bottom:0;width:2px;background:#e8f0fe} + .tl-stage{position:relative;margin-bottom:24px} + .tl-dot{position:absolute;left:-44px;top:2px;width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:1rem;z-index:1} + .tl-dot-done{background:#1a5bb8;color:#fff} + .tl-dot-active{background:#f59e0b;color:#fff;box-shadow:0 0 0 4px rgba(245,158,11,.2)} + .tl-dot-pending{background:#f1f5f9;color:#94a3b8;border:2px solid #d1d9e8} + .tl-content{background:#fff;border-radius:10px;padding:16px;border:1px solid #e8f0fe} + .tl-content.done{border-color:#dbeafe;background:#f0f6ff} + .tl-content.active{border-color:#f59e0b;background:#fffbeb} + .tl-label{font-weight:700;color:#1a2744;font-size:.95rem} + .tl-date{font-size:.85rem;color:#1a5bb8;font-weight:600;margin-top:2px} + .tl-desc{font-size:.85rem;color:#64748b;margin-top:4px} + + .doc-row{display:flex;align-items:center;justify-content:space-between;padding:12px 0;border-bottom:1px solid #f1f5f9;gap:12px;flex-wrap:wrap} + .doc-row:last-child{border-bottom:none} + .doc-label{font-weight:600;font-size:.9rem;color:#1a2744} + .doc-expiry{font-size:.85rem;color:#64748b} + .expiry-ok{color:#16a34a;font-weight:600} + .expiry-warn{color:#d97706;font-weight:600} + .expiry-danger{color:#dc2626;font-weight:600} + .expiry-expired{color:#dc2626;font-weight:700} + + .fee-total{font-size:1.6rem;font-weight:800;color:#1a5bb8} + .occ-row{padding:12px 16px;border-radius:8px;background:#fff;border:1px solid #e8f0fe;margin-bottom:8px} + .occ-title{font-weight:700;font-size:.95rem;color:#1a2744} + .occ-meta{font-size:.8rem;color:#64748b;margin-top:3px;display:flex;gap:10px;flex-wrap:wrap} + .list-mltssl{background:#dcfce7;color:#166534;padding:2px 8px;border-radius:10px;font-weight:600;font-size:.75rem} + .list-stsol{background:#dbeafe;color:#1d4ed8;padding:2px 8px;border-radius:10px;font-weight:600;font-size:.75rem} + .state-card{background:#fff;border-radius:10px;border:1px solid #e8f0fe;padding:18px;margin-bottom:12px} + .state-card h4{font-weight:700;color:#1a2744;margin-bottom:8px} + .proc-row{display:grid;grid-template-columns:1fr 80px 80px 80px 80px;gap:8px;padding:10px 0;border-bottom:1px solid #f1f5f9;font-size:.9rem;align-items:center} + .proc-row:last-child{border-bottom:none} + .proc-header{font-weight:700;color:#1a2744;font-size:.8rem;text-transform:uppercase;letter-spacing:.5px} + .p-val{text-align:center;font-weight:600;color:#1a5bb8} + .eng-table{width:100%;border-collapse:collapse;font-size:.88rem} + .eng-table th{background:#f0f6ff;color:#1a2744;font-weight:700;padding:10px 12px;text-align:left} + .eng-table td{padding:10px 12px;border-bottom:1px solid #f1f5f9;vertical-align:top} + .eng-table tr:last-child td{border-bottom:none} + .fund-result{background:#f0fdf4;border:1px solid #bbf7d0;border-radius:10px;padding:20px;margin-top:16px} + .fund-total{font-size:1.8rem;font-weight:800;color:#16a34a} + @media(max-width:600px){.proc-row{grid-template-columns:1fr 60px 60px 60px;}.proc-row>*:last-child{display:none}} +`; + +function dashWrap(user, activeTab, content) { + const initials = user.full_name.split(' ').map(w => w[0]).join('').toUpperCase().slice(0,2); + const tabs = [ + { key: 'overview', href: '/dashboard', label: '📊 Overview' }, + { key: 'points', href: '/dashboard/points', label: '🔢 Points Calculator' }, + { key: 'timeline', href: '/dashboard/timeline', label: '📅 Visa Timeline' }, + { key: 'documents', href: '/dashboard/documents', label: '📄 Document Expiry' }, + { key: 'fees', href: '/dashboard/fees', label: '💰 VAC Fee Calculator' }, + { key: 'occupations', href: '/dashboard/occupations', label: '🔍 Occupation Search' }, + { key: 'state-criteria', href: '/dashboard/state-criteria', label: '🗺️ State Criteria' }, + { key: 'processing-times', href: '/dashboard/processing-times', label: '⏱️ Processing Times' }, + { key: 'english', href: '/dashboard/english', label: '🗣️ English Requirements' }, + { key: 'student-fund', href: '/dashboard/student-fund', label: '🎓 Student Fund Calc' }, + { key: 'courses', href: '/courses', label: '🎓 Search Courses' }, + { key: 'contact', href: '/contact', label: '📝 Book Consultation' }, + { key: 'profile', href: '/dashboard/profile', label: '👤 My Profile' }, + ]; + + return ` + +
+
+
+
+
+
${esc(initials)}
+
${esc(user.full_name)}
+
${esc(user.email)}
+
+ +
+
${content}
+
+
+
+ `; +} + +// ── POINTS CALCULATOR ─────────────────────────────────────────────────────── +export function pointsPage(user, profile, flash) { + const result = profile ? calcPoints(profile) : null; + const total = result ? result.total : 0; + const pct = Math.min(100, (total / 100) * 100); + const statusColor = total >= 90 ? '#16a34a' : total >= 65 ? '#d97706' : '#dc2626'; + const statusLabel = total >= 90 ? '✅ Competitive' : total >= 65 ? '⚠️ Getting there' : '🔴 Below typical threshold'; + + const selFor = (name, options, current) => + ``; + + const fg = (label, name, select) => ` +
+ + ${select} +
`; + + const p = profile || {}; + + const body = ` +
🔢 Points Calculator
+

Australian skilled migration points test (SC 189 / 190 / 491)

+ + ${result ? ` +
+
${total}
+
Total Points
+
${statusLabel}
+
Typical invitation threshold: 65–90+ (varies by occupation & round)
+
+
+

Points Breakdown

+ ${result.breakdown.map(b => ` +
+ ${esc(b.label)} + +${b.points} +
`).join('')} +
+ Total+${total} +
+
+
06590100
+
+
+
` : ''} + +
+

${result ? 'Update Your Points Profile' : 'Enter Your Details'}

+ ${flash ? `
${esc(flash)}
` : ''} +
+
+ ${fg('Age', 'age', ``)} + ${fg('English Level', 'english_level', selFor('english_level', POINTS_TABLE.english, p.english_level||''))} + ${fg('Highest Education', 'education_level', selFor('education_level', POINTS_TABLE.education, p.education_level||''))} + ${fg('Australian Study', 'aus_study_years', selFor('aus_study_years', POINTS_TABLE.ausStudy, p.aus_study_years||''))} + ${fg('Overseas Work Experience', 'overseas_work_years', selFor('overseas_work_years', POINTS_TABLE.overseasWork, p.overseas_work_years||''))} + ${fg('Australian Work Experience', 'aus_work_years', selFor('aus_work_years', POINTS_TABLE.ausWork, p.aus_work_years||''))} + ${fg('State Nomination', 'state_nomination', selFor('state_nomination', + [{label:'None',value:''},{label:'State nomination — SC 190 (+5)',value:'190'},{label:'State nomination — SC 491 (+15)',value:'491'}], + p.state_nomination||''))} + ${fg('Partner Status', 'partner_skills', selFor('partner_skills', + [{label:'Partner has skills + competent English (+10)',value:'1'},{label:'Single / partner is AUS citizen or PR (+10)',value:'2'},{label:'Partner (no points)',value:'0'}], + p.partner_skills||'0'))} +
+
+ + + +
+ +
+
`; + + return dashWrap(user, 'points', body); +} + +// ── VISA TIMELINE ──────────────────────────────────────────────────────────── +export function timelinePage(user, stages, flash) { + const stageMap = {}; + (stages || []).forEach(s => { stageMap[s.stage] = s; }); + const completedCount = TIMELINE_STAGES.filter(s => stageMap[s.key]?.milestone_date).length; + const activeIdx = TIMELINE_STAGES.findIndex(s => !stageMap[s.key]?.milestone_date); + + const body = ` +
📅 Visa Case Timeline
+

Track your migration journey from skills assessment to visa grant

+ ${flash ? `
${esc(flash)}
` : ''} + +
+
+
+
Progress
+
${completedCount} / ${TIMELINE_STAGES.length} stages
+
+
+
+
+
${Math.round((completedCount/TIMELINE_STAGES.length)*100)}%
+
+
+ +
+
+ ${TIMELINE_STAGES.map((stage, i) => { + const saved = stageMap[stage.key]; + const isDone = !!saved?.milestone_date; + const isActive = !isDone && i === activeIdx; + const dotClass = isDone ? 'tl-dot-done' : isActive ? 'tl-dot-active' : 'tl-dot-pending'; + const cardClass = isDone ? 'done' : isActive ? 'active' : ''; + return ` +
+
${isDone ? '✓' : stage.icon}
+
+
+
+
${stage.label}
+
${stage.desc}
+ ${saved?.milestone_date ? `
📅 ${new Date(saved.milestone_date).toLocaleDateString('en-AU',{day:'numeric',month:'long',year:'numeric'})}
` : ''} +
+
+ + ${saved?.notes ? `${esc(saved.notes)}` : ''} +
+
+
+ +
+
+
`; + }).join('')} +
+ +
`; + + return dashWrap(user, 'timeline', body); +} + +// ── DOCUMENT EXPIRY ────────────────────────────────────────────────────────── +export function documentsPage(user, docs, flash) { + const today = new Date(); + const sorted = [...(docs||[])].sort((a,b) => new Date(a.expiry_date) - new Date(b.expiry_date)); + + function expiryStatus(dateStr) { + const d = new Date(dateStr); + const days = Math.round((d - today) / 86400000); + if (days < 0) return { cls: 'expiry-expired', label: `Expired ${Math.abs(days)}d ago` }; + if (days <= 30) return { cls: 'expiry-danger', label: `Expires in ${days}d` }; + if (days <= 180) return { cls: 'expiry-warn', label: `Expires in ${days}d` }; + return { cls: 'expiry-ok', label: `${days}d remaining` }; + } + + const presets = ['Passport','Current Visa','English Test (IELTS/PTE)','Skills Assessment','EOI Expiry','Health Insurance (OSHC)','Police Clearance','Medical Certificate','Employer Sponsorship','Other Document']; + + const body = ` +
📄 Document Expiry Tracker
+

Monitor passport, visa, English test, and other critical document expiry dates

+ ${flash ? `
${esc(flash)}
` : ''} + + ${sorted.length > 0 ? ` +
+

Your Documents

+ ${sorted.map(d => { + const { cls, label } = expiryStatus(d.expiry_date); + return ` +
+
+
${esc(d.doc_label)}
+
${new Date(d.expiry_date).toLocaleDateString('en-AU',{day:'numeric',month:'long',year:'numeric'})}
+
+
+ ${label} +
+ + +
+
+
`}).join('')} +
` : ''} + +
+

Add Document

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
`; + + return dashWrap(user, 'documents', body); +} + +// ── VAC FEE CALCULATOR ──────────────────────────────────────────────────────── +export function feesPage(user) { + const body = ` +
💰 VAC Fee Calculator
+

Estimate your Visa Application Charge (as at 2025–26)

+ +
+

Select Visa & Applicants

+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
Primary
+
Secondary
+
Children
+
Total VAC
+
+
+
Figures in AUD. VAC fees are subject to change; verify at homeaffairs.gov.au before lodging.
+
+
+ + `; + + return dashWrap(user, 'fees', body); +} + +// ── OCCUPATION SEARCH ───────────────────────────────────────────────────────── +export function occupationsPage(user, query) { + const q = (query || '').toLowerCase().trim(); + const results = q ? OCCUPATIONS.filter(o => + o.title.toLowerCase().includes(q) || + o.anzsco.includes(q) || + o.authority.toLowerCase().includes(q) + ) : OCCUPATIONS.slice(0, 30); + + const body = ` +
🔍 Occupation Search
+

MLTSSL and STSOL occupations with visa eligibility and assessing authority

+ +
+
+ + +
+
+ + ${results.length === 0 ? `
No occupations found for "${esc(q)}"
` : ` +
${q ? `${results.length} result${results.length!==1?'s':''} for "${esc(q)}"` : `Showing first ${results.length} occupations — search to filter`}
+ ${results.map(o => ` +
+
+
+
${esc(o.title)}
+
+ ANZSCO: ${esc(o.anzsco)} + ${esc(o.list)} + Authority: ${esc(o.authority)} +
+
+
+ ${o.visas.map(v => `SC ${v}`).join('')} +
+
+
`).join('')}`} + +
+ 💡 MLTSSL occupations can be nominated for SC 189 (independent), 190, 491, 482, and 186. + STSOL occupations require state/employer sponsorship (190, 491, 482). Always verify the current list at + immi.homeaffairs.gov.au. +
`; + + return dashWrap(user, 'occupations', body); +} + +// ── STATE NOMINATION CRITERIA ───────────────────────────────────────────────── +export function stateCriteriaPage(user) { + const body = ` +
🗺️ State Nomination Criteria
+

Minimum points and key requirements for SC 190 and SC 491 by state/territory (2025–26)

+ +
+ ${STATE_CRITERIA.map(s => ` +
+

${s.fullName} (${s.state})

+
+
SC 190 — Skilled Nominated
+
+ ${s.sc190.minPoints ? s.sc190.minPoints + ' pts min' : 'N/A'} +
+
${esc(s.sc190.notes)}
+
+
+
SC 491 — Skilled Regional
+
+ ${s.sc491.minPoints != null ? s.sc491.minPoints + ' pts min' : 'N/A'} +
+
${esc(s.sc491.notes)}
+
+ Official site → +
`).join('')} +
+
+ ⚠️ State criteria change frequently and may close mid-year. Always verify directly with the state migration authority before applying. +
+ `; + + return dashWrap(user, 'state-criteria', body); +} + +// ── PROCESSING TIMES ────────────────────────────────────────────────────────── +export function processingTimesPage(user) { + const body = ` +
⏱️ Visa Processing Times
+

Estimated processing times in months (percentile-based, 2025–26)

+ +
+
+ VisaP25P50P75P90 +
+ ${PROCESSING_TIMES.map(v => ` +
+
+
SC ${v.subclass} — ${v.name}
+ ${v.note ? `
${esc(v.note)}
` : ''} +
+ ${v.p25}m + ${v.p50}m + ${v.p75}m + ${v.p90}m +
`).join('')} +
+
+ Reading this table: P50 means 50% of applications are decided within that many months. + P25 = fastest quarter, P90 = slowest 10%. Times are estimates — check + DHA's live dashboard for current figures. +
`; + + return dashWrap(user, 'processing-times', body); +} + +// ── ENGLISH REQUIREMENTS ────────────────────────────────────────────────────── +export function englishPage(user) { + const body = ` +
🗣️ English Requirements
+

Required scores by visa subclass across IELTS, PTE, TOEFL, and OET

+ +
+ + + + + + + + + + + + + ${ENGLISH_REQUIREMENTS.map(e => ` + + + + + + + + + + `).join('')} + +
VisaLevel RequiredIELTSPTE AcademicTOEFL iBTOET
SC ${e.visa}
${esc(e.name)}
${esc(e.level)}${esc(e.ielts)}${esc(e.pte)}${esc(e.toefl)}${esc(e.oet)}
${esc(e.notes)}
+
+
+ ⚠️ Scores shown are component-by-component minimums. Some subclasses require all four skills to meet the threshold individually. Verify on the DHA website before testing. +
+ `; + + return dashWrap(user, 'english', body); +} + +// ── STUDENT FUND CALCULATOR ──────────────────────────────────────────────────── +export function studentFundPage(user) { + const body = ` +
🎓 Student Fund Calculator
+

Minimum financial capacity required for an Australian student visa (SC 500)

+ +
+

Calculate Required Funds

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
Tuition (total)
+
Living Costs
+
Travel (est.)
+
Partner
+
Children
+
+
+
Minimum Funds Required
+
+
+
+
${esc(STUDENT_FUND.note)}
+
+ +
+

2025–26 Living Cost Figures (DHA)

+
+
Principal applicant: $${STUDENT_FUND.livingCostPerYear.toLocaleString()}/year
+
Partner: $${STUDENT_FUND.partnerPerYear.toLocaleString()}/year
+
Each child: $${STUDENT_FUND.childPerYear.toLocaleString()}/year
+
Travel (estimate): ~$4,000 return
+
+
+ + `; + + return dashWrap(user, 'student-fund', body); +}