Compare commits
No commits in common. "ae46f4ad0aa5480cd302827f1cde2bbb3190b16c" and "5838f782e7806e986e369d2ca7da141616fa5158" have entirely different histories.
ae46f4ad0a
...
5838f782e7
@ -1 +1 @@
|
|||||||
2aa2de355546f6401f80a07e00066bd83f37fdc6-803b2c2ecad09a0fbca070296808a53489de891a-e9eccd451b778829eb2f2c9752c670b707e1268b
|
fcb1ff42e88c57ae313a74da813f6a3cdb19904f-803b2c2ecad09a0fbca070296808a53489de891a-e9eccd451b778829eb2f2c9752c670b707e1268b
|
||||||
|
@ -29,7 +29,6 @@ steps:
|
|||||||
|
|
||||||
|
|
||||||
- name: staging-deploy
|
- name: staging-deploy
|
||||||
depends_on: [security-scan]
|
|
||||||
image: google/cloud-sdk:latest
|
image: google/cloud-sdk:latest
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- bash
|
- bash
|
||||||
@ -113,19 +112,6 @@ steps:
|
|||||||
export SUPPORT_SENDGRID_API_KEY; \
|
export SUPPORT_SENDGRID_API_KEY; \
|
||||||
GOOGLE_MAPS_API_KEY=$(gcloud secrets versions access latest --secret=GOOGLE_MAPS_API_KEY_$ENV --project=$PROJECT); \
|
GOOGLE_MAPS_API_KEY=$(gcloud secrets versions access latest --secret=GOOGLE_MAPS_API_KEY_$ENV --project=$PROJECT); \
|
||||||
export GOOGLE_MAPS_API_KEY; \
|
export GOOGLE_MAPS_API_KEY; \
|
||||||
SERVER1_PORT=$(gcloud secrets versions access latest --secret=SERVER1_PORT_$ENV --project=$PROJECT); \
|
|
||||||
export SERVER1_PORT
|
|
||||||
SERVER2_PORT=$(gcloud secrets versions access latest --secret=SERVER2_PORT_$ENV --project=$PROJECT); \
|
|
||||||
export SERVER2_PORT
|
|
||||||
SERVER3_PORT=$(gcloud secrets versions access latest --secret=SERVER3_PORT_$ENV --project=$PROJECT); \
|
|
||||||
export SERVER3_PORT
|
|
||||||
ENV_NAME=$(gcloud secrets versions access latest --secret=ENV_NAME_$ENV --project=$PROJECT); \
|
|
||||||
export ENV_NAME
|
|
||||||
CORS_ALLOWED_ORIGINS=$(gcloud secrets versions access latest --secret=CORS_ALLOWED_ORIGINS_$ENV --project=$PROJECT); \
|
|
||||||
export CORS_ALLOWED_ORIGINS
|
|
||||||
APTIVA_API_BASE=$(gcloud secrets versions access latest --secret=APTIVA_API_BASE_$ENV --project=$PROJECT); \
|
|
||||||
export APTIVA_API_BASE
|
|
||||||
|
|
||||||
export FROM_SECRETS_MANAGER=true; \
|
export FROM_SECRETS_MANAGER=true; \
|
||||||
\
|
\
|
||||||
# ── DEK sync: copy dev wrapped DEK into staging volume path ── \
|
# ── DEK sync: copy dev wrapped DEK into staging volume path ── \
|
||||||
@ -143,9 +129,9 @@ steps:
|
|||||||
fi; \
|
fi; \
|
||||||
\
|
\
|
||||||
cd /home/jcoakley/aptiva-staging-app; \
|
cd /home/jcoakley/aptiva-staging-app; \
|
||||||
sudo --preserve-env=IMG_TAG,FROM_SECRETS_MANAGER,JWT_SECRET,OPENAI_API_KEY,ONET_USERNAME,ONET_PASSWORD,STRIPE_SECRET_KEY,STRIPE_PUBLISHABLE_KEY,STRIPE_WH_SECRET,STRIPE_PRICE_PREMIUM_MONTH,STRIPE_PRICE_PREMIUM_YEAR,STRIPE_PRICE_PRO_MONTH,STRIPE_PRICE_PRO_YEAR,DB_NAME,DB_HOST,DB_PORT,DB_USER,DB_PASSWORD,DB_SSL_CA,DB_SSL_CERT,DB_SSL_KEY,TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_MESSAGING_SERVICE_SID,KMS_KEY_NAME,DEK_PATH,SUPPORT_SENDGRID_API_KEY,GOOGLE_MAPS_API_KEY,SERVER1_PORT,SERVER2_PORT,SERVER3_PORT,CORS_ALLOWED_ORIGINS,ENV_NAME,APTIVA_API_BASE \
|
sudo --preserve-env=IMG_TAG,FROM_SECRETS_MANAGER,JWT_SECRET,OPENAI_API_KEY,ONET_USERNAME,ONET_PASSWORD,STRIPE_SECRET_KEY,STRIPE_PUBLISHABLE_KEY,STRIPE_WH_SECRET,STRIPE_PRICE_PREMIUM_MONTH,STRIPE_PRICE_PREMIUM_YEAR,STRIPE_PRICE_PRO_MONTH,STRIPE_PRICE_PRO_YEAR,DB_NAME,DB_HOST,DB_PORT,DB_USER,DB_PASSWORD,DB_SSL_CA,DB_SSL_CERT,DB_SSL_KEY,TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_MESSAGING_SERVICE_SID,KMS_KEY_NAME,DEK_PATH,SUPPORT_SENDGRID_API_KEY,GOOGLE_MAPS_API_KEY \
|
||||||
docker compose pull; \
|
docker compose pull; \
|
||||||
sudo --preserve-env=IMG_TAG,FROM_SECRETS_MANAGER,JWT_SECRET,OPENAI_API_KEY,ONET_USERNAME,ONET_PASSWORD,STRIPE_SECRET_KEY,STRIPE_PUBLISHABLE_KEY,STRIPE_WH_SECRET,STRIPE_PRICE_PREMIUM_MONTH,STRIPE_PRICE_PREMIUM_YEAR,STRIPE_PRICE_PRO_MONTH,STRIPE_PRICE_PRO_YEAR,DB_NAME,DB_HOST,DB_PORT,DB_USER,DB_PASSWORD,DB_SSL_CA,DB_SSL_CERT,DB_SSL_KEY,TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_MESSAGING_SERVICE_SID,KMS_KEY_NAME,DEK_PATH,SUPPORT_SENDGRID_API_KEY,GOOGLE_MAPS_API_KEY,SERVER1_PORT,SERVER2_PORT,SERVER3_PORT,CORS_ALLOWED_ORIGINS,ENV_NAME,APTIVA_API_BASE \
|
sudo --preserve-env=IMG_TAG,FROM_SECRETS_MANAGER,JWT_SECRET,OPENAI_API_KEY,ONET_USERNAME,ONET_PASSWORD,STRIPE_SECRET_KEY,STRIPE_PUBLISHABLE_KEY,STRIPE_WH_SECRET,STRIPE_PRICE_PREMIUM_MONTH,STRIPE_PRICE_PREMIUM_YEAR,STRIPE_PRICE_PRO_MONTH,STRIPE_PRICE_PRO_YEAR,DB_NAME,DB_HOST,DB_PORT,DB_USER,DB_PASSWORD,DB_SSL_CA,DB_SSL_CERT,DB_SSL_KEY,TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_MESSAGING_SERVICE_SID,KMS_KEY_NAME,DEK_PATH,SUPPORT_SENDGRID_API_KEY,GOOGLE_MAPS_API_KEY \
|
||||||
docker compose up -d --force-recreate --remove-orphans; \
|
docker compose up -d --force-recreate --remove-orphans; \
|
||||||
echo "✅ Staging stack refreshed with tag $IMG_TAG"'
|
echo "✅ Staging stack refreshed with tag $IMG_TAG"'
|
||||||
|
|
||||||
@ -156,6 +142,3 @@ steps:
|
|||||||
when:
|
when:
|
||||||
event:
|
event:
|
||||||
- push
|
- push
|
||||||
branch:
|
|
||||||
- master
|
|
||||||
- dev-master
|
|
||||||
|
@ -17,7 +17,6 @@ import { v4 as uuidv4 } from 'uuid';
|
|||||||
import pkg from 'pdfjs-dist';
|
import pkg from 'pdfjs-dist';
|
||||||
import pool from './config/mysqlPool.js';
|
import pool from './config/mysqlPool.js';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { decrypt } from './shared/crypto/encryption.js'
|
|
||||||
|
|
||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
@ -3030,18 +3029,8 @@ app.get('/api/premium/college-profile/all', authenticatePremiumUser, async (req,
|
|||||||
WHERE cp.user_id = ?
|
WHERE cp.user_id = ?
|
||||||
ORDER BY cp.created_at DESC
|
ORDER BY cp.created_at DESC
|
||||||
`;
|
`;
|
||||||
const [rows] = await pool.query(sql, [req.id]);
|
const [rows] = await pool.query(sql,[req.id]);
|
||||||
const decrypted = rows.map(r => {
|
res.json({ collegeProfiles: rows });
|
||||||
const row = { ...r };
|
|
||||||
for (const k of ['career_title', 'selected_school', 'selected_program']) {
|
|
||||||
const v = row[k];
|
|
||||||
if (typeof v === 'string' && v.startsWith('gcm:')) {
|
|
||||||
try { row[k] = decrypt(v); } catch {} // best-effort
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return row;
|
|
||||||
});
|
|
||||||
res.json({ collegeProfiles: decrypted });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
|
@ -126,42 +126,41 @@ export default function CareerCoach({
|
|||||||
if (chatRef.current) chatRef.current.scrollTop = chatRef.current.scrollHeight;
|
if (chatRef.current) chatRef.current.scrollTop = chatRef.current.scrollHeight;
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
|
// chat history persistence
|
||||||
|
useEffect(() => {
|
||||||
|
const saved = localStorage.getItem('coachChat:'+careerProfileId);
|
||||||
|
if (saved) setMessages(JSON.parse(saved));
|
||||||
|
}, [careerProfileId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('coachChat:'+careerProfileId, JSON.stringify(messages.slice(-20)));
|
||||||
|
}, [messages, careerProfileId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (!careerProfileId) return;
|
if (!careerProfileId) return;
|
||||||
|
// try to reuse the newest coach thread; create one named after the scenario
|
||||||
// list threads for this profile
|
const r = await authFetch('/api/premium/coach/chat/threads');
|
||||||
const r = await authFetch(
|
|
||||||
`/api/premium/coach/chat/threads?careerProfileId=${encodeURIComponent(careerProfileId)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!(r.ok && (r.headers.get('content-type') || '').includes('application/json'))) {
|
|
||||||
setThreadId(null); // coach offline; no network errors on mount
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { threads = [] } = await r.json();
|
const { threads = [] } = await r.json();
|
||||||
const existing = threads.find(Boolean);
|
const existing = threads.find(Boolean);
|
||||||
if (!existing?.id) {
|
let id = existing?.id;
|
||||||
setThreadId(null); // no thread yet; lazy-create on first send
|
if (!id) {
|
||||||
return;
|
const r2 = await authFetch('/api/premium/coach/chat/threads', {
|
||||||
|
method:'POST',
|
||||||
|
headers:{ 'Content-Type':'application/json' },
|
||||||
|
body: JSON.stringify({ title: (scenarioRow?.scenario_title || scenarioRow?.career_name || 'Coach chat') })
|
||||||
|
});
|
||||||
|
({ id } = await r2.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = existing.id;
|
|
||||||
setThreadId(id);
|
setThreadId(id);
|
||||||
|
|
||||||
// preload history
|
// preload history
|
||||||
const r3 = await authFetch(
|
const r3 = await authFetch(`/api/premium/coach/chat/threads/${id}`);
|
||||||
`/api/premium/coach/chat/threads/${id}?careerProfileId=${encodeURIComponent(careerProfileId)}`
|
const { messages: msgs = [] } = await r3.json();
|
||||||
);
|
setMessages(msgs);
|
||||||
if (r3.ok && (r3.headers.get('content-type') || '').includes('application/json')) {
|
|
||||||
const { messages: msgs = [] } = await r3.json();
|
|
||||||
setMessages(msgs);
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
}, [careerProfileId]);
|
}, [careerProfileId]);
|
||||||
|
|
||||||
|
|
||||||
/* -------------- intro ---------------- */
|
/* -------------- intro ---------------- */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!scenarioRow) return;
|
if (!scenarioRow) return;
|
||||||
@ -235,18 +234,14 @@ I'm here to support you with personalized coaching. What would you like to focus
|
|||||||
async function callAi(updatedHistory, opts = {}) {
|
async function callAi(updatedHistory, opts = {}) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
if (!threadId) throw new Error('thread not ready');
|
|
||||||
const context = { userProfile, financialProfile, scenarioRow, collegeProfile };
|
const context = { userProfile, financialProfile, scenarioRow, collegeProfile };
|
||||||
const r = await authFetch(`/api/premium/coach/chat/threads/${threadId}/messages`, {
|
const r = await authFetch(`/api/premium/coach/chat/threads/${threadId}/messages`, {
|
||||||
method:'POST',
|
method:'POST',
|
||||||
headers:{ 'Content-Type':'application/json' },
|
headers:{ 'Content-Type':'application/json' },
|
||||||
body: JSON.stringify({ content: updatedHistory.at(-1)?.content || '', context })
|
body: JSON.stringify({ content: updatedHistory.at(-1)?.content || '', context })
|
||||||
});
|
});
|
||||||
let reply = 'Sorry, something went wrong.';
|
const data = await r.json();
|
||||||
if (r.ok && (r.headers.get('content-type')||'').includes('application/json')) {
|
const reply = (data?.reply || '').trim() || 'Sorry, something went wrong.';
|
||||||
const data = await r.json();
|
|
||||||
reply = (data?.reply || '').trim() || reply;
|
|
||||||
}
|
|
||||||
setMessages(prev => [...prev, { role:'assistant', content: reply }]);
|
setMessages(prev => [...prev, { role:'assistant', content: reply }]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
@ -84,10 +84,7 @@ export default function CareerProfileForm() {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(await res.text());
|
if (!res.ok) throw new Error(await res.text());
|
||||||
const data = await res.json(); // { career_profile_id: '...' }
|
nav(-1);
|
||||||
const activeId = data.career_profile_id || id; // handle edit vs new
|
|
||||||
localStorage.setItem('lastSelectedCareerProfileId', activeId);
|
|
||||||
nav(`/career-roadmap/${activeId}`, { replace: true }); // guarantees Roadmap selects this one
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
alert(err.message);
|
alert(err.message);
|
||||||
|
@ -395,7 +395,7 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) {
|
|||||||
const chat = useContext(ChatCtx) || {};
|
const chat = useContext(ChatCtx) || {};
|
||||||
const setChatSnapshot = chat?.setChatSnapshot;
|
const setChatSnapshot = chat?.setChatSnapshot;
|
||||||
|
|
||||||
const dataReady = !!scenarioRow && !!financialProfile && collegeProfile !== null; // unchanged; {} is truthy
|
const dataReady = !!scenarioRow && !!financialProfile && collegeProfile !== null;
|
||||||
|
|
||||||
const reloadScenarioAndCollege = useCallback(async () => {
|
const reloadScenarioAndCollege = useCallback(async () => {
|
||||||
if (!careerProfileId) return;
|
if (!careerProfileId) return;
|
||||||
@ -409,24 +409,10 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) {
|
|||||||
setScenarioRow(row);
|
setScenarioRow(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
const c = await authFetch(`/api/premium/college-profile?careerProfileId=${encodeURIComponent(careerProfileId)}`);
|
const c = await authFetch(
|
||||||
if (c.status === 404) {
|
`/api/premium/college-profile?careerProfileId=${careerProfileId}`
|
||||||
setCollegeProfile({}); // no profile yet → use defaults
|
);
|
||||||
return;
|
if (c.ok) setCollegeProfile(await c.json());
|
||||||
}
|
|
||||||
if (c.ok) {
|
|
||||||
const ct = c.headers.get('content-type') || '';
|
|
||||||
if (ct.includes('application/json')) {
|
|
||||||
setCollegeProfile(await c.json());
|
|
||||||
} else {
|
|
||||||
// defensive: upstream returned HTML (e.g., 502 body with wrong status)
|
|
||||||
setCollegeProfile({});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// non-OK (e.g., 502)
|
|
||||||
console.error('college-profile fetch failed', c.status);
|
|
||||||
setCollegeProfile({});
|
|
||||||
}, [careerProfileId]);
|
}, [careerProfileId]);
|
||||||
|
|
||||||
const milestoneGroups = useMemo(() => {
|
const milestoneGroups = useMemo(() => {
|
||||||
@ -528,16 +514,15 @@ useEffect(() => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const up = await authFetch('/api/user-profile');
|
const up = await authFetch('/api/user-profile');
|
||||||
if (up.ok && (up.headers.get('content-type')||'').includes('application/json')) {
|
if (up.ok) setUserProfile(await up.json());
|
||||||
setUserProfile(await up.json());
|
|
||||||
}
|
|
||||||
const fp = await authFetch('/api/premium/financial-profile');
|
const fp = await authFetch('/api/premium/financial-profile');
|
||||||
if (fp.status === 404) {
|
if (fp.status === 404) {
|
||||||
// user skipped onboarding – treat as empty object
|
// user skipped onboarding – treat as empty object
|
||||||
setFinancialProfile({});
|
setFinancialProfile({});
|
||||||
} else if (fp.ok && (fp.headers.get('content-type')||'').includes('application/json')) {
|
} else if (fp.ok) {
|
||||||
setFinancialProfile(await fp.json());
|
setFinancialProfile(await fp.json());
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -704,92 +689,7 @@ useEffect(() => {
|
|||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
(async function init () {
|
(async function init () {
|
||||||
// ─────────────────────────────────────────────────────────────
|
/* 1 ▸ get every row the user owns */
|
||||||
// 0) Prefer a real profile. Use ephemeral ONLY if none matches.
|
|
||||||
// Precedence when arriving with premiumOnboardingState:
|
|
||||||
// a) SOC match
|
|
||||||
// b) title match
|
|
||||||
// c) school match (via college_profile/all)
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
if (!careerId) {
|
|
||||||
const pos = location.state?.premiumOnboardingState;
|
|
||||||
const sc = pos?.selectedCareer;
|
|
||||||
if (sc) {
|
|
||||||
const chosenSoc = sc.code || sc.soc_code || sc.socCode || '';
|
|
||||||
const chosenName = (sc.title || sc.career_name || '').trim();
|
|
||||||
|
|
||||||
// 1) fetch all user profiles
|
|
||||||
let all = [];
|
|
||||||
try {
|
|
||||||
all = await getAllCareerProfiles(); // uses authFetch
|
|
||||||
} catch { /* ignore; backend will log */ }
|
|
||||||
|
|
||||||
const bySoc = chosenSoc
|
|
||||||
? all.find(p => p.soc_code === chosenSoc)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const norm = (s='') => s.toLowerCase().replace(/\s+/g,' ').trim();
|
|
||||||
const byTitle = !bySoc && chosenName
|
|
||||||
? all.find(p => norm(p.career_name||p.scenario_title||'') === norm(chosenName))
|
|
||||||
: null;
|
|
||||||
|
|
||||||
let bySchool = null;
|
|
||||||
if (!bySoc && !byTitle && pos.selectedSchool) {
|
|
||||||
const schoolName = norm(pos.selectedSchool?.INSTNM || '');
|
|
||||||
try {
|
|
||||||
const r = await authFetch('/api/premium/college-profile/all');
|
|
||||||
if (r.ok && (r.headers.get('content-type')||'').includes('application/json')) {
|
|
||||||
const { collegeProfiles = [] } = await r.json();
|
|
||||||
const ids = new Set(
|
|
||||||
collegeProfiles
|
|
||||||
.filter(cp => norm(cp.selected_school || '') === schoolName)
|
|
||||||
.map(cp => cp.career_profile_id)
|
|
||||||
);
|
|
||||||
// pick newest start_date among candidates
|
|
||||||
const sorted = [...all].sort((a,b) => (a.start_date > b.start_date ? -1 : 1));
|
|
||||||
bySchool = sorted.find(p => ids.has(p.id)) || null;
|
|
||||||
}
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = bySoc || byTitle || bySchool;
|
|
||||||
if (match && !cancelled) {
|
|
||||||
// ✅ Normal mode: select existing profile
|
|
||||||
setCareerProfileId(match.id);
|
|
||||||
setSelectedCareer(match);
|
|
||||||
localStorage.setItem('lastSelectedCareerProfileId', match.id);
|
|
||||||
window.history.replaceState({}, '', location.pathname); // clear bridge
|
|
||||||
return; // let the rest of the effects run as usual
|
|
||||||
}
|
|
||||||
|
|
||||||
// ⚠️ No matching profile → ephemeral (financial profile is still used)
|
|
||||||
if (!cancelled) {
|
|
||||||
setCareerProfileId(null);
|
|
||||||
setSelectedCareer(sc);
|
|
||||||
setScenarioRow({
|
|
||||||
id: 'temp',
|
|
||||||
scenario_title: chosenName,
|
|
||||||
career_name : chosenName,
|
|
||||||
status : 'planned',
|
|
||||||
start_date : new Date().toISOString().slice(0,10),
|
|
||||||
college_enrollment_status: ''
|
|
||||||
});
|
|
||||||
const sch = pos.selectedSchool;
|
|
||||||
setCollegeProfile(sch ? {
|
|
||||||
selected_school : sch?.INSTNM || '',
|
|
||||||
selected_program : sch?.CIPDESC || '',
|
|
||||||
tuition : Number(sch?.['In_state cost'] ?? sch?.['Out_state cost'] ?? 0) || 0
|
|
||||||
} : {});
|
|
||||||
if (chosenSoc) {
|
|
||||||
setFullSocCode(chosenSoc);
|
|
||||||
setStrippedSocCode(chosenSoc.split('.')[0]);
|
|
||||||
}
|
|
||||||
window.history.replaceState({}, '', location.pathname); // clear bridge
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const r = await authFetch('/api/premium/career-profile/all');
|
const r = await authFetch('/api/premium/career-profile/all');
|
||||||
if (!r?.ok || cancelled) return;
|
if (!r?.ok || cancelled) return;
|
||||||
const { careerProfiles=[] } = await r.json();
|
const { careerProfiles=[] } = await r.json();
|
||||||
@ -1043,10 +943,10 @@ useEffect(() => {
|
|||||||
{ signal: ctrl.signal }
|
{ signal: ctrl.signal }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.ok && (res.headers.get('content-type')||'').includes('application/json')) {
|
if (res.ok) {
|
||||||
setEconomicProjections(await res.json());
|
setEconomicProjections(await res.json());
|
||||||
setEconLoading(false);
|
setEconLoading(false);
|
||||||
} else {
|
} else {
|
||||||
console.error('[Econ fetch]', res.status);
|
console.error('[Econ fetch]', res.status);
|
||||||
setEconLoading(false);
|
setEconLoading(false);
|
||||||
}
|
}
|
||||||
@ -1201,7 +1101,7 @@ if (allMilestones.length) {
|
|||||||
annualFinancialAid: collegeData.annualFinancialAid,
|
annualFinancialAid: collegeData.annualFinancialAid,
|
||||||
calculatedTuition: collegeData.calculatedTuition,
|
calculatedTuition: collegeData.calculatedTuition,
|
||||||
extraPayment: collegeData.extraPayment,
|
extraPayment: collegeData.extraPayment,
|
||||||
enrollmentDate: collegeProfile.enrollment_date || null,
|
enrollmentDate: collegeProfile.enrollment_Date || null,
|
||||||
inCollege: collegeData.inCollege,
|
inCollege: collegeData.inCollege,
|
||||||
gradDate: collegeData.gradDate,
|
gradDate: collegeData.gradDate,
|
||||||
programType: collegeData.programType,
|
programType: collegeData.programType,
|
||||||
@ -1238,7 +1138,7 @@ if (allMilestones.length) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!financialProfile || !scenarioRow || collegeProfile === null) return;
|
if (!financialProfile || !scenarioRow || !collegeProfile) return;
|
||||||
fetchMilestones();
|
fetchMilestones();
|
||||||
}, [financialProfile, scenarioRow, collegeProfile, careerProfileId, simulationYears, interestStrategy, flatAnnualRate, randomRangeMin, randomRangeMax]);
|
}, [financialProfile, scenarioRow, collegeProfile, careerProfileId, simulationYears, interestStrategy, flatAnnualRate, randomRangeMin, randomRangeMax]);
|
||||||
|
|
||||||
@ -1384,11 +1284,16 @@ const currentIdRef = useRef(null);
|
|||||||
const fetchMilestones = useCallback(async () => {
|
const fetchMilestones = useCallback(async () => {
|
||||||
if (!careerProfileId) return;
|
if (!careerProfileId) return;
|
||||||
|
|
||||||
const profRes = await authFetch(`/api/premium/milestones?careerProfileId=${careerProfileId}`);
|
const [profRes, uniRes] = await Promise.all([
|
||||||
if (!profRes.ok) return;
|
authFetch(`/api/premium/milestones?careerProfileId=${careerProfileId}`),
|
||||||
const ct = profRes.headers.get('content-type') || '';
|
authFetch(`/api/premium/milestones?careerProfileId=universal`)
|
||||||
const { milestones: profMs = [] } = ct.includes('application/json') ? await profRes.json() : { milestones: [] };
|
]);
|
||||||
const merged = profMs;
|
if (!profRes.ok || !uniRes.ok) return;
|
||||||
|
|
||||||
|
const [{ milestones: profMs }, { milestones: uniMs }] =
|
||||||
|
await Promise.all([profRes.json(), uniRes.json()]);
|
||||||
|
|
||||||
|
const merged = [...profMs, ...uniMs];
|
||||||
setScenarioMilestones(merged);
|
setScenarioMilestones(merged);
|
||||||
if (financialProfile && scenarioRow && collegeProfile) {
|
if (financialProfile && scenarioRow && collegeProfile) {
|
||||||
buildProjection(merged);
|
buildProjection(merged);
|
||||||
|
@ -172,21 +172,10 @@ function normalizeCipList(arr) {
|
|||||||
const proceed = window.confirm(
|
const proceed = window.confirm(
|
||||||
'You’re about to move to the financial planning portion of the app, which is reserved for premium subscribers. Do you want to continue?'
|
'You’re about to move to the financial planning portion of the app, which is reserved for premium subscribers. Do you want to continue?'
|
||||||
);
|
);
|
||||||
if (!proceed) return;
|
if (proceed) {
|
||||||
// normalize selectedCareer and carry it forward
|
navigate('/career-roadmap', { state: { selectedSchool: school } });
|
||||||
const sel = selectedCareer
|
}
|
||||||
? { ...selectedCareer, code: selectedCareer.code || selectedCareer.soc_code || selectedCareer.socCode }
|
};
|
||||||
: null;
|
|
||||||
|
|
||||||
navigate('/career-roadmap', {
|
|
||||||
state: {
|
|
||||||
premiumOnboardingState: {
|
|
||||||
selectedCareer: sel, // SOC-bearing career object
|
|
||||||
selectedSchool: school // school card just chosen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function getSearchLinks(ksaName, careerTitle) {
|
function getSearchLinks(ksaName, careerTitle) {
|
||||||
const combinedQuery = `${careerTitle} ${ksaName}`.trim();
|
const combinedQuery = `${careerTitle} ${ksaName}`.trim();
|
||||||
|
Loading…
Reference in New Issue
Block a user