diff --git a/app/components/FormEditPanel.vue b/app/components/FormEditPanel.vue new file mode 100644 index 0000000..0a1c55a --- /dev/null +++ b/app/components/FormEditPanel.vue @@ -0,0 +1,180 @@ +// The edit panel with 5-step stepper for application and radio inputs for clinical forms + + + \ No newline at end of file diff --git a/app/components/Notes.vue b/app/components/Notes.vue index 011427e..f91ab5d 100644 --- a/app/components/Notes.vue +++ b/app/components/Notes.vue @@ -141,6 +141,8 @@ const localPreviousNotes = ref([...props.previousNotes]) const localSessionNotes = ref([...props.sessionNotes]) + const formEditPanelRef = ref<{ handleSave: () => void } | null>(null) + watch( () => props.previousNotes, (v) => { @@ -250,6 +252,12 @@ } async function selectSessionNote(sn: SessionNoteRow) { + if (sn.status === 'DRAFT' && sn.appointmentId) { + selectedAppointmentId.value = sn.appointmentId + noteContent.value = sn.content + currentNoteKind.value = sn.kind ?? 'PROGRESS' + return + } if (sn.appointmentId && sn.appointmentId === selectedAppointmentId.value) { alert('This session is already open in the current note editor.') return @@ -294,7 +302,7 @@ const newFormModalSelection = ref('') function openNewFormVersion() { - newFormModalSelection.value = props.forms[0]?.label ?? '' + newFormModalSelection.value = props.forms.filter(f => f.label !== 'Application')[0]?.label ?? '' showNewFormModal.value = true } @@ -449,6 +457,25 @@ const formPreviewError = ref(null) const isEditingForm = ref(false) const editableAnswers = ref<{ label: string; answer: string }[]>([]) + + const viewerStep = ref(1) + + const APP_STEP_RANGES = [ + [0, 6], + [7, 17], + [18, 35], + [36, 45], + [46, 49], + ] + + const viewerStepLabels = ['Profile', 'Child', 'Guardian', 'Treatment', 'Therapy'] + + const viewerStepAnswers = computed(() => { + const range = APP_STEP_RANGES[viewerStep.value - 1] + if (!range) return editableAnswers.value + return editableAnswers.value.slice(range[0] ?? 0, (range[1] ?? 0) + 1) + }) + let formPreviewSeq = 0 function severityColor(label: string): string { @@ -513,18 +540,24 @@ } }) - async function saveFormEdits() { + async function onFormEditSave(answers: { label: string; answer: string }[]) { const key = FORM_LABEL_TO_KEY[selectedForm.value!] await $fetch(`/api/clients/${props.client.id}/forms/${key}`, { method: 'PATCH', - body: { answers: editableAnswers.value } + body: { answers }, }) - if (formPreviewData.value) { - formPreviewData.value.questions = [...editableAnswers.value] - } + + // Re-fetch to get updated score/severity + const data = await $fetch( + `/api/clients/${props.client.id}/forms/${key}` + ) + formPreviewData.value = data + editableAnswers.value = data.questions.map((q) => ({ ...q })) + if (data.score != null) formScores.value[selectedForm.value!] = data.score + if (data.severity != null) formSeverities.value[selectedForm.value!] = data.severity + isEditingForm.value = false } - const isEditingPreviousPanel = ref(false) const editingNoteId = ref(null) const editingSessionNoteId = ref(null) @@ -1027,6 +1060,35 @@ const diff = Math.floor((now.getTime() - date.getTime()) / 60000) return diff < 1 ? 'just now' : `${diff} min ago` } + + function formatAppAnswer(val: string): string { + if (!val) return '—' + if (val.includes('|||')) { + return val.split('|||').filter(Boolean).join(', ') + } + try { + const parsed = JSON.parse(val) + if (Array.isArray(parsed)) { + return parsed.map((item: any) => + typeof item === 'object' + ? Object.values(item).filter(Boolean).join(' ') + : String(item) + ).join(', ') + } + if (parsed && typeof parsed === 'object') { + if (Array.isArray(parsed.values)) { + return [...parsed.values, parsed.other].filter(Boolean).join(', ') + } + if (typeof parsed.value === 'string') return parsed.value + if (typeof parsed.firstName === 'string') { + return [parsed.firstName, parsed.middleInitial, parsed.lastName, parsed.age ? `(age ${parsed.age})` : '', parsed.relationship].filter(Boolean).join(' ') + } + } + } catch { + // plain text + } + return val.replace(/_/g, ' ') + } + \ No newline at end of file diff --git a/app/composables/useApplicationOptions.ts b/app/composables/useApplicationOptions.ts new file mode 100644 index 0000000..c85f18d --- /dev/null +++ b/app/composables/useApplicationOptions.ts @@ -0,0 +1,57 @@ +// Shared options (gender, yes/no, custody, etc.) +export function useApplicationOptions() { + const genderOptions = [ + { label: 'Male', value: 'Male' }, + { label: 'Female', value: 'Female' }, + { label: 'Non-binary', value: 'Non-binary' }, + { label: 'Prefer not to say', value: 'Prefer not to say' }, + { label: 'Other', value: 'Other' }, + ] + const yesNoOptions = [ + { label: 'Yes', value: 'yes' }, + { label: 'No', value: 'no' }, + ] + const custodyOptions = [ + { label: 'Mother', value: 'mother' }, + { label: 'Father', value: 'father' }, + { label: 'Joint', value: 'joint' }, + { label: 'Other', value: 'other' }, + ] + const caregiverOptions = [ + { label: 'Mom', value: 'Mom' }, + { label: 'Dad', value: 'Dad' }, + { label: 'Both', value: 'Both' }, + { label: 'Other', value: 'Other' }, + ] + const siblingOptions = [ + { label: 'Yes', value: 'yes' }, + { label: 'No', value: 'no' }, + { label: 'N/A', value: 'na' }, + ] + const supportGroupOptions = [ + { label: 'Adolescent child diagnosed with cancer', value: 'adolescent_child_diagnosed_with_cancer' }, + { label: 'Adolescent sibling', value: 'adolescent_sibling' }, + { label: 'Parent', value: 'parent' }, + { label: 'No', value: 'no' }, + ] + const referralOptions = [ + { label: 'I have a therapist', value: 'have_therapist' }, + { label: 'I need a referral', value: 'need_referral' }, + ] + const insuranceOptions = [ + { label: 'Yes, with mental health benefits', value: 'yes_with_mental_health_benefits' }, + { label: 'Yes, without mental health benefits', value: 'yes_without_mental_health_benefits' }, + { label: 'No insurance', value: 'no_insurance' }, + ] + + return { + genderOptions, + yesNoOptions, + custodyOptions, + caregiverOptions, + siblingOptions, + supportGroupOptions, + referralOptions, + insuranceOptions, + } +} \ No newline at end of file diff --git a/app/pages/forms/pcl.vue b/app/pages/forms/pcl.vue index 2eb82ee..943cfed 100644 --- a/app/pages/forms/pcl.vue +++ b/app/pages/forms/pcl.vue @@ -58,11 +58,29 @@ } function buildPayload() { - const body: Record = { worstEvent: worstEvent.value } - responses.value.forEach((val, i) => { - body[`q${i + 1}`] = val === -1 ? null : val - }) - return body + return { + worstEvent: worstEvent.value, + q01: responses.value[0], + q02: responses.value[1], + q03: responses.value[2], + q04: responses.value[3], + q05: responses.value[4], + q06: responses.value[5], + q07: responses.value[6], + q08: responses.value[7], + q09: responses.value[8], + q10: responses.value[9], + q11: responses.value[10], + q12: responses.value[11], + q13: responses.value[12], + q14: responses.value[13], + q15: responses.value[14], + q16: responses.value[15], + q17: responses.value[16], + q18: responses.value[17], + q19: responses.value[18], + q20: responses.value[19], + } } async function submitForm() { diff --git a/prisma/seed.ts b/prisma/seed.ts index df323a9..8a706c4 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -14,12 +14,8 @@ async function seedForms(userId: string) { // 1. AppForm (Application) const appForm = await prisma.appForm.upsert({ where: { userId }, - update: {}, - create: { - userId, - status: 'COMPLETE', - submittedAt: new Date(), - }, + update: { status: 'COMPLETE', submittedAt: new Date() }, + create: { userId, status: 'COMPLETE', submittedAt: new Date() }, }) await prisma.appQuestion.upsert({ @@ -31,33 +27,15 @@ async function seedForms(userId: string) { q01: 'Bob', q02: 'Builder', q05: '1234567890', - }, - }) + }, +}) // 2. GAD-7 const gadForm = await prisma.gadForm.create({ - data: { - userId, - status: 'COMPLETE', - totalScore: 12, - severity: 'Moderate', - submittedAt: new Date(), - }, + data: { userId, status: 'COMPLETE', totalScore: 12, severity: 'Moderate', submittedAt: new Date() }, }) - await prisma.gadQuestion.create({ - data: { - formId: gadForm.id, - userId, - g01: 2, - g02: 1, - g03: 2, - g04: 1, - g05: 2, - g06: 2, - g07: 2, - g08: 1, - }, + data: { formId: gadForm.id, userId, g01: 2, g02: 1, g03: 2, g04: 1, g05: 2, g06: 2, g07: 2, g08: 1 }, }) // 3. PHQ-9 @@ -70,61 +48,19 @@ async function seedForms(userId: string) { submittedAt: new Date(), }, }) - - await prisma.phqQuestion.upsert({ - where: { formId: phqForm.id }, - update: {}, - create: { - formId: phqForm.id, - userId, - q1: 2, - q2: 1, - q3: 2, - q4: 1, - q5: 2, - q6: 2, - q7: 2, - q8: 1, - q9: 1, - q10: 1, - }, + await prisma.phqQuestion.create({ + data: { formId: phqForm.id, userId, q1: 2, q2: 1, q3: 2, q4: 1, q5: 2, q6: 2, q7: 2, q8: 1, q9: 1, q10: 1 }, }) // 4. PCL-5 const pclForm = await prisma.pclForm.create({ - data: { - userId, - status: 'COMPLETE', - totalScore: 45, - severity: 'Moderate', - submittedAt: new Date(), - }, + data: { userId, status: 'COMPLETE', totalScore: 45, severity: 'Moderate', submittedAt: new Date() }, }) - await prisma.pclQuestion.create({ data: { - formId: pclForm.id, - userId, - q01: 3, - q02: 2, - q03: 3, - q04: 2, - q05: 2, - q06: 3, - q07: 2, - q08: 2, - q09: 2, - q10: 2, - q11: 2, - q12: 2, - q13: 2, - q14: 2, - q15: 2, - q16: 2, - q17: 2, - q18: 2, - q19: 2, - q20: 2, + formId: pclForm.id, userId, + q01: 3, q02: 2, q03: 3, q04: 2, q05: 2, q06: 3, q07: 2, q08: 2, q09: 2, q10: 2, + q11: 2, q12: 2, q13: 2, q14: 2, q15: 2, q16: 2, q17: 2, q18: 2, q19: 2, q20: 2, }, }) @@ -138,31 +74,19 @@ async function seedForms(userId: string) { submittedAt: new Date(), }, }) - - await prisma.aceQuestion.upsert({ - where: { formId: aceForm.id }, - update: {}, - create: { - formId: aceForm.id, - userId, - a01: 'Yes', - a02: 'Yes', - a03: 'No', - a04: 'No', - a05: 'No', - a06: 'No', - a07: 'No', - a08: 'No', - a09: 'No', - a10: 'No', + await prisma.aceQuestion.create({ + data: { + formId: aceForm.id, userId, + a01: 'Yes', a02: 'No', a03: 'No', a04: 'Yes', a05: 'Yes', + a06: 'Yes', a07: 'No', a08: 'Yes', a09: 'No', a10: 'No', }, }) } -async function ensureBobBuilderSessionNotes(bobUserId: string) { +async function ensureBobBuilderSessionNotes(bobUserId: string, clinicianUserId: string) { const client = await prisma.client.upsert({ where: { userId: bobUserId }, - update: { status: 'ACTIVE' }, + update: { status: 'ACTIVE', clinician: { connect: { id: clinicianUserId } } }, create: { userId: bobUserId, status: 'ACTIVE' }, }) @@ -170,27 +94,27 @@ async function ensureBobBuilderSessionNotes(bobUserId: string) { where: { clientId: client.id }, }) - if (existingSessionNotes === 0) { - await prisma.sessionNote.createMany({ - data: [ - { - clientId: client.id, - sessionName: 'Intake / Week 1', - sessionNumber: 1, - content: - 'Intake / Week 1 — Rapport established. Bob reviewed clinic policies and confidentiality. Reported primary stressors related to work deadlines and sleep disruption. PHQ-9 and GAD-7 administered; safety screen negative. Plan: sleep hygiene handout, begin weekly CBT skills.', - }, - { - clientId: client.id, - sessionName: 'Session 2', - sessionNumber: 2, - content: - 'Session 2 — Focus on thought challenging around catastrophic predictions at work. Homework: thought record for 3 situations. Bob engaged well; identified one automatic thought pattern to monitor between sessions.', - }, - ], - }) - console.log('Created sample SessionNote rows for Bob Builder.') - } + // if (existingSessionNotes === 0) { + // await prisma.sessionNote.createMany({ + // data: [ + // { + // clientId: client.id, + // sessionName: 'Intake / Week 1', + // sessionNumber: 1, + // content: + // 'Intake / Week 1 — Rapport established. Bob reviewed clinic policies and confidentiality. Reported primary stressors related to work deadlines and sleep disruption. PHQ-9 and GAD-7 administered; safety screen negative. Plan: sleep hygiene handout, begin weekly CBT skills.', + // }, + // { + // clientId: client.id, + // sessionName: 'Session 2', + // sessionNumber: 2, + // content: + // 'Session 2 — Focus on thought challenging around catastrophic predictions at work. Homework: thought record for 3 situations. Bob engaged well; identified one automatic thought pattern to monitor between sessions.', + // }, + // ], + // }) + // console.log('Created sample SessionNote rows for Bob Builder.') + // } // Populate form dummy data if it doesn't exist const existingApp = await prisma.appForm.count({ where: { userId: bobUserId } }) @@ -198,6 +122,89 @@ async function ensureBobBuilderSessionNotes(bobUserId: string) { await seedForms(bobUserId) console.log('Seeded clinical forms for Bob Builder.') } + + return client +} + +async function seedApprovalWorkflowNotes( + clientId: string, + clinicianUserId: string, + adminUserId: string +) { + const existing = await prisma.sessionNote.count({ where: { clientId } }) + if (existing > 0) { + console.log('Session notes already exist; skipping approval-workflow seed.') + return + } + + const PLACEHOLDER_SIG = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=' + + const now = new Date() + + await prisma.sessionNote.create({ + data: { + clientId, sessionName: 'Intake – initial session', sessionNumber: 1, + kind: 'PROGRESS', status: 'DRAFT', + content: 'Client arrived on time. Presenting concerns include sleep disruption and work stress. Draft — still gathering history.', + attendanceStatus: 'show', + }, + }) + + const pendingNote = await prisma.sessionNote.create({ + data: { + clientId, sessionName: 'Session 2 – CBT intro', sessionNumber: 2, + kind: 'PROGRESS', status: 'CLINICIAN_SIGNED', + content: 'Reviewed sleep-hygiene worksheet. Client reports mild improvement. Introduced cognitive-restructuring framework.', + attendanceStatus: 'late', + clinicianSignedAt: now, clinicianSignedById: clinicianUserId, clinicianSignatureData: PLACEHOLDER_SIG, + }, + }) + + await prisma.notification.create({ + data: { + userId: adminUserId, + type: 'NOTE_READY_FOR_APPROVAL', + title: 'Note awaiting approval', + message: 'Carl Karl signed a progress note for Bob Builder. Please review and countersign.', + sessionNoteId: pendingNote.id, + }, + }) + + await prisma.sessionNote.create({ + data: { + clientId, sessionName: 'Session 3 – thought records', sessionNumber: 3, + kind: 'PROGRESS', status: 'FULLY_APPROVED', + content: 'Completed two thought-record examples in session. Client identified core belief patterns and committed to daily practice.', + attendanceStatus: 'no-show', + clinicianSignedAt: now, clinicianSignedById: clinicianUserId, clinicianSignatureData: PLACEHOLDER_SIG, + adminSignedAt: now, adminSignedById: adminUserId, adminSignatureData: PLACEHOLDER_SIG, + adminApprovalNote: 'Reviewed — documentation meets clinic standard.', + }, + }) + + await prisma.sessionNote.create({ + data: { + clientId, sessionName: 'Session 3 – clinician process notes', sessionNumber: 3, + kind: 'PSYCHOTHERAPY', status: 'DRAFT', + content: 'Clinician-only process notes: countertransference observations, working hypotheses, and next-session targets.', + attendanceStatus: 'show', + }, + }) + + await prisma.sessionNote.create({ + data: { + clientId, sessionName: 'Session 2 – clinician process notes', sessionNumber: 2, + kind: 'PSYCHOTHERAPY', status: 'FULLY_APPROVED', + content: 'Process notes: explored defense patterns around perfectionism. Plan to revisit in session 4.', + attendanceStatus: 'show', + clinicianSignedAt: now, clinicianSignedById: clinicianUserId, clinicianSignatureData: PLACEHOLDER_SIG, + adminSignedAt: now, adminSignedById: adminUserId, adminSignatureData: PLACEHOLDER_SIG, + adminApprovalNote: 'Approved — psychotherapy note retained separately.', + }, + }) + + console.log('Seeded 5 session notes across DRAFT / CLINICIAN_SIGNED / FULLY_APPROVED.') } async function main() { @@ -207,16 +214,14 @@ async function main() { await backfillSessionNotesRequestTemplates(prisma) // Create / Upsert Alice (Admin) - await prisma.user.upsert({ + const alice = await prisma.user.upsert({ where: { email: 'alice@a.com' }, update: { role: 'ADMIN', name: 'Alice Wonderland' }, - create: { - id: 'alice_id', - email: 'alice@a.com', - name: 'Alice Wonderland', - emailVerified: true, - role: 'ADMIN', - }, + create: { id: 'alice_id', + email: 'alice@a.com', + name: 'Alice Wonderland', + emailVerified: true, + role: 'ADMIN' }, }) console.log('Seeded Admin: alice@a.com') @@ -245,34 +250,22 @@ async function main() { console.log(`Seeded Admin: ${admin.email}`) } - await prisma.user.upsert({ + const carl = await prisma.user.upsert({ where: { email: 'carl@c.com' }, update: { role: 'CLINICIAN', name: 'Carl Karl' }, - create: { - id: 'carl_id', - email: 'carl@c.com', - name: 'Carl Karl', - emailVerified: true, - role: 'CLINICIAN', - }, + create: { id: 'carl_id', email: 'carl@c.com', name: 'Carl Karl', emailVerified: true, role: 'CLINICIAN' }, }) console.log('Seeded Clinician: carl@c.com') - // Create / Upsert Bob (Client) const bob = await prisma.user.upsert({ where: { email: 'bob@b.com' }, update: { role: 'CLIENT', name: 'Bob Builder' }, - create: { - id: 'bob_id', - email: 'bob@b.com', - name: 'Bob Builder', - emailVerified: true, - role: 'CLIENT', - }, + create: { id: 'bob_id', email: 'bob@b.com', name: 'Bob Builder', emailVerified: true, role: 'CLIENT' }, }) console.log('Seeded Client: bob@b.com') - await ensureBobBuilderSessionNotes(bob.id) + const bobClient = await ensureBobBuilderSessionNotes(bob.id, carl.id) + await seedApprovalWorkflowNotes(bobClient.id, carl.id, alice.id) console.log('Seeding finished.') } diff --git a/server/api/admin/backfill-absences.post.ts b/server/api/admin/backfill-absences.post.ts index 4a39ae6..23b4777 100644 --- a/server/api/admin/backfill-absences.post.ts +++ b/server/api/admin/backfill-absences.post.ts @@ -22,7 +22,7 @@ export default defineEventHandler(async (event) => { const calendarAbsences = await prisma.sessionNote.count({ where: { clientId: client.id, - attended: false, + attendanceStatus: 'no-show', }, }) diff --git a/server/api/appointments/index.post.ts b/server/api/appointments/index.post.ts index 4a1b91d..64df377 100644 --- a/server/api/appointments/index.post.ts +++ b/server/api/appointments/index.post.ts @@ -130,7 +130,7 @@ export default defineEventHandler(async (event) => { sessionName, sessionNumber, content: '', - attended: true, + attendanceStatus: 'show', }, }) } diff --git a/server/api/clients/[id]/forms/[formKey].get.ts b/server/api/clients/[id]/forms/[formKey].get.ts index 9c2bb50..ad5af96 100644 --- a/server/api/clients/[id]/forms/[formKey].get.ts +++ b/server/api/clients/[id]/forms/[formKey].get.ts @@ -115,7 +115,18 @@ export default defineEventHandler(async (event) => { if (!trimmed) return '' try { const parsed = JSON.parse(trimmed) as unknown - if (Array.isArray(parsed)) return parsed.join(', ') + if (Array.isArray(parsed)) { + return parsed.map((item: unknown) => { + if (item && typeof item === 'object') { + const r = item as Record + if (typeof r.firstName === 'string') { + return [r.firstName, r.middleInitial, r.lastName, r.age ? `(age ${r.age})` : '', r.relationship].filter(Boolean).join(' ') + } + return Object.values(r).filter(Boolean).join(' ') + } + return String(item) + }).join(', ') + } if (parsed && typeof parsed === 'object') { const r = parsed as Record if (Array.isArray(r.values)) { @@ -176,6 +187,7 @@ export default defineEventHandler(async (event) => { formName: 'GAD-7', questions, submitted: gadForm?.status === 'COMPLETE', + submittedAt: gadForm?.submittedAt, score: gadForm?.totalScore, severity: gadForm?.severity, } @@ -193,10 +205,13 @@ export default defineEventHandler(async (event) => { formName: 'PHQ-9', questions, submitted: phqForm?.status === 'COMPLETE', + submittedAt: phqForm?.submittedAt, score: phqForm?.totalScore, + severity: phqForm?.severity, } } + // ── PCL-5 ───────────────────────────────────────────────── if (formKey === 'pcl') { const pclForm = await prisma.pclForm.findFirst({ where: { userId: clientUserId }, @@ -209,7 +224,46 @@ export default defineEventHandler(async (event) => { (await prisma.pclQuestion.findFirst({ where: { formId: pclForm.id } })) ?? (await prisma.pclQuestion.findFirst({ where: { userId: clientUserId } })) } - const questions = await loadClinicalFormQuestions(prisma, clientUserId, 'pcl') + let questions = await loadClinicalFormQuestions(prisma, clientUserId, 'pcl') + + const OPTIONS = ['Not at all', 'A little bit', 'Moderately', 'Quite a bit', 'Extremely'] + + if (questions.length < 20 && pclForm) { // ← was `< 0`, which is never true + const PCL5_QUESTIONS = [ + 'Repeated, disturbing, and unwanted memories of the stressful experience?', + 'Repeated, disturbing dreams of the stressful experience?', + 'Suddenly feeling or acting as if the stressful experience were actually happening again?', + 'Feeling very upset when something reminded you of the stressful experience?', + 'Having strong physical reactions when something reminded you of the stressful experience?', + 'Avoiding memories, thoughts, or feelings related to the stressful experience?', + 'Avoiding external reminders of the stressful experience?', + 'Trouble remembering important parts of the stressful experience?', + 'Having strong negative beliefs about yourself, other people, or the world?', + 'Blaming yourself or someone else for the stressful experience or what happened after it?', + 'Having strong negative feelings such as fear, horror, anger, guilt, or shame?', + 'Loss of interest in activities that you used to enjoy?', + 'Feeling distant or cut off from other people?', + 'Trouble experiencing positive feelings?', + 'Irritable behavior, angry outbursts, or acting aggressively?', + 'Taking too many risks or doing things that could cause you harm?', + 'Being "superalert" or watchful or on guard?', + 'Feeling jumpy or easily startled?', + 'Having difficulty concentrating?', + 'Trouble falling or staying asleep?', + ] + + questions = [ + // worstEvent as the first question so it appears at the top + { label: 'Worst event', answer: q?.worstEvent ?? '' }, + ...PCL5_QUESTIONS.map((label, i) => { + const key = `q${String(i + 1).padStart(2, '0')}` as keyof typeof q + const raw = q?.[key] + const answer = typeof raw === 'number' && raw >= 0 && raw <= 4 ? (OPTIONS[raw] ?? '') : '' + return { label, answer } + }), + ] + } + let totalScore = pclForm?.totalScore ?? null if (q && totalScore == null) { totalScore = 0 @@ -236,6 +290,5 @@ export default defineEventHandler(async (event) => { severity, } } - throw createError({ statusCode: 400, statusMessage: 'Invalid form key' }) }) diff --git a/server/api/clients/[id]/forms/[formKey].patch.ts b/server/api/clients/[id]/forms/[formKey].patch.ts index 0b77895..392944c 100644 --- a/server/api/clients/[id]/forms/[formKey].patch.ts +++ b/server/api/clients/[id]/forms/[formKey].patch.ts @@ -11,6 +11,13 @@ const PHQ_OPTIONS: Record = { 'Nearly every day': 3, } +const PHQ_DIFFICULTY_OPTIONS: Record = { + 'Not difficult at all': 0, + 'Somewhat difficult': 1, + 'Very difficult': 2, + 'Extremely difficult': 3, +} + const GAD_OPTIONS: Record = { 'Not at all': 0, 'Several days': 1, @@ -18,6 +25,14 @@ const GAD_OPTIONS: Record = { 'Nearly every day': 3, } +const PCL_OPTIONS: Record = { + 'Not at all': 0, + 'A little bit': 1, + 'Moderately': 2, + 'Quite a bit': 3, + 'Extremely': 4, +} + function toInt(val: string, map: Record): number | null { if (val in map) return map[val]! const n = parseInt(val) @@ -56,12 +71,17 @@ export default defineEventHandler(async (event) => { // ── ACE ────────────────────────────────────────────────── if (formKey === 'ace') { - const form = await prisma.aceForm.findFirst({ where: { userId: clientUserId } }) + const form = await prisma.aceForm.findFirst({ where: { userId: clientUserId }, orderBy: { id: 'desc' } }) if (!form) throw createError({ statusCode: 404, statusMessage: 'Form not found' }) const keys = ['a01','a02','a03','a04','a05','a06','a07','a08','a09','a10'] const data: Record = {} answers.forEach((a, i) => { if (keys[i]) data[keys[i]!] = a.answer }) await prisma.aceQuestion.update({ where: { formId: form.id }, data }) + + // Recalculate score + const total = Object.values(data).filter(v => v === 'Yes').length + const severity = total === 0 ? 'No reported ACEs' : total <= 3 ? 'Low' : total <= 6 ? 'Moderate' : 'High' + await prisma.aceForm.update({ where: { id: form.id }, data: { totalScore: total, severity } }) return { ok: true } } @@ -71,8 +91,17 @@ export default defineEventHandler(async (event) => { if (!form) throw createError({ statusCode: 404, statusMessage: 'Form not found' }) const keys = ['g01','g02','g03','g04','g05','g06','g07','g08'] const data: Record = {} - answers.forEach((a, i) => { if (keys[i]) data[keys[i]!] = toInt(a.answer, GAD_OPTIONS) }) + answers.forEach((a, i) => { + if (!keys[i]) return + const map = keys[i] === 'g08' ? PHQ_DIFFICULTY_OPTIONS : GAD_OPTIONS + data[keys[i]!] = toInt(a.answer, map) + }) await prisma.gadQuestion.update({ where: { formId: form.id }, data }) + + // Recalculate score (g01-g07 only, g08 is difficulty) + const total = ['g01','g02','g03','g04','g05','g06','g07'].reduce((sum, k) => sum + (data[k] ?? 0), 0) + const severity = total <= 4 ? 'Minimal' : total <= 9 ? 'Mild' : total <= 14 ? 'Moderate' : 'Severe' + await prisma.gadForm.update({ where: { id: form.id }, data: { totalScore: total, severity } }) return { ok: true } } @@ -82,23 +111,56 @@ export default defineEventHandler(async (event) => { if (!form) throw createError({ statusCode: 404, statusMessage: 'Form not found' }) const keys = ['q1','q2','q3','q4','q5','q6','q7','q8','q9','q10'] const data: Record = {} - answers.forEach((a, i) => { if (keys[i]) data[keys[i]!] = toInt(a.answer, PHQ_OPTIONS) }) - await prisma.phqQuestion.update({ where: { formId: form.id }, data }) - return { ok: true } - } - - // ── PCL-5 ───────────────────────────────────────────────── - if (formKey === 'pcl') { - const form = await prisma.pclForm.findFirst({ where: { userId: clientUserId }, orderBy: { id: 'desc' } }) - if (!form) throw createError({ statusCode: 404, statusMessage: 'Form not found' }) - const data: Record = {} answers.forEach((a, i) => { - const key = `q${String(i + 1).padStart(2, '0')}` - data[key] = toInt(a.answer, {}) + if (!keys[i]) return + // q10 uses different options than q1-q9 + const map = keys[i] === 'q10' ? PHQ_DIFFICULTY_OPTIONS : PHQ_OPTIONS + data[keys[i]!] = toInt(a.answer, map) }) - await prisma.pclQuestion.update({ where: { formId: form.id }, data }) + await prisma.phqQuestion.update({ where: { formId: form.id }, data }) + + // Recalculate score (q1-q9 only, q10 is difficulty) + const total = ['q1','q2','q3','q4','q5','q6','q7','q8','q9'].reduce((sum, k) => sum + (data[k] ?? 0), 0) + const severity = total <= 4 ? 'Minimal' : total <= 9 ? 'Mild' : total <= 14 ? 'Moderate depression' : total <= 19 ? 'Moderately severe depression' : 'Severe depression' + await prisma.phqForm.update({ where: { id: form.id }, data: { totalScore: total, severity } }) return { ok: true } } +// ── PCL-5 ───────────────────────────────────────────────── +if (formKey === 'pcl') { + const form = await prisma.pclForm.findFirst({ where: { userId: clientUserId }, orderBy: { id: 'desc' } }) + if (!form) throw createError({ statusCode: 404, statusMessage: 'Form not found' }) + + const worstEventAnswer = answers.find(a => a.label === 'Worst event') + const questionAnswers = answers.filter(a => a.label !== 'Worst event') + + const data: Record = {} + + if (worstEventAnswer !== undefined) { + data.worstEvent = worstEventAnswer.answer + } + + questionAnswers.forEach((a, i) => { + const key = `q${String(i + 1).padStart(2, '0')}` + data[key] = toInt(a.answer, PCL_OPTIONS) + }) + + await prisma.pclQuestion.update({ where: { formId: form.id }, data }) + + // Recalculate score (only scored questions, not worstEvent) + const total = questionAnswers.reduce((sum, a) => sum + (PCL_OPTIONS[a.answer] ?? 0), 0) + const severity = total <= 20 ? 'Minimal' : total <= 40 ? 'Mild' : total <= 60 ? 'Moderate' : 'Severe' + await prisma.pclForm.update({ + where: { id: form.id }, + data: { + totalScore: total, + severity, + status: 'COMPLETE', // ← add + submittedAt: new Date(), // ← add + } + }) + return { ok: true } +} + throw createError({ statusCode: 400, statusMessage: 'Invalid form key' }) }) \ No newline at end of file diff --git a/server/api/clients/[id]/forms/[formKey]/raw.get.ts b/server/api/clients/[id]/forms/[formKey]/raw.get.ts new file mode 100644 index 0000000..5abb70e --- /dev/null +++ b/server/api/clients/[id]/forms/[formKey]/raw.get.ts @@ -0,0 +1,41 @@ +// Returns raw db values for the application form +import { requireUser } from '../../../../../utils/guard' +import { assertStaffCanAccessClient } from '../../../../../utils/clinician-access' +import { createError, defineEventHandler, getRouterParam } from 'h3' +import { prisma } from '../../../../../utils/prisma' + +export default defineEventHandler(async (event) => { + requireUser(event) + const clientUserId = getRouterParam(event, 'id') + const formKey = getRouterParam(event, 'formKey') + + if (!clientUserId || !formKey) { + throw createError({ statusCode: 400, statusMessage: 'Missing params' }) + } + if (!event.context.isStaff) { + throw createError({ statusCode: 403, statusMessage: 'Staff only' }) + } + await assertStaffCanAccessClient(event, clientUserId) + + if (formKey !== 'application') { + throw createError({ statusCode: 400, statusMessage: 'Raw endpoint only supports application' }) + } + + const appForm = await prisma.appForm.findFirst({ + where: { userId: clientUserId }, + orderBy: { id: 'desc' }, + include: { questions: true }, + }) + + if (!appForm?.questions) return { answers: {} } + + const q = appForm.questions + const answers: Record = {} + for (let i = 1; i <= 50; i++) { + const key = `q${String(i).padStart(2, '0')}` as keyof typeof q + const val = q[key] + answers[key] = typeof val === 'string' ? val : '' + } + + return { answers } +}) \ No newline at end of file diff --git a/server/api/forms/pcl/save.post.ts b/server/api/forms/pcl/save.post.ts index 319e1d8..1def41a 100644 --- a/server/api/forms/pcl/save.post.ts +++ b/server/api/forms/pcl/save.post.ts @@ -42,7 +42,7 @@ export default defineEventHandler(async (event) => { let form = await prisma.pclForm.findFirst({ where: { userId }, - orderBy: { id: 'asc' }, + orderBy: { id: 'desc' }, }) if (!form) { @@ -102,8 +102,8 @@ export default defineEventHandler(async (event) => { await prisma.pclForm.update({ where: { id: form.id }, data: { - status: 'IN_PROGRESS', - submittedAt: null, + status: 'COMPLETE', + submittedAt: new Date(), totalScore, severity, }, diff --git a/server/api/forms/phq/save.post.ts b/server/api/forms/phq/save.post.ts index 9c2f9a4..94b484e 100644 --- a/server/api/forms/phq/save.post.ts +++ b/server/api/forms/phq/save.post.ts @@ -31,7 +31,7 @@ export default defineEventHandler(async (event) => { let form = await prisma.phqForm.findFirst({ where: { userId }, - orderBy: { id: 'asc' }, + orderBy: { id: 'desc' }, }) if (!form) { @@ -86,11 +86,12 @@ export default defineEventHandler(async (event) => { data, }) + // save score await prisma.phqForm.update({ where: { id: form.id }, data: { - status: 'IN_PROGRESS', - submittedAt: null, + status: 'COMPLETE', // ← was 'IN_PROGRESS' + submittedAt: new Date(), // ← was null totalScore, severity, }, diff --git a/server/api/session-notes/pending-approvals.get.ts b/server/api/session-notes/pending-approvals.get.ts index 7409519..20e2198 100644 --- a/server/api/session-notes/pending-approvals.get.ts +++ b/server/api/session-notes/pending-approvals.get.ts @@ -15,7 +15,7 @@ export default defineEventHandler(async (event) => { typeof query.kind === 'string' ? query.kind.toUpperCase() : '' const kindFilter = kindParam === 'PROGRESS' || kindParam === 'PSYCHOTHERAPY' - ? { kind: kindParam } + ? { kind: kindParam as 'PROGRESS' | 'PSYCHOTHERAPY' } : {} const rows = await prisma.sessionNote.findMany({ @@ -55,7 +55,7 @@ export default defineEventHandler(async (event) => { kind: r.kind, status: r.status, content: r.content, - attended: r.attended, + attendanceStatus: r.attendanceStatus, appointmentId: r.appointmentId, appointmentStartTime: r.appointment?.startTime?.toISOString() ?? null, clinicianSignedAt: r.clinicianSignedAt?.toISOString() ?? null, diff --git a/server/utils/clinical-form-display.ts b/server/utils/clinical-form-display.ts index 47e868a..4d68ce4 100644 --- a/server/utils/clinical-form-display.ts +++ b/server/utils/clinical-form-display.ts @@ -27,6 +27,29 @@ export const PHQ_LABELS = [ 'If you checked any problems, how difficult have they made it?', ] +export const PCL_LABELS = [ + 'Repeated, disturbing, and unwanted memories of the stressful experience?', + 'Repeated, disturbing dreams of the stressful experience?', + 'Suddenly feeling or acting as if the stressful experience were actually happening again?', + 'Feeling very upset when something reminded you of the stressful experience?', + 'Having strong physical reactions when something reminded you of the stressful experience?', + 'Avoiding memories, thoughts, or feelings related to the stressful experience?', + 'Avoiding external reminders of the stressful experience?', + 'Trouble remembering important parts of the stressful experience?', + 'Having strong negative beliefs about yourself, other people, or the world?', + 'Blaming yourself or someone else for the stressful experience or what happened after it?', + 'Having strong negative feelings such as fear, horror, anger, guilt, or shame?', + 'Loss of interest in activities that you used to enjoy?', + 'Feeling distant or cut off from other people?', + 'Trouble experiencing positive feelings?', + 'Irritable behavior, angry outbursts, or acting aggressively?', + 'Taking too many risks or doing things that could cause you harm?', + 'Being "superalert" or watchful or on guard?', + 'Feeling jumpy or easily startled?', + 'Having difficulty concentrating?', + 'Trouble falling or staying asleep?', +] + export const PHQ_OPTIONS: Record = { 0: 'Not at all', 1: 'Several days', @@ -41,6 +64,21 @@ export const GAD_OPTIONS: Record = { 3: 'Nearly every day', } +export const DIFFICULTY_OPTIONS: Record = { + 0: 'Not difficult at all', + 1: 'Somewhat difficult', + 2: 'Very difficult', + 3: 'Extremely difficult', +} + +export const PCL_OPTIONS: Record = { + 0: 'Not at all', + 1: 'A little bit', + 2: 'Moderately', + 3: 'Quite a bit', + 4: 'Extremely', +} + const ACE_QUESTIONS_TEXT = [ 'Did a parent or other adult in the household often swear at you, insult you, put you down, or humiliate you?', 'Did a parent or other adult in the household often push, grab, slap, or throw something at you?', @@ -96,7 +134,9 @@ export async function loadClinicalFormQuestions( const answers = [q.g01, q.g02, q.g03, q.g04, q.g05, q.g06, q.g07, q.g08] return GAD_LABELS.slice(0, answers.length).map((label, i) => ({ label, - answer: answers[i] != null ? (GAD_OPTIONS[answers[i] as number] ?? String(answers[i])) : '', + answer: answers[i] != null + ? ((i === 7 ? DIFFICULTY_OPTIONS : GAD_OPTIONS)[answers[i] as number] ?? String(answers[i])) + : '', })) } @@ -114,7 +154,9 @@ export async function loadClinicalFormQuestions( const answers = [q.q1, q.q2, q.q3, q.q4, q.q5, q.q6, q.q7, q.q8, q.q9, q.q10] return PHQ_LABELS.slice(0, answers.length).map((label, i) => ({ label, - answer: answers[i] != null ? (PHQ_OPTIONS[answers[i] as number] ?? String(answers[i])) : '', + answer: answers[i] != null + ? ((i === 9 ? DIFFICULTY_OPTIONS : PHQ_OPTIONS)[answers[i] as number] ?? String(answers[i])) + : '', })) } @@ -130,14 +172,14 @@ export async function loadClinicalFormQuestions( (await prisma.pclQuestion.findFirst({ where: { userId } })) } if (!q) return [] - const questions: ClinicalFormQuestionRow[] = [] + const questions: ClinicalFormQuestionRow[] = [ + { label: 'Worst event', answer: q.worstEvent ?? '' }, + ] for (let i = 1; i <= 20; i++) { const key = `q${String(i).padStart(2, '0')}` as keyof typeof q const val = q[key] const numVal = typeof val === 'number' ? val : null - if (numVal != null && numVal >= 0) { - questions.push({ label: `Item ${i}`, answer: String(numVal) }) - } + questions.push({ label: PCL_LABELS[i - 1] ?? `Item ${i}`, answer: numVal != null ? (PCL_OPTIONS[numVal] ?? String(numVal)) : '' }) } return questions }