+
{{ selectedSessionNoteRow.sessionName }}
{{ selectedNoteData.date }}
-
-
-
+
+
+
-
- Edit History
-
-
-
- Edited {{ new Date(edit.editedAt).toLocaleString('en-US') }}
-
-
Reason: {{ edit.reason }}
+
Edit History ({{ selectedNoteEdits.length }})
+
+
+
+
+
+ Edited {{ new Date(edit.editedAt).toLocaleString('en-US') }}
+
+
Reason: {{ edit.reason }}
+
-
+
+
{{ selectedAppointment?.sessionName }}
{{ currentNote.date }}
@@ -1544,17 +1673,40 @@
Psychotherapy (process) notes are stored separately and are not visible to the client.
-
+
+
+
Current
+
+
+ {{ new Date(selectedAppointment.startTime) > new Date() ? 'Upcoming' : 'Draft' }}
+
+
+ {{ currentNoteKind === 'PSYCHOTHERAPY' ? 'Psychotherapy note' : 'Progress note' }}
+
+
+
+
-
-
+
+
+
@@ -1565,6 +1717,7 @@
>
{{ currentNoteLockMessage }}
+
@@ -1597,7 +1750,7 @@
!isEditingPreviousPanel &&
(!selectedAppointmentId || !canEditCurrentNote || !canMarkAttendance)
"
- @click="showSaveModal = true"
+ @click="saveNote"
class="w-auto"
/>
@@ -1614,7 +1767,7 @@
-
- {{ formPreviewData.submitted ? 'Submitted' : 'Not submitted' }}
-
- ·
- {{
- new Date(
- formPreviewData.completedAt ?? formPreviewData.submittedAt ?? ''
- ).toLocaleString('en-US')
- }}
-
-
-
- Score: {{ formPreviewData.score }}
-
-
- {{ formPreviewData.severity }}
+
+ {{ formPreviewData.submitted ? 'Submitted' : 'Not submitted' }}
+
+ ·
+ {{
+ new Date(
+ formPreviewData.completedAt ?? formPreviewData.submittedAt ?? ''
+ ).toLocaleString('en-US')
+ }}
+
-
-
-
-
{{ q.label }}
-
-
- {{ q.answer || '—' }}
-
+
+
+
-
-
-
-
+
+
+
+
+ Score: {{ formPreviewData.score }}
+
+
+ {{ formPreviewData.severity }}
+
+
+
+
+
+
+
+
+
+
+ {{ label }}
+
+
+
+
+
+
{{ q.label }}
+
+ {{ selectedFormKey === 'application' ? formatAppAnswer(q.answer) : (q.answer || '—') }}
+
+
+
+
No answers yet.
@@ -1828,6 +1992,30 @@
+
+
+
+
+
+ Please mark whether the client attended this session before submitting the note.
+
+
+
+ Okay
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/app/components/NotesToolbar.vue b/app/components/NotesToolbar.vue
index 8b1d77a..1ff56cf 100644
--- a/app/components/NotesToolbar.vue
+++ b/app/components/NotesToolbar.vue
@@ -14,6 +14,11 @@ const emit = defineEmits<{
const localContent = ref(props.modelValue)
+watch(localContent, (val) => emit('update:modelValue', val))
+watch(() => props.modelValue, (val) => {
+ if (val !== localContent.value) localContent.value = val
+})
+
const items: EditorToolbarItem[][] = [
[
{
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/calendar.vue b/app/pages/calendar.vue
index f497898..191c7e0 100644
--- a/app/pages/calendar.vue
+++ b/app/pages/calendar.vue
@@ -17,6 +17,9 @@
| ({ id: string; role?: string } & Record
)
| null) ?? null
)
+ const deleteType = ref<'ONE' | 'FUTURE' | 'ALL' | null>(null)
+ const isDeleteTypeModalOpen = ref(false)
+ const isDeleteConfirmOpen = ref(false)
const { data: adminData, refresh: refreshAdminData } = await useFetch<{
isAdmin: boolean
isClinician: boolean
@@ -78,6 +81,7 @@
videoProvider: string | null
videoJoinUrl: string | null
assignedClinicianName: string | null
+ seriesId?: string | null
}
const { data: clinicians } = await useFetch('/api/clinicians', {
@@ -306,6 +310,32 @@
const isEditMode = ref(false)
const isDeleteConfirming = ref(false)
const mobileView = ref('week')
+ const currentRangeLabel = ref('')
+
+ function formatShortDate(date: Date) {
+ return date.toLocaleDateString('en-US')
+ }
+
+ function updateRangeLabel(viewType: string, start: Date) {
+ if (viewType.includes('Week')) {
+ currentRangeLabel.value = `Week of ${formatShortDate(start)}`
+ return
+ }
+
+ if (viewType.includes('Month')) {
+ currentRangeLabel.value = start.toLocaleDateString('en-US', {
+ month: 'long',
+ year: 'numeric',
+ })
+ return
+ }
+
+ currentRangeLabel.value = start.toLocaleDateString('en-US', {
+ month: 'long',
+ day: 'numeric',
+ year: 'numeric',
+ })
+ }
watch(mobileView, (view) => {
changeView(view)
@@ -372,6 +402,7 @@
datesSet(info: any) {
const view = info.view.type
+ updateRangeLabel(view, info.view.currentStart)
if (view.includes('Day')) mobileView.value = 'day'
if (view.includes('Week')) mobileView.value = 'week'
@@ -432,14 +463,14 @@
isEditMode.value = false
- // Get clientName from either extendedProps or _def.extendedProps
const clientName =
info.event.extendedProps?.clientName || info.event._def?.extendedProps?.clientName
const ext = info.event.extendedProps || info.event._def?.extendedProps || {}
+
selectedEvent.value = {
...ext,
- clientName: clientName, // Make sure clientName is included
+ clientName,
id: info.event.id,
clientId: ext.clientId,
title: info.event.title,
@@ -447,8 +478,12 @@
sessionNumber: ext.sessionNumber ?? null,
start: info.event.start,
end: info.event.end,
+
+ // merged fields
description: ext.description,
status: ext.status,
+ seriesId: ext.seriesId ?? null,
+
videoProvider: ext.videoProvider ?? null,
videoJoinUrl: ext.videoJoinUrl ?? null,
assignedClinicianName: ext.assignedClinicianName ?? null,
@@ -545,13 +580,40 @@
}
}
- async function deleteEvent() {
+ function onDeleteClick() {
+ // CLOSE the current modal first
+ isViewModalOpen.value = false
+
+ setTimeout(() => {
+ if (selectedEvent.value?.seriesId) {
+ isDeleteTypeModalOpen.value = true
+ } else {
+ deleteType.value = 'ONE'
+ isDeleteConfirmOpen.value = true
+ }
+ }, 100) // small delay so UI updates cleanly
+ }
+
+ function selectDeleteType(type: 'ONE' | 'FUTURE' | 'ALL') {
+ deleteType.value = type
+ isDeleteTypeModalOpen.value = false
+
+ setTimeout(() => {
+ isDeleteConfirmOpen.value = true
+ }, 100)
+ }
+
+ async function confirmDelete() {
const event = selectedEvent.value
if (!event) return
try {
- await $fetch(`/api/appointments/${event.id}`, {
+ await $fetch(`/api/appointments/${selectedEvent.value?.id}`, {
method: 'DELETE',
- credentials: 'include',
+ body: {
+ type: deleteType.value || 'ONE', // fallback safety
+ startTime: event.start.toISOString(),
+ seriesId: event.seriesId || null,
+ },
})
toast.add({
@@ -559,12 +621,14 @@
color: 'success',
})
- isDeleteConfirming.value = false
+ // reset state
+ deleteType.value = null
+ isDeleteConfirmOpen.value = false
isViewModalOpen.value = false
await loadEvents()
} catch (error) {
- console.error('Delete error:', error)
+ console.error(error)
toast.add({
title: 'Failed to delete session',
@@ -581,6 +645,7 @@
date: '',
startTime: '',
endTime: '',
+ recurrence: '',
includeVideo: false,
videoProvider: '' as '' | 'GOOGLE_MEET' | 'ZOOM' | 'OTHER',
videoJoinUrl: '',
@@ -670,6 +735,8 @@
date: form.date,
startTime: form.startTime,
endTime: form.endTime,
+ isRecurring: !!form.recurrence,
+ recurrence: form.recurrence || null,
videoProvider: form.includeVideo ? form.videoProvider || 'OTHER' : undefined,
videoJoinUrl: form.includeVideo ? form.videoJoinUrl.trim() : undefined,
},
@@ -726,6 +793,9 @@
+
+ {{ currentRangeLabel }}
+
@@ -876,7 +946,12 @@
-
+
+ Does not repeat
+ Daily
+ Weekly
+ Monthly
+
Cancel
@@ -891,6 +966,35 @@
+
+
+ Delete Recurring Session
+
+
+
+ This event only
+ This and future events
+ All events in series
+
+
+
+
+
+
+ Confirm Delete
+
+
+
+
Are you sure you want to delete this session?
+
+
+ Cancel
+
+ Delete
+
+
+
+
Meeting Link
{{ selectedEvent.videoJoinUrl }}
@@ -953,7 +1057,7 @@
:label="
selectedEvent?.videoProvider === 'GOOGLE_MEET'
? 'Join Google Meet'
- : `Join ${selectedEvent?.videoProvider ? VIDEO_PROVIDER_LABEL[selectedEvent.videoProvider] ?? 'meeting' : 'meeting'}`
+ : `Join ${selectedEvent?.videoProvider ? (VIDEO_PROVIDER_LABEL[selectedEvent.videoProvider] ?? 'meeting') : 'meeting'}`
"
/>
@@ -1031,27 +1135,13 @@
-
-
-
- Are you sure you want to delete this session?
-
-
- Cancel
- Delete
-
-
-
Delete
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/package-lock.json b/package-lock.json
index 8f4b126..c711f51 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,6 +26,7 @@
"marked": "^17.0.4",
"nodemailer": "^7.0.11",
"nuxt": "4.4.2",
+ "uuid": "^14.0.0",
"vue": "^3.5.25",
"vue-router": "^4.6.4",
"vue-signature-pad": "^3.0.2",
@@ -94,6 +95,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -467,6 +469,7 @@
"version": "1.4.18",
"resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.4.18.tgz",
"integrity": "sha512-q+awYgC7nkLEBdx2sW0iJjkzgSHlIxGnOpsN1r/O1+a4m7osJNHtfK2mKJSL1I+GfNyIlxJF8WvD/NLuYMpmcg==",
+ "peer": true,
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"zod": "^4.3.5"
@@ -496,12 +499,14 @@
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.0.tgz",
"integrity": "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@better-fetch/fetch": {
"version": "1.1.21",
"resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz",
- "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="
+ "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==",
+ "peer": true
},
"node_modules/@bomb.sh/tab": {
"version": "0.0.14",
@@ -646,7 +651,8 @@
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz",
"integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==",
"devOptional": true,
- "license": "Apache-2.0"
+ "license": "Apache-2.0",
+ "peer": true
},
"node_modules/@electric-sql/pglite-socket": {
"version": "0.0.20",
@@ -1132,6 +1138,7 @@
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@floating-ui/core": "^1.7.5",
"@floating-ui/utils": "^0.2.11"
@@ -1893,6 +1900,7 @@
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.4.2.tgz",
"integrity": "sha512-5+IPRNX2CjkBhuWUwz0hBuLqiaJPRoKzQ+SvcdrQDbAyE+VDeFt74VpSFr5/R0ujrK4b+XnSHUJWdS72w6hsog==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"c12": "^3.3.3",
"consola": "^3.4.2",
@@ -3707,6 +3715,7 @@
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.3.0.tgz",
"integrity": "sha512-FXBIxirqQfdC6b6HnNgxGmU7ydCPEPk7maHMOduJJfnTP+MuOGa15X4omjR/zpPUUpm8ef/mEFQjJudOGkXFcQ==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@prisma/client-runtime-utils": "7.3.0"
},
@@ -4855,6 +4864,7 @@
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.22.4.tgz",
"integrity": "sha512-vGIGm/HpqLg8EAAQXQ+koV+/S828OEpzocfWcPOwo1u2QUVf9dQG47Yy6JJ8zFFaJwfv4dBcOXli+7BrJwsxDQ==",
"license": "MIT",
+ "peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
@@ -4951,6 +4961,7 @@
"resolved": "https://registry.npmjs.org/@tiptap/extension-collaboration/-/extension-collaboration-3.20.5.tgz",
"integrity": "sha512-IalIm6BznHds2VzR4+6gMAgi4VXwZAXdYkl28EPZ8/xscBaUn1tCxcTBbCpmN3UhkABaCoJtmmER5TDy+x72Ag==",
"license": "MIT",
+ "peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
@@ -4980,6 +4991,7 @@
"resolved": "https://registry.npmjs.org/@tiptap/extension-drag-handle/-/extension-drag-handle-3.20.5.tgz",
"integrity": "sha512-D2W2fkpmXKRG4i0K0XVfbsTRYQMU9RudQIXBB7HnYZFosyJs3dvsT1cFurDUANKLPCxbI24eiubAKs6b1vzGpA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@floating-ui/dom": "^1.6.13"
},
@@ -5140,6 +5152,7 @@
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.22.4.tgz",
"integrity": "sha512-Xe8UFvvHmyp/c/TJsFwlwU9CWACYbBirNsluJ3U1+H8BTu1wqdrT/AXR5uIXeyCl5kiWKgX5q71eHWbYFOrqrg==",
"license": "MIT",
+ "peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
@@ -5195,6 +5208,7 @@
"resolved": "https://registry.npmjs.org/@tiptap/extension-node-range/-/extension-node-range-3.20.5.tgz",
"integrity": "sha512-6CbgZULF+dQ9KTothAORBZAXPdGneWicMWTV3Gyeh9gNySC18QsGQj3D2GxnllpMekmZXydtDbNFSoCiWjKFWQ==",
"license": "MIT",
+ "peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
@@ -5326,6 +5340,7 @@
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.20.5.tgz",
"integrity": "sha512-c4am6SznqfMnbUNSh4MvufiD7cMLdqL1BArok22uBgSWkS1sB9RVBYe8+x0jrOkk0UPEVlzDHbQ+nU+WmIyS2Q==",
"license": "MIT",
+ "peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
@@ -5357,6 +5372,7 @@
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.22.4.tgz",
"integrity": "sha512-hj8Qka6WcHRllHUdeSjDnq2XaisUo4KsoGJc1WcFpoa1Yd+OeD861zUMnV7DFVGdZRy45Obht0CUYJpXQ4yA4w==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"prosemirror-changeset": "^2.3.0",
"prosemirror-commands": "^1.6.2",
@@ -5417,6 +5433,7 @@
"resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-3.20.5.tgz",
"integrity": "sha512-5fqRNgnzYdJ1oDpyLqwrbVsZwvI+5VW/U89LPMvBYM7sFS7Xd0xfyxyAOWcJN4V0zLeTcuElWN3R+IUTLKbU+Q==",
"license": "MIT",
+ "peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
@@ -5431,6 +5448,7 @@
"resolved": "https://registry.npmjs.org/@tiptap/vue-3/-/vue-3-3.20.5.tgz",
"integrity": "sha512-5uUK3RAMNvUetZOv56Kz8nurhxHxMH60GgCCrVFgIBZoTc14u3d3v7EpcA6gNgzogutrR8GxvyFU3iIkj4kkHA==",
"license": "MIT",
+ "peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
@@ -5881,6 +5899,7 @@
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz",
"integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "14.2.1",
@@ -6006,6 +6025,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -6340,6 +6360,7 @@
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
"license": "Apache-2.0",
+ "peer": true,
"peerDependencies": {
"bare-abort-controller": "*"
},
@@ -6563,6 +6584,7 @@
"resolved": "https://registry.npmjs.org/better-call/-/better-call-1.1.8.tgz",
"integrity": "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@better-auth/utils": "^0.3.0",
"@better-fetch/fetch": "^1.1.4",
@@ -6708,6 +6730,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -6809,6 +6832,7 @@
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=8"
}
@@ -6926,6 +6950,7 @@
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"consola": "^3.2.3"
}
@@ -7676,7 +7701,8 @@
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/embla-carousel-auto-height": {
"version": "8.6.0",
@@ -8253,6 +8279,7 @@
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
"integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==",
"license": "Apache-2.0",
+ "peer": true,
"engines": {
"node": ">=10"
}
@@ -8506,6 +8533,7 @@
"integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=16.9.0"
}
@@ -8899,7 +8927,6 @@
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
"license": "MIT",
- "peer": true,
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
@@ -8934,6 +8961,7 @@
"resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
"integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==",
"license": "MIT",
+ "peer": true,
"funding": {
"url": "https://github.com/sponsors/panva"
}
@@ -8997,6 +9025,7 @@
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.10.tgz",
"integrity": "sha512-ksNxfzIW77OcZ+QWSAPC7yDqUSaIVwkTWnTPNiIy//vifNbwsSgQ57OkkncHxxpcBHM3LRfLAZVEh7kjq5twVA==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.0.0"
}
@@ -9058,7 +9087,6 @@
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz",
"integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"isomorphic.js": "^0.2.4"
},
@@ -9768,6 +9796,7 @@
"integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0",
@@ -9824,6 +9853,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": "^20.0.0 || >=22.0.0"
}
@@ -10160,6 +10190,7 @@
"resolved": "https://registry.npmjs.org/nuxt/-/nuxt-4.4.2.tgz",
"integrity": "sha512-iWVFpr/YEqVU/CenqIHMnIkvb2HE/9f+q8oxZ+pj2et+60NljGRClCgnmbvGPdmNFE0F1bEhoBCYfqbDOCim3Q==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@dxup/nuxt": "^0.4.0",
"@nuxt/cli": "^3.34.0",
@@ -10512,6 +10543,7 @@
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.117.0.tgz",
"integrity": "sha512-l3cbgK5wUvWDVNWM/JFU77qDdGZK1wudnLsFcrRyNo/bL1CyU8pC25vDhMHikVY29lbK2InTWsX42RxVSutUdQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@oxc-project/types": "^0.117.0"
},
@@ -10711,6 +10743,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -11199,7 +11232,6 @@
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==",
"license": "MIT",
- "peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
@@ -11237,6 +11269,7 @@
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -11345,6 +11378,7 @@
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@prisma/config": "7.3.0",
"@prisma/dev": "0.20.0",
@@ -11476,6 +11510,7 @@
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"orderedmap": "^2.0.0"
}
@@ -11496,6 +11531,7 @@
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0",
@@ -11529,6 +11565,7 @@
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.7.tgz",
"integrity": "sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"prosemirror-model": "^1.20.0",
"prosemirror-state": "^1.0.0",
@@ -11880,6 +11917,7 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz",
"integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -12166,8 +12204,7 @@
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"devOptional": true,
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/scule": {
"version": "1.3.0",
@@ -12735,6 +12772,7 @@
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
"integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==",
"license": "MIT",
+ "peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
@@ -12763,7 +12801,8 @@
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
"integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/tapable": {
"version": "2.3.2",
@@ -13009,6 +13048,7 @@
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
@@ -13477,12 +13517,26 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
+ "node_modules/uuid": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz",
+ "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist-node/bin/uuid"
+ }
+ },
"node_modules/valibot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz",
"integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"typescript": ">=5"
},
@@ -13605,6 +13659,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -13960,6 +14015,7 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.31.tgz",
"integrity": "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.31",
"@vue/compiler-sfc": "3.5.31",
@@ -14002,6 +14058,7 @@
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
"integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vue/devtools-api": "^6.6.4"
},
@@ -14200,6 +14257,7 @@
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"license": "ISC",
+ "peer": true,
"bin": {
"yaml": "bin.mjs"
},
@@ -14316,6 +14374,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
+ "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/package.json b/package.json
index 2fb442e..1462c61 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
"marked": "^17.0.4",
"nodemailer": "^7.0.11",
"nuxt": "4.4.2",
+ "uuid": "^14.0.0",
"vue": "^3.5.25",
"vue-router": "^4.6.4",
"vue-signature-pad": "^3.0.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d8cbda5..18bb813 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -71,6 +71,9 @@ importers:
nuxt:
specifier: 4.4.2
version: 4.4.2(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@electric-sql/pglite@0.3.15)(@parcel/watcher@2.5.6)(@types/node@25.5.0)(@vue/compiler-sfc@3.5.30)(better-sqlite3@12.8.0)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.3.15)(better-sqlite3@12.8.0)(mysql2@3.15.3))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.2)(mysql2@3.15.3)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)
+ uuid:
+ specifier: ^14.0.0
+ version: 14.0.0
vue:
specifier: ^3.5.25
version: 3.5.30(typescript@5.9.3)
@@ -854,56 +857,48 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@oxc-minify/binding-linux-arm64-musl@0.117.0':
resolution: {integrity: sha512-C3zapJconWpl2Y7LR3GkRkH6jxpuV2iVUfkFcHT5Ffn4Zu7l88mZa2dhcfdULZDybN1Phka/P34YUzuskUUrXw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@oxc-minify/binding-linux-ppc64-gnu@0.117.0':
resolution: {integrity: sha512-2T/Bm+3/qTfuNS4gKSzL8qbiYk+ErHW2122CtDx+ilZAzvWcJ8IbqdZIbEWOlwwe03lESTxPwTBLFqVgQU2OeQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
- libc: [glibc]
'@oxc-minify/binding-linux-riscv64-gnu@0.117.0':
resolution: {integrity: sha512-MKLjpldYkeoB4T+yAi4aIAb0waifxUjLcKkCUDmYAY3RqBJTvWK34KtfaKZL0IBMIXfD92CbKkcxQirDUS9Xcg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
- libc: [glibc]
'@oxc-minify/binding-linux-riscv64-musl@0.117.0':
resolution: {integrity: sha512-UFVcbPvKUStry6JffriobBp8BHtjmLLPl4bCY+JMxIn/Q3pykCpZzRwFTcDurG/kY8tm+uSNfKKdRNa5Nh9A7g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
- libc: [musl]
'@oxc-minify/binding-linux-s390x-gnu@0.117.0':
resolution: {integrity: sha512-B9GyPQ1NKbvpETVAMyJMfRlD3c6UJ7kiuFUAlx9LTYiQL+YIyT6vpuRlq1zgsXxavZluVrfeJv6x0owV4KDx4Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
- libc: [glibc]
'@oxc-minify/binding-linux-x64-gnu@0.117.0':
resolution: {integrity: sha512-fXfhtr+WWBGNy4M5GjAF5vu/lpulR4Me34FjTyaK9nDrTZs7LM595UDsP1wliksqp4hD/KdoqHGmbCrC+6d4vA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@oxc-minify/binding-linux-x64-musl@0.117.0':
resolution: {integrity: sha512-jFBgGbx1oLadb83ntJmy1dWlAHSQanXTS21G4PgkxyONmxZdZ/UMKr7KsADzMuoPsd2YhJHxzRpwJd9U+4BFBw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
- libc: [musl]
'@oxc-minify/binding-openharmony-arm64@0.117.0':
resolution: {integrity: sha512-nxPd9vx1vYz8IlIMdl9HFdOK/ood1H5hzbSFsyO8JU55tkcJoBL8TLCbuFf9pHpOy27l2gcPyV6z3p4eAcTH5Q==}
@@ -981,56 +976,48 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@oxc-parser/binding-linux-arm64-musl@0.117.0':
resolution: {integrity: sha512-QagKTDF4lrz8bCXbUi39Uq5xs7C7itAseKm51f33U+Dyar9eJY/zGKqfME9mKLOiahX7Fc1J3xMWVS0AdDXLPg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@oxc-parser/binding-linux-ppc64-gnu@0.117.0':
resolution: {integrity: sha512-RPddpcE/0xxWaommWy0c5i/JdrXcXAkxBS2GOrAUh5LKmyCh03hpJedOAWszG4ADsKQwoUQQ1/tZVGRhZIWtKA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
- libc: [glibc]
'@oxc-parser/binding-linux-riscv64-gnu@0.117.0':
resolution: {integrity: sha512-ur/WVZF9FSOiZGxyP+nfxZzuv6r5OJDYoVxJnUR7fM/hhXLh4V/be6rjbzm9KLCDBRwYCEKJtt+XXNccwd06IA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
- libc: [glibc]
'@oxc-parser/binding-linux-riscv64-musl@0.117.0':
resolution: {integrity: sha512-ujGcAx8xAMvhy7X5sBFi3GXML1EtyORuJZ5z2T6UV3U416WgDX/4OCi3GnoteeenvxIf6JgP45B+YTHpt71vpA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
- libc: [musl]
'@oxc-parser/binding-linux-s390x-gnu@0.117.0':
resolution: {integrity: sha512-hbsfKjUwRjcMZZvvmpZSc+qS0bHcHRu8aV/I3Ikn9BzOA0ZAgUE7ctPtce5zCU7bM8dnTLi4sJ1Pi9YHdx6Urw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
- libc: [glibc]
'@oxc-parser/binding-linux-x64-gnu@0.117.0':
resolution: {integrity: sha512-1QrTrf8rige7UPJrYuDKJLQOuJlgkt+nRSJLBMHWNm9TdivzP48HaK3f4q18EjNlglKtn03lgjMu4fryDm8X4A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@oxc-parser/binding-linux-x64-musl@0.117.0':
resolution: {integrity: sha512-gRvK6HPzF5ITRL68fqb2WYYs/hGviPIbkV84HWCgiJX+LkaOpp+HIHQl3zVZdyKHwopXToTbXbtx/oFjDjl8pg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
- libc: [musl]
'@oxc-parser/binding-openharmony-arm64@0.117.0':
resolution: {integrity: sha512-QPJvFbnnDZZY7xc+xpbIBWLThcGBakwaYA9vKV8b3+oS5MGfAZUoTFJcix5+Zg2Ri46sOfrUim6Y6jsKNcssAQ==}
@@ -1111,56 +1098,48 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@oxc-transform/binding-linux-arm64-musl@0.117.0':
resolution: {integrity: sha512-ykxpPQp0eAcSmhy0Y3qKvdanHY4d8THPonDfmCoktUXb6r0X6qnjpJB3V+taN1wevW55bOEZd97kxtjTKjqhmg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@oxc-transform/binding-linux-ppc64-gnu@0.117.0':
resolution: {integrity: sha512-Rvspti4Kr7eq6zSrURK5WjscfWQPvmy/KjJZV45neRKW8RLonE3r9+NgrwSLGoHvQ3F24fbqlkplox1RtlhH5A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
- libc: [glibc]
'@oxc-transform/binding-linux-riscv64-gnu@0.117.0':
resolution: {integrity: sha512-Dr2ZW9ZZ4l1eQ5JUEUY3smBh4JFPCPuybWaDZTLn3ADZjyd8ZtNXEjeMT8rQbbhbgSL9hEgbwaqraole3FNThQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
- libc: [glibc]
'@oxc-transform/binding-linux-riscv64-musl@0.117.0':
resolution: {integrity: sha512-oD1Bnes1bIC3LVBSrWEoSUBj6fvatESPwAVWfJVGVQlqWuOs/ZBn1e4Nmbipo3KGPHK7DJY75r/j7CQCxhrOFQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
- libc: [musl]
'@oxc-transform/binding-linux-s390x-gnu@0.117.0':
resolution: {integrity: sha512-qT//IAPLvse844t99Kff5j055qEbXfwzWgvCMb0FyjisnB8foy25iHZxZIocNBe6qwrCYWUP1M8rNrB/WyfS1Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
- libc: [glibc]
'@oxc-transform/binding-linux-x64-gnu@0.117.0':
resolution: {integrity: sha512-2YEO5X+KgNzFqRVO5dAkhjcI5gwxus4NSWVl/+cs2sI6P0MNPjqE3VWPawl4RTC11LvetiiZdHcujUCPM8aaUw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@oxc-transform/binding-linux-x64-musl@0.117.0':
resolution: {integrity: sha512-3wqWbTSaIFZvDr1aqmTul4cg8PRWYh6VC52E8bLI7ytgS/BwJLW+sDUU2YaGIds4sAf/1yKeJRmudRCDPW9INg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
- libc: [musl]
'@oxc-transform/binding-openharmony-arm64@0.117.0':
resolution: {integrity: sha512-Ebxx6NPqhzlrjvx4+PdSqbOq+li0f7X59XtJljDghkbJsbnkHvhLmPR09ifHt5X32UlZN63ekjwcg/nbmHLLlA==}
@@ -1220,42 +1199,36 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
- libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.6':
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
- libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.6':
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.6':
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.6':
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.6':
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
- libc: [musl]
'@parcel/watcher-wasm@2.5.6':
resolution: {integrity: sha512-byAiBZ1t3tXQvc8dMD/eoyE7lTXYorhn+6uVW5AC+JGI1KtJC/LvDche5cfUE+qiefH+Ybq0bUCJU0aB1cSHUA==}
@@ -1475,79 +1448,66 @@ packages:
resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==}
cpu: [arm]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.60.0':
resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==}
cpu: [arm]
os: [linux]
- libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.60.0':
resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.60.0':
resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.60.0':
resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==}
cpu: [loong64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.60.0':
resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==}
cpu: [loong64]
os: [linux]
- libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.60.0':
resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==}
cpu: [ppc64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.60.0':
resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==}
cpu: [ppc64]
os: [linux]
- libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.60.0':
resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==}
cpu: [riscv64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.60.0':
resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==}
cpu: [riscv64]
os: [linux]
- libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.60.0':
resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==}
cpu: [s390x]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.60.0':
resolution: {integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.60.0':
resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==}
cpu: [x64]
os: [linux]
- libc: [musl]
'@rollup/rollup-openbsd-x64@4.60.0':
resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==}
@@ -1634,28 +1594,24 @@ packages:
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.2.2':
resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.2.2':
resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.2.2':
resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
- libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.2.2':
resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==}
@@ -3292,28 +3248,24 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
- libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
- libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
- libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
- libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
@@ -4699,6 +4651,10 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+ uuid@14.0.0:
+ resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==}
+ hasBin: true
+
valibot@1.2.0:
resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==}
peerDependencies:
@@ -9856,6 +9812,8 @@ snapshots:
util-deprecate@1.0.2: {}
+ uuid@14.0.0: {}
+
valibot@1.2.0(typescript@5.9.3):
optionalDependencies:
typescript: 5.9.3
diff --git a/prisma/migrations/20260129194131_init/migration.sql b/prisma/migrations/20260129194131_init/migration.sql
new file mode 100644
index 0000000..b547781
--- /dev/null
+++ b/prisma/migrations/20260129194131_init/migration.sql
@@ -0,0 +1,66 @@
+-- CreateTable
+CREATE TABLE "user" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "name" TEXT NOT NULL,
+ "email" TEXT NOT NULL,
+ "emailVerified" BOOLEAN NOT NULL DEFAULT false,
+ "image" TEXT,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL
+);
+
+-- CreateTable
+CREATE TABLE "session" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "expiresAt" DATETIME NOT NULL,
+ "token" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL,
+ "ipAddress" TEXT,
+ "userAgent" TEXT,
+ "userId" TEXT NOT NULL,
+ CONSTRAINT "session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "account" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "accountId" TEXT NOT NULL,
+ "providerId" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "accessToken" TEXT,
+ "refreshToken" TEXT,
+ "idToken" TEXT,
+ "accessTokenExpiresAt" DATETIME,
+ "refreshTokenExpiresAt" DATETIME,
+ "scope" TEXT,
+ "password" TEXT,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL,
+ CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "verification" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "identifier" TEXT NOT NULL,
+ "value" TEXT NOT NULL,
+ "expiresAt" DATETIME NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "user_email_key" ON "user"("email");
+
+-- CreateIndex
+CREATE INDEX "session_userId_idx" ON "session"("userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "session_token_key" ON "session"("token");
+
+-- CreateIndex
+CREATE INDEX "account_userId_idx" ON "account"("userId");
+
+-- CreateIndex
+CREATE INDEX "verification_identifier_idx" ON "verification"("identifier");
diff --git a/prisma/migrations/20260217005051_add_gad/migration.sql b/prisma/migrations/20260217005051_add_gad/migration.sql
new file mode 100644
index 0000000..278fd8e
--- /dev/null
+++ b/prisma/migrations/20260217005051_add_gad/migration.sql
@@ -0,0 +1,36 @@
+-- CreateTable
+CREATE TABLE "GadForm" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "userId" TEXT NOT NULL,
+ "status" TEXT NOT NULL DEFAULT 'IN_PROGRESS',
+ "totalScore" INTEGER,
+ "severity" TEXT,
+ "submittedAt" DATETIME,
+ CONSTRAINT "GadForm_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "GadQuestion" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "formId" INTEGER NOT NULL,
+ "userId" TEXT NOT NULL,
+ "g01" INTEGER,
+ "g02" INTEGER,
+ "g03" INTEGER,
+ "g04" INTEGER,
+ "g05" INTEGER,
+ "g06" INTEGER,
+ "g07" INTEGER,
+ "g08" INTEGER,
+ CONSTRAINT "GadQuestion_formId_fkey" FOREIGN KEY ("formId") REFERENCES "GadForm" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT "GadQuestion_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateIndex
+CREATE INDEX "GadForm_userId_idx" ON "GadForm"("userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "GadQuestion_formId_key" ON "GadQuestion"("formId");
+
+-- CreateIndex
+CREATE INDEX "GadQuestion_userId_idx" ON "GadQuestion"("userId");
diff --git a/prisma/migrations/20260226040142/migration.sql b/prisma/migrations/20260226040142/migration.sql
new file mode 100644
index 0000000..38b3e29
--- /dev/null
+++ b/prisma/migrations/20260226040142/migration.sql
@@ -0,0 +1,123 @@
+/*
+ Warnings:
+
+ - You are about to drop the `GadForm` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `GadQuestion` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the column `image` on the `user` table. All the data in the column will be lost.
+
+*/
+-- DropIndex
+DROP INDEX "GadForm_userId_idx";
+
+-- DropIndex
+DROP INDEX "GadQuestion_userId_idx";
+
+-- DropIndex
+DROP INDEX "GadQuestion_formId_key";
+
+-- DropTable
+PRAGMA foreign_keys=off;
+DROP TABLE "GadForm";
+PRAGMA foreign_keys=on;
+
+-- DropTable
+PRAGMA foreign_keys=off;
+DROP TABLE "GadQuestion";
+PRAGMA foreign_keys=on;
+
+-- CreateTable
+CREATE TABLE "AppForm" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "userId" TEXT NOT NULL,
+ "status" TEXT NOT NULL DEFAULT 'IN_PROGRESS',
+ "submittedAt" DATETIME,
+ CONSTRAINT "AppForm_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "AppQuestion" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "formId" INTEGER NOT NULL,
+ "userId" TEXT NOT NULL,
+ "q01" TEXT,
+ "q02" TEXT,
+ "q03" TEXT,
+ "q04" TEXT,
+ "q05" TEXT,
+ "q06" TEXT,
+ "q07" TEXT,
+ "q08" TEXT,
+ "q09" TEXT,
+ "q10" TEXT,
+ "q11" TEXT,
+ "q12" TEXT,
+ "q13" TEXT,
+ "q14" TEXT,
+ "q15" TEXT,
+ "q16" TEXT,
+ "q17" TEXT,
+ "q18" TEXT,
+ "q19" TEXT,
+ "q20" TEXT,
+ "q21" TEXT,
+ "q22" TEXT,
+ "q23" TEXT,
+ "q24" TEXT,
+ "q25" TEXT,
+ "q26" TEXT,
+ "q27" TEXT,
+ "q28" TEXT,
+ "q29" TEXT,
+ "q30" TEXT,
+ "q31" TEXT,
+ "q32" TEXT,
+ "q33" TEXT,
+ "q34" TEXT,
+ "q35" TEXT,
+ "q36" TEXT,
+ "q37" TEXT,
+ "q38" TEXT,
+ "q39" TEXT,
+ "q40" TEXT,
+ "q41" TEXT,
+ "q42" TEXT,
+ "q43" TEXT,
+ "q44" TEXT,
+ "q45" TEXT,
+ "q46" TEXT,
+ "q47" TEXT,
+ "q48" TEXT,
+ "q49" TEXT,
+ "q50" TEXT,
+ CONSTRAINT "AppQuestion_formId_fkey" FOREIGN KEY ("formId") REFERENCES "AppForm" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT "AppQuestion_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- RedefineTables
+PRAGMA defer_foreign_keys=ON;
+PRAGMA foreign_keys=OFF;
+CREATE TABLE "new_user" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "name" TEXT NOT NULL,
+ "email" TEXT NOT NULL,
+ "emailVerified" BOOLEAN NOT NULL DEFAULT false,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL,
+ "role" TEXT NOT NULL DEFAULT 'CLIENT',
+ "phoneNumber" INTEGER
+);
+INSERT INTO "new_user" ("createdAt", "email", "emailVerified", "id", "name", "updatedAt") SELECT "createdAt", "email", "emailVerified", "id", "name", "updatedAt" FROM "user";
+DROP TABLE "user";
+ALTER TABLE "new_user" RENAME TO "user";
+CREATE UNIQUE INDEX "user_email_key" ON "user"("email");
+PRAGMA foreign_keys=ON;
+PRAGMA defer_foreign_keys=OFF;
+
+-- CreateIndex
+CREATE UNIQUE INDEX "AppForm_userId_key" ON "AppForm"("userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "AppQuestion_formId_key" ON "AppQuestion"("formId");
+
+-- CreateIndex
+CREATE INDEX "AppQuestion_userId_idx" ON "AppQuestion"("userId");
diff --git a/prisma/migrations/20260226042411_add_image/migration.sql b/prisma/migrations/20260226042411_add_image/migration.sql
new file mode 100644
index 0000000..b34507c
--- /dev/null
+++ b/prisma/migrations/20260226042411_add_image/migration.sql
@@ -0,0 +1,203 @@
+-- AlterTable
+ALTER TABLE "user" ADD COLUMN "image" TEXT;
+
+-- CreateTable
+CREATE TABLE "form" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "title" TEXT NOT NULL,
+ "description" TEXT,
+ "slug" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL
+);
+
+-- CreateTable
+CREATE TABLE "question" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "text" TEXT NOT NULL,
+ "type" TEXT NOT NULL,
+ "alias" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL
+);
+
+-- CreateTable
+CREATE TABLE "form_question" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "formId" TEXT NOT NULL,
+ "questionId" TEXT NOT NULL,
+ "order" INTEGER NOT NULL,
+ CONSTRAINT "form_question_formId_fkey" FOREIGN KEY ("formId") REFERENCES "form" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT "form_question_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "question" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "form_assignment" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "userId" TEXT NOT NULL,
+ "formId" TEXT NOT NULL,
+ "assignedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "completedAt" DATETIME,
+ CONSTRAINT "form_assignment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT "form_assignment_formId_fkey" FOREIGN KEY ("formId") REFERENCES "form" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "ace_response" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "userId" TEXT NOT NULL,
+ "responses" TEXT NOT NULL,
+ "completedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT "ace_response_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "GadForm" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "userId" TEXT NOT NULL,
+ "status" TEXT NOT NULL DEFAULT 'IN_PROGRESS',
+ "totalScore" INTEGER,
+ "severity" TEXT,
+ "submittedAt" DATETIME,
+ CONSTRAINT "GadForm_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "GadQuestion" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "formId" INTEGER NOT NULL,
+ "userId" TEXT NOT NULL,
+ "g01" INTEGER,
+ "g02" INTEGER,
+ "g03" INTEGER,
+ "g04" INTEGER,
+ "g05" INTEGER,
+ "g06" INTEGER,
+ "g07" INTEGER,
+ "g08" INTEGER,
+ CONSTRAINT "GadQuestion_formId_fkey" FOREIGN KEY ("formId") REFERENCES "GadForm" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT "GadQuestion_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "PhqForm" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "userId" TEXT NOT NULL,
+ "status" TEXT NOT NULL DEFAULT 'IN_PROGRESS',
+ "totalScore" INTEGER,
+ "submittedAt" DATETIME,
+ CONSTRAINT "PhqForm_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "PhqQuestion" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "formId" INTEGER NOT NULL,
+ "userId" TEXT NOT NULL,
+ "q1" INTEGER,
+ "q2" INTEGER,
+ "q3" INTEGER,
+ "q4" INTEGER,
+ "q5" INTEGER,
+ "q6" INTEGER,
+ "q7" INTEGER,
+ "q8" INTEGER,
+ "q9" INTEGER,
+ CONSTRAINT "PhqQuestion_formId_fkey" FOREIGN KEY ("formId") REFERENCES "PhqForm" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT "PhqQuestion_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "PclForm" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "userId" TEXT NOT NULL,
+ "status" TEXT NOT NULL DEFAULT 'IN_PROGRESS',
+ "submittedAt" DATETIME,
+ CONSTRAINT "PclForm_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "PclQuestion" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "formId" INTEGER NOT NULL,
+ "userId" TEXT NOT NULL,
+ "q01" INTEGER,
+ "q02" INTEGER,
+ "q03" INTEGER,
+ "q04" INTEGER,
+ "q05" INTEGER,
+ "q06" INTEGER,
+ "q07" INTEGER,
+ "q08" INTEGER,
+ "q09" INTEGER,
+ "q10" INTEGER,
+ "q11" INTEGER,
+ "q12" INTEGER,
+ "q13" INTEGER,
+ "q14" INTEGER,
+ "q15" INTEGER,
+ "q16" INTEGER,
+ "q17" INTEGER,
+ "q18" INTEGER,
+ "q19" INTEGER,
+ "q20" INTEGER,
+ CONSTRAINT "PclQuestion_formId_fkey" FOREIGN KEY ("formId") REFERENCES "PclForm" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT "PclQuestion_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "form_slug_key" ON "form"("slug");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "question_alias_key" ON "question"("alias");
+
+-- CreateIndex
+CREATE INDEX "form_question_formId_idx" ON "form_question"("formId");
+
+-- CreateIndex
+CREATE INDEX "form_question_questionId_idx" ON "form_question"("questionId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "form_question_formId_questionId_key" ON "form_question"("formId", "questionId");
+
+-- CreateIndex
+CREATE INDEX "form_assignment_userId_idx" ON "form_assignment"("userId");
+
+-- CreateIndex
+CREATE INDEX "form_assignment_formId_idx" ON "form_assignment"("formId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "form_assignment_userId_formId_key" ON "form_assignment"("userId", "formId");
+
+-- CreateIndex
+CREATE INDEX "ace_response_userId_idx" ON "ace_response"("userId");
+
+-- CreateIndex
+CREATE INDEX "GadForm_userId_idx" ON "GadForm"("userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "GadQuestion_formId_key" ON "GadQuestion"("formId");
+
+-- CreateIndex
+CREATE INDEX "GadQuestion_userId_idx" ON "GadQuestion"("userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "PhqForm_userId_key" ON "PhqForm"("userId");
+
+-- CreateIndex
+CREATE INDEX "PhqForm_userId_idx" ON "PhqForm"("userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "PhqQuestion_formId_key" ON "PhqQuestion"("formId");
+
+-- CreateIndex
+CREATE INDEX "PhqQuestion_userId_idx" ON "PhqQuestion"("userId");
+
+-- CreateIndex
+CREATE INDEX "PclForm_userId_idx" ON "PclForm"("userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "PclQuestion_formId_key" ON "PclQuestion"("formId");
+
+-- CreateIndex
+CREATE INDEX "PclQuestion_userId_idx" ON "PclQuestion"("userId");
diff --git a/prisma/migrations/20260226060301/migration.sql b/prisma/migrations/20260226060301/migration.sql
new file mode 100644
index 0000000..d40952a
--- /dev/null
+++ b/prisma/migrations/20260226060301/migration.sql
@@ -0,0 +1,14 @@
+-- CreateTable
+CREATE TABLE "client" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "userId" TEXT NOT NULL,
+ "status" TEXT NOT NULL DEFAULT 'INCOMPLETE',
+ "therapyWeek" INTEGER,
+ CONSTRAINT "client_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "client_userId_key" ON "client"("userId");
+
+-- CreateIndex
+CREATE INDEX "client_status_idx" ON "client"("status");
diff --git a/prisma/migrations/20260226072412_appointments/migration.sql b/prisma/migrations/20260226072412_appointments/migration.sql
new file mode 100644
index 0000000..e8d585f
--- /dev/null
+++ b/prisma/migrations/20260226072412_appointments/migration.sql
@@ -0,0 +1,26 @@
+/*
+ Warnings:
+
+ - You are about to drop the `client` table. If the table is not empty, all the data it contains will be lost.
+
+*/
+-- DropTable
+PRAGMA foreign_keys=off;
+DROP TABLE "client";
+PRAGMA foreign_keys=on;
+
+-- CreateTable
+CREATE TABLE "Appointment" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "clientId" TEXT NOT NULL,
+ "adminId" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "description" TEXT,
+ "startTime" DATETIME NOT NULL,
+ "endTime" DATETIME NOT NULL,
+ "status" TEXT NOT NULL DEFAULT 'SCHEDULED',
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL,
+ CONSTRAINT "Appointment_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "user" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
+ CONSTRAINT "Appointment_adminId_fkey" FOREIGN KEY ("adminId") REFERENCES "user" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
+);
diff --git a/prisma/migrations/20260409184455_add_recurring_fields/migration.sql b/prisma/migrations/20260409184455_add_recurring_fields/migration.sql
new file mode 100644
index 0000000..c2580e5
--- /dev/null
+++ b/prisma/migrations/20260409184455_add_recurring_fields/migration.sql
@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "Appointment" ADD COLUMN "recurrence" TEXT;
+ALTER TABLE "Appointment" ADD COLUMN "seriesId" TEXT;
diff --git a/prisma/migrations/20260409203356/migration.sql b/prisma/migrations/20260409203356/migration.sql
new file mode 100644
index 0000000..ffdd9af
--- /dev/null
+++ b/prisma/migrations/20260409203356/migration.sql
@@ -0,0 +1,310 @@
+/*
+ Warnings:
+
+ - You are about to drop the `ace_response` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `form` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `form_assignment` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `form_question` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `question` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the column `recurrence` on the `Appointment` table. All the data in the column will be lost.
+ - You are about to drop the column `seriesId` on the `Appointment` table. All the data in the column will be lost.
+
+*/
+-- DropIndex
+DROP INDEX "ace_response_userId_idx";
+
+-- DropIndex
+DROP INDEX "form_slug_key";
+
+-- DropIndex
+DROP INDEX "form_assignment_userId_formId_key";
+
+-- DropIndex
+DROP INDEX "form_assignment_formId_idx";
+
+-- DropIndex
+DROP INDEX "form_assignment_userId_idx";
+
+-- DropIndex
+DROP INDEX "form_question_formId_questionId_key";
+
+-- DropIndex
+DROP INDEX "form_question_questionId_idx";
+
+-- DropIndex
+DROP INDEX "form_question_formId_idx";
+
+-- DropIndex
+DROP INDEX "question_alias_key";
+
+-- AlterTable
+ALTER TABLE "PclForm" ADD COLUMN "severity" TEXT;
+ALTER TABLE "PclForm" ADD COLUMN "totalScore" INTEGER;
+
+-- AlterTable
+ALTER TABLE "PclQuestion" ADD COLUMN "worstEvent" TEXT;
+
+-- AlterTable
+ALTER TABLE "PhqForm" ADD COLUMN "severity" TEXT;
+
+-- AlterTable
+ALTER TABLE "PhqQuestion" ADD COLUMN "q10" INTEGER;
+
+-- DropTable
+PRAGMA foreign_keys=off;
+DROP TABLE "ace_response";
+PRAGMA foreign_keys=on;
+
+-- DropTable
+PRAGMA foreign_keys=off;
+DROP TABLE "form";
+PRAGMA foreign_keys=on;
+
+-- DropTable
+PRAGMA foreign_keys=off;
+DROP TABLE "form_assignment";
+PRAGMA foreign_keys=on;
+
+-- DropTable
+PRAGMA foreign_keys=off;
+DROP TABLE "form_question";
+PRAGMA foreign_keys=on;
+
+-- DropTable
+PRAGMA foreign_keys=off;
+DROP TABLE "question";
+PRAGMA foreign_keys=on;
+
+-- CreateTable
+CREATE TABLE "client" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "userId" TEXT NOT NULL,
+ "status" TEXT NOT NULL DEFAULT 'INCOMPLETE',
+ "therapyWeek" INTEGER,
+ "missedSessions" INTEGER NOT NULL DEFAULT 0,
+ CONSTRAINT "client_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "client_permission" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "clientId" TEXT NOT NULL,
+ "canViewScores" BOOLEAN NOT NULL DEFAULT false,
+ "canViewNotes" BOOLEAN NOT NULL DEFAULT false,
+ "canViewPlan" BOOLEAN NOT NULL DEFAULT false,
+ CONSTRAINT "client_permission_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "client" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "client_plan" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "clientId" TEXT NOT NULL,
+ "content" TEXT NOT NULL DEFAULT '',
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL,
+ CONSTRAINT "client_plan_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "client" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "change_audit" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "entityType" TEXT NOT NULL,
+ "entityId" TEXT NOT NULL,
+ "oldValue" TEXT,
+ "newValue" TEXT,
+ "reasoning" TEXT,
+ "documentationPath" TEXT,
+ "documentationName" TEXT,
+ "signatureData" TEXT NOT NULL,
+ "signedById" TEXT NOT NULL,
+ "signedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT "change_audit_signedById_fkey" FOREIGN KEY ("signedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "ace_form" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "userId" TEXT NOT NULL,
+ "status" TEXT NOT NULL DEFAULT 'IN_PROGRESS',
+ "totalScore" INTEGER,
+ "severity" TEXT,
+ "submittedAt" DATETIME,
+ CONSTRAINT "ace_form_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "ace_question" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "formId" INTEGER NOT NULL,
+ "userId" TEXT NOT NULL,
+ "a01" TEXT,
+ "a02" TEXT,
+ "a03" TEXT,
+ "a04" TEXT,
+ "a05" TEXT,
+ "a06" TEXT,
+ "a07" TEXT,
+ "a08" TEXT,
+ "a09" TEXT,
+ "a10" TEXT,
+ CONSTRAINT "ace_question_formId_fkey" FOREIGN KEY ("formId") REFERENCES "ace_form" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT "ace_question_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "PhysicianStatementForm" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "userId" TEXT NOT NULL,
+ "status" TEXT NOT NULL DEFAULT 'NOT_SUBMITTED',
+ "originalFileName" TEXT,
+ "storedFileName" TEXT,
+ "mimeType" TEXT,
+ "uploadedAt" DATETIME,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL,
+ CONSTRAINT "PhysicianStatementForm_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "ReleaseOfInformationAuthorizationForm" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "userId" TEXT NOT NULL,
+ "status" TEXT NOT NULL DEFAULT 'NOT_SUBMITTED',
+ "originalFileName" TEXT,
+ "storedFileName" TEXT,
+ "mimeType" TEXT,
+ "uploadedAt" DATETIME,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL,
+ CONSTRAINT "ReleaseOfInformationAuthorizationForm_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "session_note" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "clientId" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "attended" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT "session_note_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "client" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "session_note_edit" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "sessionNoteId" TEXT NOT NULL,
+ "originalContent" TEXT,
+ "reason" TEXT,
+ "signature" TEXT,
+ "editedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT "session_note_edit_sessionNoteId_fkey" FOREIGN KEY ("sessionNoteId") REFERENCES "session_note" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "declaration_template" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "requestKind" TEXT NOT NULL,
+ "version" INTEGER NOT NULL,
+ "content" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+-- CreateTable
+CREATE TABLE "session_notes_request" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "clientId" TEXT NOT NULL,
+ "requestKind" TEXT NOT NULL,
+ "status" TEXT NOT NULL DEFAULT 'PENDING',
+ "signatureData" TEXT NOT NULL,
+ "declarationTemplateId" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "decidedAt" DATETIME,
+ "decidedByUserId" TEXT,
+ "rejectionReason" TEXT,
+ "approvedSummaryText" TEXT,
+ CONSTRAINT "session_notes_request_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "client" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT "session_notes_request_declarationTemplateId_fkey" FOREIGN KEY ("declarationTemplateId") REFERENCES "declaration_template" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
+ CONSTRAINT "session_notes_request_decidedByUserId_fkey" FOREIGN KEY ("decidedByUserId") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE CASCADE
+);
+
+-- RedefineTables
+PRAGMA defer_foreign_keys=ON;
+PRAGMA foreign_keys=OFF;
+CREATE TABLE "new_Appointment" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "clientId" TEXT NOT NULL,
+ "adminId" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "description" TEXT,
+ "startTime" DATETIME NOT NULL,
+ "endTime" DATETIME NOT NULL,
+ "status" TEXT NOT NULL DEFAULT 'SCHEDULED',
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL,
+ CONSTRAINT "Appointment_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "user" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
+ CONSTRAINT "Appointment_adminId_fkey" FOREIGN KEY ("adminId") REFERENCES "user" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
+);
+INSERT INTO "new_Appointment" ("adminId", "clientId", "createdAt", "description", "endTime", "id", "startTime", "status", "title", "updatedAt") SELECT "adminId", "clientId", "createdAt", "description", "endTime", "id", "startTime", "status", "title", "updatedAt" FROM "Appointment";
+DROP TABLE "Appointment";
+ALTER TABLE "new_Appointment" RENAME TO "Appointment";
+CREATE INDEX "Appointment_clientId_idx" ON "Appointment"("clientId");
+CREATE INDEX "Appointment_adminId_idx" ON "Appointment"("adminId");
+PRAGMA foreign_keys=ON;
+PRAGMA defer_foreign_keys=OFF;
+
+-- CreateIndex
+CREATE UNIQUE INDEX "client_userId_key" ON "client"("userId");
+
+-- CreateIndex
+CREATE INDEX "client_status_idx" ON "client"("status");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "client_permission_clientId_key" ON "client_permission"("clientId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "client_plan_clientId_key" ON "client_plan"("clientId");
+
+-- CreateIndex
+CREATE INDEX "change_audit_entityType_entityId_idx" ON "change_audit"("entityType", "entityId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "ace_form_userId_key" ON "ace_form"("userId");
+
+-- CreateIndex
+CREATE INDEX "ace_form_userId_idx" ON "ace_form"("userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "ace_question_formId_key" ON "ace_question"("formId");
+
+-- CreateIndex
+CREATE INDEX "ace_question_userId_idx" ON "ace_question"("userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "PhysicianStatementForm_userId_key" ON "PhysicianStatementForm"("userId");
+
+-- CreateIndex
+CREATE INDEX "PhysicianStatementForm_status_idx" ON "PhysicianStatementForm"("status");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "ReleaseOfInformationAuthorizationForm_userId_key" ON "ReleaseOfInformationAuthorizationForm"("userId");
+
+-- CreateIndex
+CREATE INDEX "ReleaseOfInformationAuthorizationForm_status_idx" ON "ReleaseOfInformationAuthorizationForm"("status");
+
+-- CreateIndex
+CREATE INDEX "session_note_clientId_idx" ON "session_note"("clientId");
+
+-- CreateIndex
+CREATE INDEX "session_note_edit_sessionNoteId_idx" ON "session_note_edit"("sessionNoteId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "declaration_template_requestKind_version_key" ON "declaration_template"("requestKind", "version");
+
+-- CreateIndex
+CREATE INDEX "session_notes_request_clientId_idx" ON "session_notes_request"("clientId");
+
+-- CreateIndex
+CREATE INDEX "session_notes_request_status_idx" ON "session_notes_request"("status");
+
+-- CreateIndex
+CREATE INDEX "session_notes_request_declarationTemplateId_idx" ON "session_notes_request"("declarationTemplateId");
diff --git a/prisma/schema/appointments.prisma b/prisma/schema/appointments.prisma
index e01aa7d..01f34b6 100644
--- a/prisma/schema/appointments.prisma
+++ b/prisma/schema/appointments.prisma
@@ -6,23 +6,33 @@ enum VideoConferenceProvider {
}
model Appointment {
- id String @id @default(cuid())
- clientId String
- adminId String
- title String
+ id String @id @default(cuid())
+
+ clientId String
+ adminId String
+
+ title String
sessionName String
sessionNumber Int
- description String?
- startTime DateTime
- endTime DateTime
- status String @default("SCHEDULED")
- /** When set with videoJoinUrl, describes how the client joins the call. */
+
+ description String?
+ startTime DateTime
+ endTime DateTime
+
+ status String @default("SCHEDULED")
+
+ seriesId String?
+ recurrence String
+
videoProvider VideoConferenceProvider?
videoJoinUrl String?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- client User @relation("ClientAppointments", fields: [clientId], references: [id], onDelete: Restrict)
- admin User @relation("AdminAppointments", fields: [adminId], references: [id], onDelete: Restrict)
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ client User @relation("ClientAppointments", fields: [clientId], references: [id], onDelete: Restrict)
+ admin User @relation("AdminAppointments", fields: [adminId], references: [id], onDelete: Restrict)
+
sessionNotes SessionNote[] @relation("AppointmentSessionNotes")
@@index([clientId])
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/[id].delete.ts b/server/api/appointments/[id].delete.ts
index f16d2bf..03fc703 100644
--- a/server/api/appointments/[id].delete.ts
+++ b/server/api/appointments/[id].delete.ts
@@ -1,64 +1,67 @@
import { requireStaff } from '../../utils/guard'
import { assertStaffCanAccessClient } from '../../utils/clinician-access'
import { prisma } from '../../utils/prisma'
-import { createError, defineEventHandler, getHeaders, getRouterParam } from 'h3'
+import { createError, defineEventHandler, getRouterParam, readBody } from 'h3'
export default defineEventHandler(async (event) => {
- try {
- const id = getRouterParam(event, 'id')
- if (!id) {
- throw createError({
- statusCode: 400,
- statusMessage: 'Missing appointment ID',
- })
- }
+ const id = getRouterParam(event, 'id')
+ const { type, startTime, seriesId } = await readBody(event)
- const user = requireStaff(event)
- const adminId = user.id
+ if (!id) {
+ throw createError({
+ statusCode: 400,
+ statusMessage: 'Missing appointment id',
+ })
+ }
- const existing = await prisma.appointment.findUnique({
- where: { id },
- select: { clientId: true },
+ // keep stage auth checks
+ const user = requireStaff(event)
+ const adminId = user.id // optional if needed elsewhere
+
+ const existing = await prisma.appointment.findUnique({
+ where: { id },
+ select: {
+ clientId: true,
+ seriesId: true,
+ },
+ })
+
+ if (!existing) {
+ throw createError({
+ statusCode: 404,
+ statusMessage: 'Appointment not found',
})
- if (!existing) {
- throw createError({ statusCode: 404, statusMessage: 'Appointment not found' })
- }
- await assertStaffCanAccessClient(event, existing.clientId)
+ }
+
+ await assertStaffCanAccessClient(event, existing.clientId)
+ // ONE event only
+ if (type === 'ONE' || !seriesId) {
await prisma.appointment.delete({
where: { id },
})
+ }
- return {
- success: true,
- }
- } catch (error: any) {
- if (error?.code === 'P2025') {
- throw createError({
- statusCode: 404,
- statusMessage: 'Appointment not found',
- })
- }
-
- if (error && typeof error === 'object' && 'statusCode' in error) {
- throw error
- }
-
- console.error('Delete appointment error:', error)
-
- // Check if it's a Prisma error
- if (error && typeof error === 'object') {
- const errorObj = error as Record
- console.error('Error details:', {
- message: errorObj.message,
- code: errorObj.code,
- meta: errorObj.meta,
- })
- }
+ // entire series
+ else if (type === 'ALL') {
+ await prisma.appointment.deleteMany({
+ where: {
+ seriesId,
+ },
+ })
+ }
- throw createError({
- statusCode: 500,
- statusMessage: 'Failed to delete appointment',
+ // this and future
+ else if (type === 'FUTURE') {
+ await prisma.appointment.deleteMany({
+ where: {
+ seriesId,
+ startTime: {
+ gte: new Date(startTime),
+ },
+ },
})
}
+
+ return { success: true }
})
diff --git a/server/api/appointments/[id].put.ts b/server/api/appointments/[id].put.ts
index 935e982..10e5a23 100644
--- a/server/api/appointments/[id].put.ts
+++ b/server/api/appointments/[id].put.ts
@@ -4,35 +4,53 @@ import { assertStaffCanAccessClient } from '../../utils/clinician-access'
import { normalizeVideoJoinUrl, parseVideoProviderInput } from '../../utils/video-conference'
import { defineEventHandler, getRouterParam, readBody, createError } from 'h3'
import type { VideoConferenceProvider } from '../../../prisma/generated/enums'
-
export default defineEventHandler(async (event) => {
requireStaff(event)
try {
const id = getRouterParam(event, 'id')
- if (!id) throw createError({ statusCode: 400, statusMessage: 'Missing ID' })
+ if (!id) {
+ throw createError({
+ statusCode: 400,
+ statusMessage: 'Missing ID',
+ })
+ }
+
const body = await readBody(event)
- const { description, date, startTime, endTime, videoProvider, videoJoinUrl } = body
+
+ const { type, startTime, seriesId, description, date, endTime, videoProvider, videoJoinUrl } =
+ body
const startTimeDate = new Date(`${date}T${startTime}`)
const endTimeDate = new Date(`${date}T${endTime}`)
const existing = await prisma.appointment.findUnique({
where: { id },
- select: { videoProvider: true, videoJoinUrl: true, clientId: true },
+ select: {
+ clientId: true,
+ videoProvider: true,
+ videoJoinUrl: true,
+ },
})
+
if (!existing) {
- throw createError({ statusCode: 404, statusMessage: 'Appointment not found' })
+ throw createError({
+ statusCode: 404,
+ statusMessage: 'Appointment not found',
+ })
}
+
await assertStaffCanAccessClient(event, existing.clientId)
const parsedProvider =
videoProvider !== undefined
? (parseVideoProviderInput(videoProvider) as VideoConferenceProvider | null)
: undefined
- const parsedJoin =
- videoJoinUrl !== undefined ? normalizeVideoJoinUrl(videoJoinUrl) : undefined
+
+ const parsedJoin = videoJoinUrl !== undefined ? normalizeVideoJoinUrl(videoJoinUrl) : undefined
+
const rawJoinInput = typeof videoJoinUrl === 'string' ? videoJoinUrl.trim() : ''
+
if (videoJoinUrl !== undefined && rawJoinInput && !parsedJoin) {
throw createError({
statusCode: 400,
@@ -40,8 +58,8 @@ export default defineEventHandler(async (event) => {
})
}
- const effectiveProvider =
- parsedProvider !== undefined ? parsedProvider : existing.videoProvider
+ const effectiveProvider = parsedProvider !== undefined ? parsedProvider : existing.videoProvider
+
const effectiveJoin = parsedJoin !== undefined ? parsedJoin : existing.videoJoinUrl
if (effectiveJoin && !effectiveProvider) {
@@ -51,20 +69,65 @@ export default defineEventHandler(async (event) => {
})
}
- await prisma.appointment.update({
- where: { id },
- data: {
- description,
- startTime: startTimeDate,
- endTime: endTimeDate,
- ...(parsedProvider !== undefined && { videoProvider: parsedProvider }),
- ...(parsedJoin !== undefined && { videoJoinUrl: parsedJoin }),
- },
- })
+ // Update only this event
+ if (type === 'ONE' || !seriesId) {
+ await prisma.appointment.update({
+ where: { id },
+ data: {
+ description,
+ startTime: startTimeDate,
+ endTime: endTimeDate,
+ ...(parsedProvider !== undefined && {
+ videoProvider: parsedProvider,
+ }),
+ ...(parsedJoin !== undefined && {
+ videoJoinUrl: parsedJoin,
+ }),
+ },
+ })
+ }
+
+ // Update all events in series
+ else if (type === 'ALL') {
+ await prisma.appointment.updateMany({
+ where: { seriesId },
+ data: {
+ description,
+ ...(parsedProvider !== undefined && {
+ videoProvider: parsedProvider,
+ }),
+ ...(parsedJoin !== undefined && {
+ videoJoinUrl: parsedJoin,
+ }),
+ },
+ })
+ }
+
+ // Update this and future events
+ else if (type === 'FUTURE') {
+ await prisma.appointment.updateMany({
+ where: {
+ seriesId,
+ startTime: {
+ gte: new Date(startTime),
+ },
+ },
+ data: {
+ description,
+ ...(parsedProvider !== undefined && {
+ videoProvider: parsedProvider,
+ }),
+ ...(parsedJoin !== undefined && {
+ videoJoinUrl: parsedJoin,
+ }),
+ },
+ })
+ }
return { success: true }
} catch (error: unknown) {
const err = error as { code?: string; statusCode?: number }
+
if (err?.code === 'P2025') {
throw createError({
statusCode: 404,
@@ -77,6 +140,7 @@ export default defineEventHandler(async (event) => {
}
console.error('Error updating appointment:', error)
+
throw createError({
statusCode: 500,
statusMessage: 'Failed to update appointment',
diff --git a/server/api/appointments/index.get.ts b/server/api/appointments/index.get.ts
index a5916ee..bf8e7fe 100644
--- a/server/api/appointments/index.get.ts
+++ b/server/api/appointments/index.get.ts
@@ -121,6 +121,7 @@ export default defineEventHandler(async (event) => {
clientName: a.client.name,
description: a.description,
status: a.status,
+ seriesId: a.seriesId,
videoProvider: a.videoProvider,
videoJoinUrl: a.videoJoinUrl,
assignedClinicianName:
diff --git a/server/api/appointments/index.post.ts b/server/api/appointments/index.post.ts
index 4a1b91d..1b398ce 100644
--- a/server/api/appointments/index.post.ts
+++ b/server/api/appointments/index.post.ts
@@ -1,8 +1,8 @@
import { requireStaff } from '../../utils/guard'
import { assertStaffCanAccessClient } from '../../utils/clinician-access'
import { prisma } from '../../utils/prisma'
-import { normalizeVideoJoinUrl, parseVideoProviderInput } from '../../utils/video-conference'
import { readBody, createError, defineEventHandler } from 'h3'
+import { normalizeVideoJoinUrl, parseVideoProviderInput } from '../../utils/video-conference'
import type { VideoConferenceProvider } from '../../../prisma/generated/enums'
function sanitizeNamePart(part: string | null | undefined) {
@@ -13,8 +13,10 @@ function sanitizeNamePart(part: string | null | undefined) {
function deriveSessionName(fullName: string | null | undefined, sessionNumber: number) {
const raw = (fullName ?? '').trim()
const pieces = raw.split(/\s+/).filter(Boolean)
+
const first = sanitizeNamePart(pieces[0] ?? 'Client') || 'Client'
const last = sanitizeNamePart(pieces.slice(1).join('_') || 'Unknown') || 'Unknown'
+
return `${first}_${last}_${String(sessionNumber).padStart(2, '0')}`
}
@@ -40,18 +42,20 @@ export default defineEventHandler(async (event) => {
statusMessage: 'Missing required fields',
})
}
+
await assertStaffCanAccessClient(event, clientId)
const start = new Date(`${date}T${startTime}`)
const end = new Date(`${date}T${endTime}`)
const now = new Date()
- if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()) || end <= start) {
+ if (end <= start) {
throw createError({
statusCode: 400,
- statusMessage: 'Invalid date/time range',
+ statusMessage: 'Invalid time range',
})
}
+
if (start < now) {
throw createError({
statusCode: 400,
@@ -60,14 +64,17 @@ export default defineEventHandler(async (event) => {
}
const parsedProvider = parseVideoProviderInput(videoProvider) as VideoConferenceProvider | null
+
const normalizedJoin = normalizeVideoJoinUrl(videoJoinUrl)
const rawJoinInput = typeof videoJoinUrl === 'string' ? videoJoinUrl.trim() : ''
+
if (rawJoinInput && !normalizedJoin) {
throw createError({
statusCode: 400,
statusMessage: 'Enter a valid meeting link starting with http:// or https://',
})
}
+
if (normalizedJoin && !parsedProvider) {
throw createError({
statusCode: 400,
@@ -99,6 +106,7 @@ export default defineEventHandler(async (event) => {
return normalized !== 'CANCELED' && normalized !== 'CANCELLED'
})
.map((a) => a.sessionNumber)
+
const sessionNumber = nextAvailableNumber(activeSessionNumbers)
const sessionName = deriveSessionName(clientUser.name, sessionNumber)
@@ -113,8 +121,8 @@ export default defineEventHandler(async (event) => {
startTime: start,
endTime: end,
status: 'SCHEDULED',
- ...(parsedProvider != null && { videoProvider: parsedProvider }),
- ...(normalizedJoin != null && { videoJoinUrl: normalizedJoin }),
+ videoProvider: parsedProvider ?? null,
+ videoJoinUrl: normalizedJoin ?? null,
},
})
@@ -122,6 +130,7 @@ export default defineEventHandler(async (event) => {
where: { userId: clientId },
select: { id: true },
})
+
if (clientRow) {
await prisma.sessionNote.create({
data: {
@@ -130,7 +139,7 @@ export default defineEventHandler(async (event) => {
sessionName,
sessionNumber,
content: '',
- attended: true,
+ attendanceStatus: 'show',
},
})
}
@@ -151,11 +160,12 @@ export default defineEventHandler(async (event) => {
})
}
- console.error('Create appointment error:', error)
+ console.error('🔥 BACKEND FULL ERROR:', error)
+ console.error('🔥 BACKEND STACK:', error?.stack)
throw createError({
statusCode: 500,
- statusMessage: 'Failed to create appointment',
+ statusMessage: error?.message || JSON.stringify(error),
})
}
})
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
}