Skip to content
Draft
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
34 changes: 27 additions & 7 deletions client/main.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<head>
<title>TimeHarbor - Your Personal Time Tracking Assistant</title>
<link rel="stylesheet" href="/client/tailwind.generated.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>

<body class="bg-base-100 min-h-screen">
Expand Down Expand Up @@ -151,27 +152,46 @@ <h3 class="text-xl font-semibold mb-4">My Activities & Tasks</h3>
</div>
{{#if showCreateTicketForm}}
<form id="createTicketForm" class="card bg-base-100 shadow p-4 mb-4 flex flex-col gap-2">
<input type="text" name="title" placeholder="Activity Title (e.g., 'Math Homework', 'Robot Assembly')" class="input input-bordered" required />
<input type="text" name="github" placeholder="Reference Link or Notes" class="input input-bordered" />
<h4 class="text-lg font-semibold mb-2">{{#if editingTicketId}}Edit Activity{{else}}Create New Activity{{/if}}</h4>
<input type="text" name="title" placeholder="Activity Title (e.g., 'Math Homework', 'Robot Assembly')" class="input input-bordered" required
value="{{#if editingTicket}}{{editingTicket.title}}{{/if}}" />
<input type="text" name="github" placeholder="Reference Link or Notes" class="input input-bordered"
value="{{#if editingTicket}}{{editingTicket.github}}{{/if}}" />
<div class="flex gap-2">
<input type="number" name="hours" min="0" placeholder="Hours" class="input input-bordered w-24" />
<input type="number" name="minutes" min="0" max="59" placeholder="Minutes" class="input input-bordered w-24" />
<input type="number" name="seconds" min="0" max="59" placeholder="Seconds" class="input input-bordered w-24" />
<input type="number" name="hours" min="0" placeholder="Hours" class="input input-bordered w-24"
value="{{editingTicketHours}}" />
<input type="number" name="minutes" min="0" max="59" placeholder="Minutes" class="input input-bordered w-24"
value="{{editingTicketMinutes}}" />
<input type="number" name="seconds" min="0" max="59" placeholder="Seconds" class="input input-bordered w-24"
value="{{editingTicketSeconds}}" />
</div>
<div class="flex gap-2">
<button type="submit" class="btn btn-primary">Create</button>
<button type="submit" class="btn btn-primary">{{#if editingTicketId}}Update{{else}}Create{{/if}}</button>
<button type="button" id="cancelCreateTicket" class="btn btn-outline">Cancel</button>
</div>
</form>
{{/if}}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{{#each tickets}}
<div class="card bg-base-100 shadow-md p-4 {{#if isActive _id}}border border-slate-600{{/if}}">
<div class="ticket-title text-lg font-bold mb-2">{{title}}</div>
<div class="ticket-title text-lg font-bold mb-2 flex items-center justify-between">
<span>{{title}}</span>
<div class="flex items-center gap-2">
<button class="edit-activity-btn text-blue-600 hover:text-blue-800" data-id="{{_id}}" title="Edit Activity">
<i class="fa-solid fa-pen text-primary"></i>
</button>
<button class="delete-activity-btn text-red-600 hover:text-red-800" data-id="{{_id}}" title="Delete Activity">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</div>
<div class="ticket-time mb-2"><span class="badge" style="background-color: #4A5568; color: white;">{{formatTime displayTime}}</span></div>
{{#if github}}
<a href="{{githubLink github}}" target="_blank" class="link text-primary hover:text-primary-focus">Reference</a>
{{/if}}
{{#if (activityErrorMessage _id)}}
<div class="text-red-600 text-sm mb-1">{{activityErrorMessage _id}}</div>
{{/if}}
<button class="activate-ticket btn btn-outline btn-neutral mt-2 w-full" data-id="{{_id}}">
{{#if isActive _id}}
Stop
Expand Down
176 changes: 120 additions & 56 deletions client/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@ Template.tickets.onCreated(function () {
// Restore last active ticket if it is still running
this.activeTicketId = new ReactiveVar(null);
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');
Expand Down Expand Up @@ -339,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 [];
Expand Down Expand Up @@ -400,6 +432,10 @@ Template.tickets.helpers({
}
return total;
},
activityErrorMessage(ticketId) {
const errors = Template.instance().activityErrorMessages.get() || {};
return errors[ticketId] || '';
},
});

Template.tickets.events({
Expand All @@ -408,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();
Expand All @@ -421,76 +465,83 @@ 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;
const isActive = t.activeTicketId.get() === ticketId;
const ticket = Tickets.findOne(ticketId);
const teamId = t.selectedTeamId.get();

// Check if user is clocked in for this team
const clockEvent = ClockEvents.findOne({ userId: Meteor.userId(), teamId, endTime: null });

// Get the error messages object
const errorMessages = t.activityErrorMessages.get() || {};
if (!isActive) {
// Stop any currently active ticket first
const currentActiveTicketId = t.activeTicketId.get();
if (currentActiveTicketId) {
const currentTicket = Tickets.findOne(currentActiveTicketId);
if (currentTicket && currentTicket.startTimestamp) {
const now = Date.now();
// Stop the current ticket
Meteor.call('updateTicketStop', currentActiveTicketId, now, (err) => {
if (err) {
alert('Failed to stop current timer: ' + err.reason);
return;
}
});

// Stop the current ticket in the clock event if needed
if (clockEvent) {
Meteor.call('clockEventStopTicket', clockEvent._id, currentActiveTicketId, now, (err) => {
if (err) {
alert('Failed to stop current ticket in clock event: ' + err.reason);
return;
}
});
}
// Prevent starting activity if session is not active
if (!clockEvent) {
errorMessages[ticketId] = 'Please start the session first.';
t.activityErrorMessages.set(errorMessages);
return;
} else {
// Clear error if session is active
if (errorMessages[ticketId]) {
delete errorMessages[ticketId];
t.activityErrorMessages.set(errorMessages);
}
}

// Start the new timer
t.activeTicketId.set(ticketId);
const now = Date.now();
Expand Down Expand Up @@ -566,6 +617,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 () {
Expand Down
Loading