From 515394241c1f0cc753aaaa85edf4acf525bcca54 Mon Sep 17 00:00:00 2001 From: Navyaprabha Rajappa Date: Wed, 16 Jul 2025 11:30:00 -0400 Subject: [PATCH 1/6] feat: add per-activity red error message above Start button when session is not active --- client/main.html | 3 +++ client/main.js | 45 ++++++++++++++++++--------------------------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/client/main.html b/client/main.html index 9a48ecc..e61d48c 100644 --- a/client/main.html +++ b/client/main.html @@ -172,6 +172,9 @@

My Activities & Tasks

{{#if github}} Reference {{/if}} + {{#if (activityErrorMessage _id)}} +
{{activityErrorMessage _id}}
+ {{/if}} +
{{formatTime displayTime}}
{{#if github}} Reference diff --git a/client/main.js b/client/main.js index cbb79e8..8972ca6 100644 --- a/client/main.js +++ b/client/main.js @@ -557,6 +557,19 @@ Template.tickets.events({ } }); }, + 'click .delete-activity-btn'(e, t) { + const ticketId = e.currentTarget.dataset.id; + // Show confirmation popup + if (confirm('Are you sure you want to delete this activity?')) { + Meteor.call('deleteTicket', ticketId, (err) => { + if (err) { + alert('Failed to delete activity: ' + err.reason); + } + // UI will update reactively + }); + } + // If user clicks Cancel, do nothing + }, }); Template.home.onCreated(function () { diff --git a/server/main.js b/server/main.js index 3345fca..6a00255 100644 --- a/server/main.js +++ b/server/main.js @@ -397,4 +397,21 @@ Meteor.methods({ ); } }, + async deleteTicket(ticketId) { + check(ticketId, String); + if (!this.userId) throw new Meteor.Error('not-authorized'); + const ticket = await Tickets.findOneAsync(ticketId); + if (!ticket) throw new Meteor.Error('not-found', 'Ticket not found'); + if (ticket.createdBy && ticket.createdBy !== this.userId) { + throw new Meteor.Error('forbidden', 'You can only delete your own activities'); + } + await Tickets.removeAsync(ticketId); + // Optionally, remove from any clock events + await ClockEvents.updateAsync( + { 'tickets.ticketId': ticketId }, + { $pull: { tickets: { ticketId } } }, + { multi: true } + ); + return true; + }, }); From 8263f3d605d26302382e2254ff541f26f5fd59e6 Mon Sep 17 00:00:00 2001 From: Navyaprabha Rajappa Date: Wed, 16 Jul 2025 16:54:16 -0400 Subject: [PATCH 5/6] feat: Add edit functionality for activities --- client/main.html | 29 ++++++++---- client/main.js | 118 +++++++++++++++++++++++++++++++++++------------ server/main.js | 29 ++++++++++++ 3 files changed, 138 insertions(+), 38 deletions(-) diff --git a/client/main.html b/client/main.html index bafb24f..0525dbb 100644 --- a/client/main.html +++ b/client/main.html @@ -151,15 +151,21 @@

My Activities & Tasks

{{#if showCreateTicketForm}}
- - +

{{#if editingTicketId}}Edit Activity{{else}}Create New Activity{{/if}}

+ +
- - - + + +
- +
@@ -169,9 +175,14 @@

My Activities & Tasks

{{title}} - +
+ + +
{{formatTime displayTime}}
{{#if github}} diff --git a/client/main.js b/client/main.js index 8972ca6..9002f02 100644 --- a/client/main.js +++ b/client/main.js @@ -295,6 +295,8 @@ Template.tickets.onCreated(function () { this.clockedIn = new ReactiveVar(false); // Add per-activity error messages this.activityErrorMessages = new ReactiveVar({}); + // Add edit functionality + this.editingTicketId = new ReactiveVar(null); this.autorun(() => { this.subscribe('userTeams'); this.subscribe('clockEventsForUser'); @@ -341,6 +343,34 @@ Template.tickets.helpers({ showCreateTicketForm() { return Template.instance().showCreateTicketForm.get(); }, + editingTicketId() { + return Template.instance().editingTicketId.get(); + }, + isEditing(ticketId) { + return Template.instance().editingTicketId.get() === ticketId; + }, + editingTicket() { + const editingId = Template.instance().editingTicketId.get(); + return editingId ? Tickets.findOne(editingId) : null; + }, + editingTicketHours() { + const editingId = Template.instance().editingTicketId.get(); + if (!editingId) return ''; + const ticket = Tickets.findOne(editingId); + return ticket ? Math.floor((ticket.accumulatedTime || 0) / 3600) : ''; + }, + editingTicketMinutes() { + const editingId = Template.instance().editingTicketId.get(); + if (!editingId) return ''; + const ticket = Tickets.findOne(editingId); + return ticket ? Math.floor(((ticket.accumulatedTime || 0) % 3600) / 60) : ''; + }, + editingTicketSeconds() { + const editingId = Template.instance().editingTicketId.get(); + if (!editingId) return ''; + const ticket = Tickets.findOne(editingId); + return ticket ? (ticket.accumulatedTime || 0) % 60 : ''; + }, tickets() { const teamId = Template.instance().selectedTeamId.get(); if (!teamId) return []; @@ -414,9 +444,17 @@ Template.tickets.events({ }, 'click #showCreateTicketForm'(e, t) { t.showCreateTicketForm.set(true); + t.editingTicketId.set(null); // Clear any editing state }, 'click #cancelCreateTicket'(e, t) { t.showCreateTicketForm.set(false); + t.editingTicketId.set(null); // Clear editing state + }, + 'click .edit-activity-btn'(e, t) { + e.stopPropagation(); + const ticketId = e.currentTarget.dataset.id; + t.editingTicketId.set(ticketId); + t.showCreateTicketForm.set(true); }, 'submit #createTicketForm'(e, t) { e.preventDefault(); @@ -427,39 +465,61 @@ Template.tickets.events({ const minutes = parseInt(e.target.minutes.value) || 0; const seconds = parseInt(e.target.seconds.value) || 0; const accumulatedTime = hours * 3600 + minutes * 60 + seconds; + const editingId = t.editingTicketId.get(); + if (!title) { - alert('Ticket title is required.'); + alert('Activity title is required.'); return; } - Meteor.call('createTicket', { teamId, title, github, accumulatedTime }, (err, ticketId) => { - if (!err) { - t.showCreateTicketForm.set(false); - // Auto-start the ticket if there's time specified - if (accumulatedTime > 0) { - const now = Date.now(); - // Start the new timer - t.activeTicketId.set(ticketId); - debugger; - Meteor.call('updateTicketStart', ticketId, now, (err) => { - if (err) { - alert('Failed to start timer: ' + err.reason); - return; - } - // If user is clocked in, add the ticket timing entry to the clock event - const clockEvent = ClockEvents.findOne({ userId: Meteor.userId(), teamId, endTime: null }); - if (clockEvent) { - Meteor.call('clockEventAddTicket', clockEvent._id, ticketId, now, (err) => { - if (err) { - alert('Failed to add ticket to clock event: ' + err.reason); - } - }); - } - }); + + if (editingId) { + // Update existing ticket + Meteor.call('updateTicket', editingId, { title, github, accumulatedTime }, (err) => { + if (!err) { + t.showCreateTicketForm.set(false); + t.editingTicketId.set(null); + // Clear form + e.target.title.value = ''; + e.target.github.value = ''; + e.target.hours.value = ''; + e.target.minutes.value = ''; + e.target.seconds.value = ''; + } else { + alert('Error updating activity: ' + err.reason); } - } else { - alert('Error creating ticket: ' + err.reason); - } - }); + }); + } else { + // Create new ticket + Meteor.call('createTicket', { teamId, title, github, accumulatedTime }, (err, ticketId) => { + if (!err) { + t.showCreateTicketForm.set(false); + // Auto-start the ticket if there's time specified + if (accumulatedTime > 0) { + const now = Date.now(); + // Start the new timer + t.activeTicketId.set(ticketId); + debugger; + Meteor.call('updateTicketStart', ticketId, now, (err) => { + if (err) { + alert('Failed to start timer: ' + err.reason); + return; + } + // If user is clocked in, add the ticket timing entry to the clock event + const clockEvent = ClockEvents.findOne({ userId: Meteor.userId(), teamId, endTime: null }); + if (clockEvent) { + Meteor.call('clockEventAddTicket', clockEvent._id, ticketId, now, (err) => { + if (err) { + alert('Failed to add ticket to clock event: ' + err.reason); + } + }); + } + }); + } + } else { + alert('Error creating activity: ' + err.reason); + } + }); + } }, 'click .activate-ticket'(e, t) { const ticketId = e.currentTarget.dataset.id; diff --git a/server/main.js b/server/main.js index 6a00255..58398fb 100644 --- a/server/main.js +++ b/server/main.js @@ -230,6 +230,35 @@ Meteor.methods({ return ticketId; }, + async updateTicket(ticketId, updates) { + check(ticketId, String); + check(updates, Object); + if (!this.userId) throw new Meteor.Error('not-authorized'); + + const ticket = await Tickets.findOneAsync(ticketId); + if (!ticket) throw new Meteor.Error('not-found', 'Activity not found'); + if (ticket.createdBy && ticket.createdBy !== this.userId) { + throw new Meteor.Error('forbidden', 'You can only edit your own activities'); + } + + // Only allow updating certain fields + const allowedUpdates = {}; + if (updates.title !== undefined) { + check(updates.title, String); + allowedUpdates.title = updates.title.trim(); + } + if (updates.github !== undefined) { + check(updates.github, String); + allowedUpdates.github = updates.github.trim(); + } + if (updates.accumulatedTime !== undefined) { + check(updates.accumulatedTime, Number); + allowedUpdates.accumulatedTime = updates.accumulatedTime; + } + + await Tickets.updateAsync(ticketId, { $set: allowedUpdates }); + return true; + }, incrementTicketTime(ticketId, seconds) { check(ticketId, String); check(seconds, Number); From 10632f300570c639c931ab7f35bcfbb0a3f3633a Mon Sep 17 00:00:00 2001 From: Navyaprabha Rajappa Date: Wed, 23 Jul 2025 16:55:41 -0400 Subject: [PATCH 6/6] Update edit and delete icons to use Font Awesome; match edit icon color to "Create Activity" button --- client/main.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/main.html b/client/main.html index 0525dbb..33aefac 100644 --- a/client/main.html +++ b/client/main.html @@ -1,6 +1,7 @@ TimeHarbor - Your Personal Time Tracking Assistant + @@ -177,10 +178,10 @@

{{#if editingTicketId}}Edit Activity{{els {{title}}