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 @@
-
+
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/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/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/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..cade8bf 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: {
@@ -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),
})
}
})