Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions client/components/pulseVault/PulseUpload.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<template name="pulseUploadModal">
<dialog id="pulseUploadModal" class="modal">
<div class="modal-box bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-2xl shadow-xl max-w-md">
<!-- Header -->
<div class="flex justify-between items-start gap-2">
<h3 class="font-bold text-lg text-gray-900 dark:text-white flex items-center gap-2">
{{> pulseVaultIcon class="w-6 h-6 text-purple-600 dark:text-purple-400"}}
<span>Upload with PulseCam</span>
</h3>
<button type="button" class="close-pulse-upload-modal p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors shrink-0" aria-label="Close">
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<p class="text-gray-500 dark:text-gray-400 mt-1 mb-4 text-sm">
{{#if pulseUploadTicketTitle}}Attach media to: <span class="font-medium text-gray-700 dark:text-gray-300">{{pulseUploadTicketTitle}}</span>{{else}}Attach media to this session{{/if}}
</p>

{{#if pulseUploadLoading}}
<!-- Loading state -->
<div class="flex flex-col items-center justify-center py-8">
<div class="w-12 h-12 border-4 border-purple-200 dark:border-purple-800 border-t-purple-600 dark:border-t-purple-400 rounded-full animate-spin"></div>
<p class="mt-4 text-gray-600 dark:text-gray-400 text-sm">Generating upload link...</p>
</div>
{{else}}
{{#if pulseUploadError}}
<!-- Error state -->
<div class="py-6 text-center">
<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
<svg class="w-6 h-6 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
</div>
<p class="text-red-600 dark:text-red-400 font-medium">{{pulseUploadError}}</p>
<button type="button" class="retry-pulse-upload mt-4 px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors text-sm font-medium">
Try Again
</button>
</div>
{{else}}
{{#if pulseUploadData}}
<!-- Success state - show QR or deep link -->
<div class="space-y-4">
<!-- Desktop: QR Code -->
<div class="hidden md:block">
<p class="text-center text-gray-600 dark:text-gray-400 text-sm mb-3">
Scan with PulseCam app to upload photos/videos
</p>
<div id="pulseQrCodeContainer" class="flex justify-center p-4 bg-white rounded-xl border border-gray-200 dark:border-gray-600">
<!-- QR code will be rendered here by JavaScript -->
<div id="pulseQrCode" class="w-48 h-48 flex items-center justify-center"></div>
</div>
<p class="text-center text-xs text-gray-500 dark:text-gray-500 mt-2">
This QR code is permanently linked to this ticket
</p>
</div>

<!-- Mobile: Deep link button -->
<div class="md:hidden">
<p class="text-center text-gray-600 dark:text-gray-400 text-sm mb-4">
Tap the button below to open PulseCam and upload media
</p>
<a href="{{pulseUploadData.deeplink}}" class="flex items-center justify-center gap-2 w-full px-4 py-3 bg-purple-600 text-white rounded-xl hover:bg-purple-700 transition-colors shadow-sm font-medium text-base" aria-label="Open PulseCam app">
{{> cameraIcon class="w-5 h-5"}}
<span>Open PulseCam App</span>
</a>
<p class="text-center text-xs text-gray-400 dark:text-gray-500 mt-3">
Don't have PulseCam? <a href="https://apps.apple.com/app/pulsecam" target="_blank" rel="noopener noreferrer" class="text-purple-600 dark:text-purple-400 hover:underline">Download</a>
</p>
</div>

<!-- Draft info -->
<div class="pt-4 border-t border-gray-100 dark:border-gray-700">
<p class="text-xs text-gray-500 dark:text-gray-500 text-center">
Draft ID: <code class="font-mono bg-gray-100 dark:bg-gray-700 px-1 rounded">{{pulseUploadData.draftId}}</code>
</p>
</div>
</div>
{{/if}}
{{/if}}
{{/if}}

<!-- Footer -->
<div class="flex justify-end mt-6">
<button type="button" class="close-pulse-upload-modal px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 font-medium rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-sm">
Close
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button type="button" class="close-pulse-upload-modal">close</button>
</form>
</dialog>
</template>
136 changes: 136 additions & 0 deletions client/components/pulseVault/PulseUpload.js
Original file line number Diff line number Diff line change
@@ -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 = '<p class="text-red-500 text-sm">Failed to generate QR code</p>';
}
});
}

/**
* 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);
}
}
});
13 changes: 13 additions & 0 deletions client/components/shared/icons.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,16 @@
<line x1="1" y1="1" x2="23" y2="23"/>
</svg>
</template>

<template name="pulseVaultIcon">
<svg class="{{class}}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
</svg>
</template>

<template name="cameraIcon">
<svg class="{{class}}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
<circle cx="12" cy="13" r="4"/>
</svg>
</template>
9 changes: 9 additions & 0 deletions client/components/tickets/TicketsPage.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ <h3 class="font-medium text-gray-900 dark:text-white truncate text-sm md:text-ba
History
</button>
</li>
<li>
<button type="button" class="pulse-upload-btn flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-left" data-id="{{_id}}" data-title="{{title}}">
{{> pulseVaultIcon class="w-4 h-4 text-purple-600 dark:text-purple-400"}}
PulseCam
</button>
</li>
<li>
<button type="button" class="edit-ticket-btn flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors text-left" data-id="{{_id}}">
<svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" /></svg>
Expand Down Expand Up @@ -427,4 +433,7 @@ <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300">Daily Aggregati
<button type="button" id="ticketHistoryModalBackdropClose">close</button>
</form>
</dialog>

<!-- PulseVault Upload Modal -->
{{> pulseUploadModal}}
</template>
12 changes: 12 additions & 0 deletions client/components/tickets/TicketsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions client/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down
3 changes: 3 additions & 0 deletions collections.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Loading
Loading