From 837881fbe859c27b80cd88502e19bda81b83e23d Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Sat, 21 Mar 2026 03:04:29 -0700 Subject: [PATCH] fix(settings): address resume tab review issues - add loadError ref (separated from empty-state path) - add stable id to WorkEntry, use as v-for key - move addExperience/removeExperience/addTag/removeTag to store actions - strip id from save payload - fix uploadError type handling in handleUpload - add outer try/except to upload_resume endpoint - gate syncFromProfile to non-loaded resume only - add date_of_birth input to personal info section - add loadError test --- dev-api.py | 31 +++++---- web/src/stores/settings/resume.test.ts | 8 +++ web/src/stores/settings/resume.ts | 38 +++++++++-- web/src/views/settings/ResumeProfileView.vue | 67 +++++++++----------- 4 files changed, 91 insertions(+), 53 deletions(-) diff --git a/dev-api.py b/dev-api.py index d2486dc..052adb6 100644 --- a/dev-api.py +++ b/dev-api.py @@ -1074,17 +1074,22 @@ def create_blank_resume(): @app.post("/api/settings/resume/upload") async def upload_resume(file: UploadFile): - from scripts.resume_parser import structure_resume - import tempfile, os - suffix = Path(file.filename).suffix.lower() - with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp: - tmp.write(await file.read()) - tmp_path = tmp.name try: - result, error = structure_resume(tmp_path) - finally: - os.unlink(tmp_path) - if error: - return {"ok": False, "error": error, "data": result} - result["exists"] = True - return {"ok": True, "data": result} + from scripts.resume_parser import structure_resume + import tempfile, os + suffix = Path(file.filename).suffix.lower() + tmp_path = None + with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp: + tmp.write(await file.read()) + tmp_path = tmp.name + try: + result, err = structure_resume(tmp_path) + finally: + if tmp_path: + os.unlink(tmp_path) + if err: + return {"ok": False, "error": err, "data": result} + result["exists"] = True + return {"ok": True, "data": result} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/web/src/stores/settings/resume.test.ts b/web/src/stores/settings/resume.test.ts index 4415513..7ab9e73 100644 --- a/web/src/stores/settings/resume.test.ts +++ b/web/src/stores/settings/resume.test.ts @@ -39,4 +39,12 @@ describe('useResumeStore', () => { await store.load() expect(store.hasResume).toBe(false) }) + + it('load() sets loadError on API error', async () => { + mockFetch.mockResolvedValue({ data: null, error: { kind: 'network', message: 'Network error' } }) + const store = useResumeStore() + await store.load() + expect(store.loadError).toBeTruthy() + expect(store.hasResume).toBe(false) + }) }) diff --git a/web/src/stores/settings/resume.ts b/web/src/stores/settings/resume.ts index 8ecbba9..0905c61 100644 --- a/web/src/stores/settings/resume.ts +++ b/web/src/stores/settings/resume.ts @@ -3,6 +3,7 @@ import { defineStore } from 'pinia' import { useApiFetch } from '../../composables/useApi' export interface WorkEntry { + id: string title: string; company: string; period: string; location: string industry: string; responsibilities: string; skills: string[] } @@ -12,6 +13,7 @@ export const useResumeStore = defineStore('settings/resume', () => { const loading = ref(false) const saving = ref(false) const saveError = ref(null) + const loadError = ref(null) // Identity (synced from profile store) const name = ref(''); const email = ref(''); const phone = ref(''); const linkedin_url = ref('') @@ -37,16 +39,21 @@ export const useResumeStore = defineStore('settings/resume', () => { async function load() { loading.value = true + loadError.value = null const { data, error } = await useApiFetch>('/api/settings/resume') loading.value = false - if (error || !data || !data.exists) { hasResume.value = false; return } + if (error) { + loadError.value = error.kind === 'network' ? error.message : (error.detail || 'Failed to load resume') + return + } + if (!data || !data.exists) { hasResume.value = false; return } hasResume.value = true name.value = String(data.name ?? ''); email.value = String(data.email ?? '') phone.value = String(data.phone ?? ''); linkedin_url.value = String(data.linkedin_url ?? '') surname.value = String(data.surname ?? ''); address.value = String(data.address ?? '') city.value = String(data.city ?? ''); zip_code.value = String(data.zip_code ?? '') date_of_birth.value = String(data.date_of_birth ?? '') - experience.value = (data.experience as WorkEntry[]) ?? [] + experience.value = (data.experience as Omit[]).map(e => ({ ...e, id: crypto.randomUUID() })) ?? [] salary_min.value = Number(data.salary_min ?? 0); salary_max.value = Number(data.salary_max ?? 0) notice_period.value = String(data.notice_period ?? '') remote.value = Boolean(data.remote); relocation.value = Boolean(data.relocation) @@ -64,7 +71,8 @@ export const useResumeStore = defineStore('settings/resume', () => { const body = { name: name.value, email: email.value, phone: phone.value, linkedin_url: linkedin_url.value, surname: surname.value, address: address.value, city: city.value, zip_code: zip_code.value, - date_of_birth: date_of_birth.value, experience: experience.value, + date_of_birth: date_of_birth.value, + experience: experience.value.map(({ id: _id, ...e }) => e), salary_min: salary_min.value, salary_max: salary_max.value, notice_period: notice_period.value, remote: remote.value, relocation: relocation.value, assessment: assessment.value, background_check: background_check.value, @@ -84,12 +92,34 @@ export const useResumeStore = defineStore('settings/resume', () => { if (!error) { hasResume.value = true; await load() } } + function addExperience() { + experience.value.push({ id: crypto.randomUUID(), title: '', company: '', period: '', location: '', industry: '', responsibilities: '', skills: [] }) + } + + function removeExperience(idx: number) { + experience.value.splice(idx, 1) + } + + function addTag(field: 'skills' | 'domains' | 'keywords', value: string) { + const arr = field === 'skills' ? skills.value : field === 'domains' ? domains.value : keywords.value + const trimmed = value.trim() + if (!trimmed || arr.includes(trimmed)) return + arr.push(trimmed) + } + + function removeTag(field: 'skills' | 'domains' | 'keywords', value: string) { + const arr = field === 'skills' ? skills.value : field === 'domains' ? domains.value : keywords.value + const idx = arr.indexOf(value) + if (idx !== -1) arr.splice(idx, 1) + } + return { - hasResume, loading, saving, saveError, + hasResume, loading, saving, saveError, loadError, name, email, phone, linkedin_url, surname, address, city, zip_code, date_of_birth, experience, salary_min, salary_max, notice_period, remote, relocation, assessment, background_check, gender, pronouns, ethnicity, veteran_status, disability, skills, domains, keywords, syncFromProfile, load, save, createBlank, + addExperience, removeExperience, addTag, removeTag, } }) diff --git a/web/src/views/settings/ResumeProfileView.vue b/web/src/views/settings/ResumeProfileView.vue index d32915d..e3ffe82 100644 --- a/web/src/views/settings/ResumeProfileView.vue +++ b/web/src/views/settings/ResumeProfileView.vue @@ -2,6 +2,11 @@

Resume Profile

+ +
+ Failed to load resume: {{ loadError }} +
+

No resume found. Choose how to get started:

@@ -65,12 +70,16 @@
+
+ + +

Work Experience

-
+
@@ -95,9 +104,9 @@