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 @@
+ + + + + + + + + + + + + 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), }) } })