diff --git a/client/components/pulseVault/PulseUpload.html b/client/components/pulseVault/PulseUpload.html new file mode 100644 index 0000000..971a0fa --- /dev/null +++ b/client/components/pulseVault/PulseUpload.html @@ -0,0 +1,90 @@ + diff --git a/client/components/pulseVault/PulseUpload.js b/client/components/pulseVault/PulseUpload.js new file mode 100644 index 0000000..baf39a3 --- /dev/null +++ b/client/components/pulseVault/PulseUpload.js @@ -0,0 +1,136 @@ + +import { Template } from 'meteor/templating'; +import { ReactiveVar } from 'meteor/reactive-var'; +import { Tracker } from 'meteor/tracker'; +import QRCode from 'qrcode'; +import './PulseUpload.html'; + +// Reactive state for the pulse upload modal +const pulseUploadState = { + loading: new ReactiveVar(false), + error: new ReactiveVar(null), + data: new ReactiveVar(null), + ticketId: new ReactiveVar(null), + ticketTitle: new ReactiveVar(null) +}; + +/** + * Open the pulse upload modal for a ticket + * @param {String} ticketId - The ticket ID + * @param {String} ticketTitle - The ticket title for display + */ +export function openPulseUploadForTicket(ticketId, ticketTitle) { + pulseUploadState.ticketId.set(ticketId); + pulseUploadState.ticketTitle.set(ticketTitle); + pulseUploadState.loading.set(true); + pulseUploadState.error.set(null); + pulseUploadState.data.set(null); + + // Show modal + const modal = document.getElementById('pulseUploadModal'); + if (modal) { + modal.showModal(); + } + + // Call server to create upload link + Meteor.call('createPulseUploadForTicket', ticketId, (err, result) => { + pulseUploadState.loading.set(false); + if (err) { + pulseUploadState.error.set(err.reason || err.message || 'Failed to create upload link'); + return; + } + pulseUploadState.data.set(result); + + // Render QR code after data is set + Tracker.afterFlush(() => { + renderQrCode(result.qrData); + }); + }); +} + +/** + * Render QR code to the container element + * @param {String} data - The data to encode in the QR code + */ +function renderQrCode(data) { + const container = document.getElementById('pulseQrCode'); + if (!container || !data) return; + + // Clear previous content + container.innerHTML = ''; + + // Create canvas element + const canvas = document.createElement('canvas'); + container.appendChild(canvas); + + // Generate QR code + QRCode.toCanvas(canvas, data, { + width: 192, + margin: 2, + color: { + dark: '#1f2937', + light: '#ffffff' + }, + errorCorrectionLevel: 'M' + }, (err) => { + if (err) { + console.error('Failed to generate QR code:', err); + container.innerHTML = '

Failed to generate QR code

'; + } + }); +} + +/** + * Close the pulse upload modal and reset state + */ +function closePulseUploadModal() { + const modal = document.getElementById('pulseUploadModal'); + if (modal) { + modal.close(); + } + + // Reset state + pulseUploadState.loading.set(false); + pulseUploadState.error.set(null); + pulseUploadState.data.set(null); + pulseUploadState.ticketId.set(null); + pulseUploadState.ticketTitle.set(null); +} + +// Template helpers +Template.pulseUploadModal.helpers({ + pulseUploadLoading() { + return pulseUploadState.loading.get(); + }, + + pulseUploadError() { + return pulseUploadState.error.get(); + }, + + pulseUploadData() { + return pulseUploadState.data.get(); + }, + + pulseUploadTicketTitle() { + return pulseUploadState.ticketTitle.get(); + } +}); + +// Template events +Template.pulseUploadModal.events({ + 'click .close-pulse-upload-modal'(event) { + event.preventDefault(); + closePulseUploadModal(); + }, + + 'click .retry-pulse-upload'(event) { + event.preventDefault(); + + const ticketId = pulseUploadState.ticketId.get(); + const ticketTitle = pulseUploadState.ticketTitle.get(); + + if (ticketId) { + openPulseUploadForTicket(ticketId, ticketTitle); + } + } +}); diff --git a/client/components/shared/icons.html b/client/components/shared/icons.html index fc4d85e..c7b6fce 100644 --- a/client/components/shared/icons.html +++ b/client/components/shared/icons.html @@ -15,3 +15,16 @@ + + + + diff --git a/client/components/tickets/TicketsPage.html b/client/components/tickets/TicketsPage.html index a280063..10671a0 100644 --- a/client/components/tickets/TicketsPage.html +++ b/client/components/tickets/TicketsPage.html @@ -73,6 +73,12 @@

+ {{> pulseVaultIcon class="w-4 h-4 text-purple-600 dark:text-purple-400"}} + PulseCam + +
  • + + + {{> pulseUploadModal}} diff --git a/client/components/tickets/TicketsPage.js b/client/components/tickets/TicketsPage.js index 8c92caf..9118e5d 100644 --- a/client/components/tickets/TicketsPage.js +++ b/client/components/tickets/TicketsPage.js @@ -10,6 +10,7 @@ import { sessionManager } from '../../utils/clockSession.js'; import { extractUrlTitle, openExternalUrl, normalizeReferenceUrl } from '../../utils/UrlUtils.js'; import { getUserTeams, getUserName } from '../../utils/UserTeamUtils.js'; import { OPEN_TICKET_HISTORY_SESSION_KEY, OPEN_TICKET_HISTORY_RETURN_ROUTE_KEY } from '../../utils/UiStateKeys.js'; +import { openPulseUploadForTicket } from '../pulseVault/PulseUpload.js'; // Utility functions const utils = { @@ -961,6 +962,17 @@ Template.tickets.events({ t.ticketToDelete.set(null); }, + // PulseVault upload button + 'click .pulse-upload-btn'(e, t) { + e.preventDefault(); + e.stopPropagation(); + const ticketId = e.currentTarget.dataset.id; + const ticketTitle = e.currentTarget.dataset.title; + if (ticketId) { + openPulseUploadForTicket(ticketId, ticketTitle); + } + }, + 'click .assign-ticket-btn'(e, t) { e.preventDefault(); e.stopPropagation(); diff --git a/client/main.js b/client/main.js index e330b95..bf8786e 100644 --- a/client/main.js +++ b/client/main.js @@ -17,6 +17,7 @@ import './components/member/MemberActivityPage.html'; import './components/guide/UserGuide.html'; import './components/notifications/NotificationInboxPage.html'; import './components/profile/ProfilePage.html'; +import './components/pulseVault/PulseUpload.html'; // Import component JS files import './components/auth/AuthPage.js'; @@ -31,6 +32,7 @@ import './components/member/MemberActivityPage.js'; import './components/guide/UserGuide.js'; import './components/notifications/NotificationInboxPage.js'; import './components/profile/ProfilePage.js'; +import './components/pulseVault/PulseUpload.js'; // Import routing configuration import './routes.js'; diff --git a/collections.js b/collections.js index 562c3f1..8f95cfe 100644 --- a/collections.js +++ b/collections.js @@ -11,3 +11,6 @@ export const Notifications = new Mongo.Collection('notifications'); /** One-to-one admin/member messages */ export const Messages = new Mongo.Collection('messages'); + +/** PulseVault draft records - links draftIds to tickets/sessions for media uploads */ +export const PulseDrafts = new Mongo.Collection('pulsedrafts'); diff --git a/package-lock.json b/package-lock.json index ae624b2..a555211 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "jquery": "^3.6.0", "meteor-node-stubs": "^1.2.1", "postcss-load-config": "^6.0.1", + "qrcode": "^1.5.4", "web-push": "^3.6.7" }, "devDependencies": { @@ -1232,7 +1233,6 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", - "optional": true, "engines": { "node": ">=8" } @@ -1242,7 +1242,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "optional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -1433,6 +1432,15 @@ "node": ">= 0.4" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001718", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", @@ -1538,7 +1546,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "devOptional": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1551,7 +1558,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "devOptional": true, "license": "MIT" }, "node_modules/color-string": { @@ -1632,6 +1638,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1650,6 +1665,12 @@ "node": ">=8" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -1752,8 +1773,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/encoding-sniffer": { "version": "0.2.1", @@ -1920,6 +1940,19 @@ "node": ">=0.8.0" } }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/firebase-admin": { "version": "13.6.0", "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.6.0.tgz", @@ -1946,6 +1979,19 @@ "@google-cloud/storage": "^7.14.0" } }, + "node_modules/firebase-admin/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -2060,7 +2106,6 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "license": "ISC", - "optional": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -2370,7 +2415,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", - "optional": true, "engines": { "node": ">=8" } @@ -2724,6 +2768,18 @@ "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -4204,6 +4260,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -4253,12 +4345,30 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -4380,6 +4490,89 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -4400,11 +4593,16 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "license": "MIT", - "optional": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -4468,6 +4666,12 @@ "node": ">=10" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/sharp": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", @@ -4559,7 +4763,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", - "optional": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -4574,7 +4777,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", - "optional": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4757,19 +4959,6 @@ "license": "MIT", "optional": true }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, "node_modules/web-push": { "version": "3.6.7", "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz", @@ -4849,6 +5038,12 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index fa82ecd..bb84241 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "jquery": "^3.6.0", "meteor-node-stubs": "^1.2.1", "postcss-load-config": "^6.0.1", + "qrcode": "^1.5.4", "web-push": "^3.6.7" }, "meteor": { diff --git a/server/main.js b/server/main.js index 5dca4ff..df82fc9 100644 --- a/server/main.js +++ b/server/main.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; import 'meteor/accounts-password'; import { check } from 'meteor/check'; -import { Tickets, Teams, Sessions, ClockEvents, Notifications, Messages } from '../collections.js'; +import { Tickets, Teams, Sessions, ClockEvents, Notifications, Messages, PulseDrafts } from '../collections.js'; // Import authentication methods import { authMethods } from './methods/auth.js'; // Import team methods @@ -17,6 +17,8 @@ import { messageMethods } from './methods/messages.js'; import './methods/calendar.js'; // Import notification methods import { notificationMethods } from './methods/notifications.js'; +// Import PulseVault integration methods +import './methods/pulseVault.js'; // Import clock event helpers for auto-clock-out import { stopTicketInClockEvent, formatDurationText } from './utils/ClockEventHelpers.js'; import { notifyTeamAdmins, notifyUser } from './utils/pushNotifications.js'; diff --git a/server/methods/pulseVault.js b/server/methods/pulseVault.js new file mode 100644 index 0000000..a6be720 --- /dev/null +++ b/server/methods/pulseVault.js @@ -0,0 +1,106 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import { PulseDrafts, Tickets } from '../../collections.js'; +import crypto from 'crypto'; + +/** + * PulseVault integration methods + * Generates deep links and QR data for uploading media via PulseCam app + */ + +export const pulseVaultMethods = { + /** + * Get or create a PulseVault draft and deeplink/QR data for a ticket. + * Reuses the same draftId and cached QR data - only calls API once per ticket. + * @param {String} ticketId - The ticket to attach the draft to + * @returns {Object} - Contains deeplink, qrData, draftId + */ + async createPulseUploadForTicket(ticketId) { + check(ticketId, String); + if (!this.userId) throw new Meteor.Error('not-authorized'); + + // Verify ticket exists and user has access + const ticket = await Tickets.findOneAsync(ticketId); + if (!ticket) throw new Meteor.Error('not-found', 'Ticket not found'); + + // Check if a draft already exists for this ticket with cached QR data + const existingDraft = await PulseDrafts.findOneAsync({ ticketId }); + + // If we have cached data, return it immediately (no API call needed) + if (existingDraft?.deeplink && existingDraft?.qrData) { + return { + draftId: existingDraft.draftId, + deeplink: existingDraft.deeplink, + qrData: existingDraft.qrData + }; + } + + // Get user email + const user = await Meteor.users.findOneAsync(this.userId); + const userEmail = user?.emails?.[0]?.address; + if (!userEmail) throw new Meteor.Error('no-email', 'User email not found'); + + // Get API key from settings + const apiKey = Meteor.settings?.private?.PULSE_PRIVATE_KEY; + if (!apiKey) throw new Meteor.Error('config-error', 'PulseVault API key not configured'); + + // Use existing draftId or generate a new one + const draftId = existingDraft?.draftId || crypto.randomUUID(); + + // Call PulseVault API to get deeplink (only on first creation) + const response = await fetch('https://pulse-vault.opensource.mieweb.org/api/qr/deeplink', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': apiKey + }, + body: JSON.stringify({ + draftId, + externalApp: 'timeharbour', + externalUserEmail: userEmail + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('PulseVault API error:', errorText); + throw new Meteor.Error('api-error', 'Failed to create PulseVault upload link'); + } + + const pulseData = await response.json(); + + // Store the draft record with cached QR data + if (existingDraft) { + // Update existing draft with QR data + await PulseDrafts.updateAsync(existingDraft._id, { + $set: { + deeplink: pulseData.deeplink, + qrData: pulseData.qrData + } + }); + } else { + // Insert new draft with QR data + await PulseDrafts.insertAsync({ + draftId, + ticketId, + userId: this.userId, + userEmail, + deeplink: pulseData.deeplink, + qrData: pulseData.qrData, + createdAt: new Date(), + status: 'active' + }); + } + + return { + draftId, + deeplink: pulseData.deeplink, + qrData: pulseData.qrData + }; + } +}; + +// Register methods with Meteor +Meteor.methods({ + createPulseUploadForTicket: pulseVaultMethods.createPulseUploadForTicket +}); diff --git a/settings.json.example b/settings.json.example index c3fa1ca..098075b 100644 --- a/settings.json.example +++ b/settings.json.example @@ -12,6 +12,7 @@ "github": { "clientId": "YOUR_GITHUB_OAUTH_CLIENT_ID", "clientSecret": "YOUR_GITHUB_OAUTH_CLIENT_SECRET" - } + }, + "PULSE_PRIVATE_KEY": "YOUR_PULSEVAULT_API_KEY" } }