1/*
2Snippet Name: Simple Project Timer
3Description: A simple project timer snippet for wp-admin using a custom table
4Author: Mark Harris
5Company: Christchurch Web Solutions
6URI: https://www.christchurchwebsolutions.co.uk
7
8This snippet provides a project timer functionality within the WordPress admin dashboard. It allows users to:
9- Start, stop, and log time spent on various tasks.
10- Manually add time entries with comments.
11- View a log of all time entries, including the date, time spent, comment, and user.
12- Clear the entire log of entries.
13- Calculate and display the total time spent on the project.
14
15Main Features:
161. Timer Functionality: Start and stop a timer to track the duration of tasks.
172. Manual Time Entry: Add time entries manually, specifying the date, time spent, comment, and user.
183. Log Display: View a detailed log of all time entries in a table format.
194. Clear Log: Clear all log entries and reset the total time.
205. Database Integration: Use a custom database table to store log entries and total time.
216. AJAX Requests: Handle all data operations (save, fetch, delete, update) via AJAX for seamless user experience.
22
23Database Schema:
24- Table Name: wp_project_timer
25- Columns:
26 - id (INT): Primary key, auto-incremented.
27 - date (DATETIME): The date and time when the entry was made.
28 - time_spent (TIME): The duration of time spent.
29 - comment (TEXT): A comment describing the task or entry.
30 - user (VARCHAR): The user who made the entry.
31
32AJAX Handlers:
33- save_project_timer_entry: Save a new time entry to the database.
34- fetch_project_timer_entries: Fetch all time entries from the database.
35- fetch_project_timer_total_time: Fetch the total time spent from the database.
36- delete_project_timer_entry: Delete a specific time entry from the database.
37- update_project_timer_total_time: Update the total time spent in the database.
38- clear_project_timer_log: Clear all time entries from the database and reset the total time.
39*/
40
41// Ensure the custom table is created
42function create_project_timer_table() {
43 global $wpdb;
44 $table_name = $wpdb->prefix . 'project_timer';
45 $charset_collate = $wpdb->get_charset_collate();
46
47 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) != $table_name) {
48 $sql = "CREATE TABLE $table_name (
49 id mediumint(9) NOT NULL AUTO_INCREMENT,
50 date datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
51 time_spent time NOT NULL,
52 comment text NOT NULL,
53 user varchar(100) NOT NULL,
54 PRIMARY KEY (id)
55 ) $charset_collate;";
56 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
57 dbDelta($sql);
58 }
59}
60create_project_timer_table();
61
62// Hook to add admin menu
63add_action('admin_menu', 'project_timer_menu');
64
65function project_timer_menu() {
66 add_menu_page('Project Timer', 'Project Timer', 'manage_options', 'project-timer', 'project_timer_page', 'dashicons-clock', 6);
67}
68
69function project_timer_page() {
70 $current_user = wp_get_current_user();
71 ?>
72 <div class="wrap">
73 <h1>Project Timer</h1>
74 <div id="stopwatch">
75 <span id="timer">00:00:00</span>
76 </div>
77 <div id="buttonContainer">
78 <button id="startBtn" class="button button-primary">Start</button>
79 <button id="stopBtn" class="button button-secondary">Stop</button>
80 <button id="clearLogBtn" class="button button-danger">Clear Log</button>
81 <button id="manualEntryBtn" class="button button-primary">Add Manual Entry</button>
82 <button id="copyClipboardBtn" class="button button-secondary">Copy to Clipboard</button>
83 </div>
84 <h2>Time Log</h2>
85 <table id="timeLog" class="widefat">
86 <thead>
87 <tr>
88 <th>Date</th>
89 <th>Time Spent</th>
90 <th>Comment</th>
91 <th>User</th>
92 <th>Action</th>
93 </tr>
94 </thead>
95 <tbody>
96 <!-- Log entries will be appended here -->
97 </tbody>
98 </table>
99 <h2>Total Time Spent on the Project: <span id="totalTime">00:00:00</span></h2>
100 </div>
101 <div id="manualEntryModal" class="modal">
102 <div class="modal-content">
103 <span class="close">×</span>
104 <h2>Add Manual Entry</h2>
105 <form id="manualEntryForm">
106 <label for="manualDate">Date:</label>
107 <input type="date" id="manualDate" name="manualDate" required><br><br>
108 <label for="manualTime">Time (hh:mm:ss):</label>
109 <input type="text" id="manualTime" name="manualTime" pattern="\d{2}:\d{2}:\d{2}" required><br><br>
110 <label for="manualComment">Comment:</label>
111 <textarea id="manualComment" name="manualComment" required></textarea><br><br>
112 <label for="manualUser">Username:</label>
113 <input type="text" id="manualUser" name="manualUser" required value="<?php echo $current_user->display_name; ?>"><br><br>
114 <input type="submit" class="button button-primary" value="Add Entry">
115 </form>
116 </div>
117 </div>
118 <script type="text/javascript">
119 document.addEventListener('DOMContentLoaded', function() {
120 let startTime;
121 let updatedTime;
122 let difference;
123 let tInterval;
124 let running = false;
125 let totalTime = 0;
126
127 const timerDisplay = document.getElementById('timer');
128 const startBtn = document.getElementById('startBtn');
129 const stopBtn = document.getElementById('stopBtn');
130 const clearLogBtn = document.getElementById('clearLogBtn');
131 const manualEntryBtn = document.getElementById('manualEntryBtn');
132 const copyClipboardBtn = document.getElementById('copyClipboardBtn');
133 const manualEntryModal = document.getElementById('manualEntryModal');
134 const closeModal = document.getElementsByClassName('close')[0];
135 const manualEntryForm = document.getElementById('manualEntryForm');
136 const timeLog = document.getElementById('timeLog').getElementsByTagName('tbody')[0];
137 const totalTimeDisplay = document.getElementById('totalTime');
138 const currentUser = '<?php echo $current_user->display_name; ?>';
139
140 // Load log entries and total time from database
141 fetchLogEntries();
142 fetchTotalTime();
143
144 // Check if timer was running
145 if (localStorage.getItem('running') === 'true') {
146 startTime = parseInt(localStorage.getItem('startTime'), 10);
147 running = true;
148 tInterval = setInterval(updateTime, 1000);
149 } else {
150 startTime = null;
151 }
152
153 startBtn.addEventListener('click', startTimer);
154 stopBtn.addEventListener('click', stopTimer);
155 clearLogBtn.addEventListener('click', clearLog);
156 manualEntryBtn.onclick = function() {
157 manualEntryModal.style.display = "block";
158 }
159
160 closeModal.onclick = function() {
161 manualEntryModal.style.display = "none";
162 }
163
164 window.onclick = function(event) {
165 if (event.target == manualEntryModal) {
166 manualEntryModal.style.display = "none";
167 }
168 }
169
170 manualEntryForm.onsubmit = function(event) {
171 event.preventDefault();
172 const manualDate = new Date(document.getElementById('manualDate').value);
173 const formattedDate = manualDate.toISOString().slice(0, 19).replace('T', ' ');
174 const manualTime = document.getElementById('manualTime').value;
175 const manualComment = document.getElementById('manualComment').value;
176 const manualUser = document.getElementById('manualUser').value;
177
178 addLogEntry(formattedDate, manualTime, manualComment, manualUser, true);
179 saveLogEntry(formattedDate, manualTime, manualComment, manualUser);
180 updateTotalTime(manualTime, true);
181 manualEntryModal.style.display = "none";
182 }
183
184 copyClipboardBtn.addEventListener('click', function() {
185 const table = document.getElementById('timeLog');
186 const range = document.createRange();
187 range.selectNode(table);
188 window.getSelection().removeAllRanges();
189 window.getSelection().addRange(range);
190
191 try {
192 document.execCommand('copy');
193 alert('Table copied to clipboard!');
194 } catch (err) {
195 console.error('Error copying table:', err);
196 }
197
198 window.getSelection().removeAllRanges();
199 });
200
201 function fetchLogEntries() {
202 fetch(ajaxurl, {
203 method: 'POST',
204 headers: {
205 'Content-Type': 'application/x-www-form-urlencoded',
206 },
207 body: new URLSearchParams({
208 action: 'fetch_project_timer_entries',
209 _wpnonce: '<?php echo wp_create_nonce('fetch_project_timer_entries'); ?>'
210 })
211 })
212 .then(response => response.json())
213 .then(data => {
214 if (data.success && data.data && Array.isArray(data.data.entries)) {
215 initializeLogEntries(data.data.entries);
216 }
217 })
218 .catch(error => {
219 console.error('Error fetching log entries:', error);
220 });
221 }
222
223 function fetchTotalTime() {
224 fetch(ajaxurl, {
225 method: 'POST',
226 headers: {
227 'Content-Type': 'application/x-www-form-urlencoded',
228 },
229 body: new URLSearchParams({
230 action: 'fetch_project_timer_total_time',
231 _wpnonce: '<?php echo wp_create_nonce('fetch_project_timer_total_time'); ?>'
232 })
233 })
234 .then(response => response.json())
235 .then(data => {
236 if (data.success && data.data !== undefined) {
237 totalTime = parseInt(data.data.total_time, 10); // Ensure total time is an integer
238 updateTotalTimeDisplay();
239 }
240 })
241 .catch(error => {
242 console.error('Error fetching total time:', error);
243 });
244 }
245
246 function initializeLogEntries(entries) {
247 if (!entries || !Array.isArray(entries)) {
248 return;
249 }
250 entries.forEach(entry => {
251 addLogEntry(entry.date, entry.time_spent, entry.comment, entry.user);
252 });
253 }
254
255 function startTimer() {
256 if (!running) {
257 startTime = new Date().getTime();
258 localStorage.setItem('startTime', startTime);
259 localStorage.setItem('running', true);
260 tInterval = setInterval(updateTime, 1000);
261 running = true;
262 }
263 }
264
265 function updateTime() {
266 updatedTime = new Date().getTime();
267 difference = updatedTime - startTime;
268
269 let hours = Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
270 let minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60));
271 let seconds = Math.floor((difference % (1000 * 60)) / 1000);
272
273 hours = (hours < 10) ? "0" + hours : hours;
274 minutes = (minutes < 10) ? "0" + minutes : minutes;
275 seconds = (seconds < 10) ? "0" + seconds : seconds;
276
277 timerDisplay.innerHTML = hours + ':' + minutes + ':' + seconds;
278 }
279
280 function stopTimer() {
281 if (running) {
282 clearInterval(tInterval);
283 running = false;
284 const elapsedTime = timerDisplay.innerHTML;
285 const comment = prompt('Please enter a comment for this time entry:');
286 const now = new Date();
287 const dateString = now.toISOString().slice(0, 19).replace('T', ' ');
288
289 addLogEntry(dateString, elapsedTime, comment, currentUser, true);
290 saveLogEntry(dateString, elapsedTime, comment, currentUser);
291 updateTotalTime(elapsedTime, true);
292 localStorage.setItem('running', false);
293 }
294 }
295
296 function addLogEntry(date, time, comment, user, prepend = false) {
297 const newRow = document.createElement('tr');
298 const dateCell = newRow.insertCell(0);
299 const timeCell = newRow.insertCell(1);
300 const commentCell = newRow.insertCell(2);
301 const userCell = newRow.insertCell(3);
302 const deleteCell = newRow.insertCell(4);
303
304 dateCell.textContent = date;
305 timeCell.textContent = time;
306 commentCell.textContent = comment;
307 userCell.textContent = user;
308
309 const deleteBtn = document.createElement('button');
310 deleteBtn.textContent = 'Delete';
311 deleteBtn.className = 'button button-secondary deleteBtn';
312 deleteBtn.onclick = function () {
313 deleteLogEntry(newRow, date, time, comment, user);
314 };
315 deleteCell.appendChild(deleteBtn);
316
317 if (prepend) {
318 timeLog.prepend(newRow);
319 } else {
320 timeLog.appendChild(newRow);
321 }
322 }
323
324 function deleteLogEntry(row, date, time, comment, user) {
325 if (confirm('Are you sure you want to delete this entry?')) {
326 row.parentNode.removeChild(row);
327
328 fetch(ajaxurl, {
329 method: 'POST',
330 headers: {
331 'Content-Type': 'application/x-www-form-urlencoded',
332 },
333 body: new URLSearchParams({
334 action: 'delete_project_timer_entry',
335 date: date,
336 time_spent: time,
337 comment: comment,
338 user: user,
339 _wpnonce: '<?php echo wp_create_nonce('delete_project_timer_entry'); ?>'
340 })
341 })
342 .then(response => {
343 if (!response.ok) {
344 throw new Error('Network response was not ok');
345 }
346 return response.json();
347 })
348 .then(data => {
349 if (data.success) {
350 console.log('Entry deleted successfully');
351 } else {
352 console.error('Failed to delete entry:', data);
353 }
354 })
355 .catch(error => {
356 console.error('Error deleting entry:', error);
357 });
358
359 const timeParts = time.split(':');
360 const hours = parseInt(timeParts[0], 10);
361 const minutes = parseInt(timeParts[1], 10);
362 const seconds = parseInt(timeParts[2], 10);
363 totalTime -= (hours * 3600) + (minutes * 60) + seconds;
364 updateDatabaseTotalTime(totalTime); // Update total time in database
365 updateTotalTimeDisplay();
366 }
367 }
368
369 function saveLogEntry(date, time, comment, user) {
370 fetch(ajaxurl, {
371 method: 'POST',
372 headers: {
373 'Content-Type': 'application/x-www-form-urlencoded',
374 },
375 body: new URLSearchParams({
376 action: 'save_project_timer_entry',
377 date: date,
378 time_spent: time,
379 comment: comment,
380 user: user,
381 _wpnonce: '<?php echo wp_create_nonce('save_project_timer_entry'); ?>'
382 })
383 })
384 .then(response => {
385 if (!response.ok) {
386 throw new Error('Network response was not ok');
387 }
388 return response.json();
389 })
390 .then(data => {
391 if (data.success) {
392 console.log('Entry saved successfully');
393 } else {
394 console.error('Failed to save entry:', data);
395 }
396 })
397 .catch(error => {
398 console.error('Error saving entry:', error);
399 });
400 }
401
402 function updateTotalTime(time, add) {
403 const timeParts = time.split(':');
404 const hours = parseInt(timeParts[0], 10);
405 const minutes = parseInt(timeParts[1], 10);
406 const seconds = parseInt(timeParts[2], 10);
407
408 const timeInSeconds = (hours * 3600) + (minutes * 60) + seconds;
409 totalTime = add ? totalTime + timeInSeconds : totalTime - timeInSeconds;
410
411 updateDatabaseTotalTime(totalTime);
412 updateTotalTimeDisplay();
413 }
414
415 function updateDatabaseTotalTime(time) {
416 fetch(ajaxurl, {
417 method: 'POST',
418 headers: {
419 'Content-Type': 'application/x-www-form-urlencoded',
420 },
421 body: new URLSearchParams({
422 action: 'update_project_timer_total_time',
423 total_time: time,
424 _wpnonce: '<?php echo wp_create_nonce('update_project_timer_total_time'); ?>'
425 })
426 })
427 .then(response => {
428 if (!response.ok) {
429 throw new Error('Network response was not ok');
430 }
431 return response.json();
432 })
433 .then(data => {
434 if (data.success) {
435 console.log('Total time updated successfully');
436 } else {
437 console.error('Failed to update total time:', data);
438 }
439 })
440 .catch(error => {
441 console.error('Error updating total time:', error);
442 });
443 }
444
445 function updateTotalTimeDisplay() {
446 const totalHours = Math.floor(totalTime / 3600);
447 const totalMinutes = Math.floor((totalTime % 3600) / 60);
448 const totalSeconds = totalTime % 60;
449
450 const formattedHours = (totalHours < 10) ? "0" + totalHours : totalHours;
451 const formattedMinutes = (totalMinutes < 10) ? "0" + totalMinutes : totalMinutes;
452 const formattedSeconds = (totalSeconds < 10) ? "0" + totalSeconds : totalSeconds;
453
454 totalTimeDisplay.innerHTML = formattedHours + ':' + formattedMinutes + ':' + formattedSeconds;
455 }
456
457 function clearLog() {
458 if (confirm('Are you sure you want to clear the log? This action cannot be undone.')) {
459 fetch(ajaxurl, {
460 method: 'POST',
461 headers: {
462 'Content-Type': 'application/x-www-form-urlencoded',
463 },
464 body: new URLSearchParams({
465 action: 'clear_project_timer_log',
466 _wpnonce: '<?php echo wp_create_nonce('clear_project_timer_log'); ?>'
467 })
468 })
469 .then(response => response.json())
470 .then(data => {
471 if (data.success) {
472 console.log('Log cleared successfully');
473 timeLog.innerHTML = ''; // Clear UI table
474 totalTime = 0; // Reset total time
475 updateTotalTimeDisplay(); // Update UI total time
476 } else {
477 console.error('Failed to clear log:', data);
478 }
479 })
480 .catch(error => {
481 console.error('Error clearing log:', error);
482 });
483 }
484 }
485 });
486 </script>
487 <style>
488 body {
489 font-family: Arial, sans-serif;
490 }
491 #stopwatch {
492 font-size: 2em;
493 margin: 20px 0;
494 padding: 10px;
495 background-color: #f0f0f0;
496 border: 1px solid #ddd;
497 border-radius: 5px;
498 display: inline-block;
499 }
500 #buttonContainer {
501 margin-top: 10px;
502 }
503 #startBtn, #stopBtn, #clearLogBtn, #manualEntryBtn, #copyClipboardBtn {
504 margin-right: 10px;
505 margin-top: 10px;
506 }
507 #timeLog {
508 margin-top: 20px;
509 width: 100%;
510 border-collapse: collapse;
511 box-shadow: 0 2px 3px rgba(0,0,0,0.1);
512 }
513 #timeLog th, #timeLog td {
514 border: 1px solid #ddd;
515 padding: 8px;
516 text-align: left;
517 }
518 #timeLog th {
519 background-color: #f8f8f8;
520 }
521 #totalTime {
522 font-weight: bold;
523 font-size: 1.2em;
524 margin-top: 20px;
525 display: inline-block;
526 }
527 .modal {
528 display: none;
529 position: fixed;
530 z-index: 1;
531 padding-top: 100px;
532 left: 0;
533 top: 0;
534 width: 100%;
535 height: 100%;
536 overflow: auto;
537 background-color: rgb(0,0,0);
538 background-color: rgba(0,0,0,0.4);
539 }
540 .modal-content {
541 background-color: #fff;
542 margin: auto;
543 padding: 20px;
544 border: 1px solid #888;
545 width: 80%;
546 border-radius: 5px;
547 box-shadow: 0 5px 15px rgba(0,0,0,0.3);
548 }
549 .close {
550 color: #aaa;
551 float: right;
552 font-size: 28px;
553 font-weight: bold;
554 }
555 .close:hover,
556 .close:focus {
557 color: black;
558 text-decoration: none;
559 cursor: pointer;
560 }
561 .modal-content h2 {
562 border-bottom: 2px solid #ddd;
563 padding-bottom: 10px;
564 margin-bottom: 20px;
565 }
566 .modal-content label {
567 font-weight: bold;
568 margin-right: 10px;
569 }
570 .modal-content input[type="text"],
571 .modal-content input[type="date"],
572 .modal-content textarea {
573 width: calc(100% - 20px);
574 padding: 10px;
575 margin-bottom: 20px;
576 border: 1px solid #ddd;
577 border-radius: 4px;
578 }
579 .modal-content textarea {
580 resize: vertical;
581 }
582 .modal-content input[type="submit"] {
583 padding: 10px 20px;
584 border: none;
585 background-color: #0073aa;
586 color: white;
587 border-radius: 4px;
588 cursor: pointer;
589 }
590 .modal-content input[type="submit"]:hover {
591 background-color: #005a87;
592 }
593 </style>
594 <?php
595}
596
597// AJAX handlers
598add_action('wp_ajax_save_project_timer_entry', 'save_project_timer_entry');
599add_action('wp_ajax_fetch_project_timer_entries', 'fetch_project_timer_entries');
600add_action('wp_ajax_fetch_project_timer_total_time', 'fetch_project_timer_total_time');
601add_action('wp_ajax_delete_project_timer_entry', 'delete_project_timer_entry');
602add_action('wp_ajax_update_project_timer_total_time', 'update_project_timer_total_time');
603add_action('wp_ajax_clear_project_timer_log', 'clear_project_timer_log');
604
605function save_project_timer_entry() {
606 check_ajax_referer('save_project_timer_entry', '_wpnonce');
607
608 if (isset($_POST['date'], $_POST['time_spent'], $_POST['comment'], $_POST['user'])) {
609 global $wpdb;
610 $table_name = $wpdb->prefix . 'project_timer';
611
612 $date = sanitize_text_field($_POST['date']);
613 $time_spent = sanitize_text_field($_POST['time_spent']);
614 $comment = sanitize_textarea_field($_POST['comment']);
615 $user = sanitize_text_field($_POST['user']);
616
617 $wpdb->insert($table_name, [
618 'date' => $date,
619 'time_spent' => $time_spent,
620 'comment' => $comment,
621 'user' => $user
622 ]);
623
624 wp_send_json_success();
625 } else {
626 wp_send_json_error();
627 }
628}
629
630function fetch_project_timer_entries() {
631 check_ajax_referer('fetch_project_timer_entries', '_wpnonce');
632
633 global $wpdb;
634 $table_name = $wpdb->prefix . 'project_timer';
635
636 $entries = $wpdb->get_results("SELECT * FROM $table_name ORDER BY date DESC", ARRAY_A);
637
638 if (!empty($entries)) {
639 wp_send_json_success(['entries' => $entries]);
640 } else {
641 wp_send_json_success(['entries' => []]); // Return an empty array if no entries
642 }
643}
644
645function fetch_project_timer_total_time() {
646 check_ajax_referer('fetch_project_timer_total_time', '_wpnonce');
647
648 $total_time = get_option('project_timer_total_time', 0);
649
650 wp_send_json_success(['total_time' => $total_time]);
651}
652
653function delete_project_timer_entry() {
654 check_ajax_referer('delete_project_timer_entry', '_wpnonce');
655
656 if (isset($_POST['date'], $_POST['time_spent'], $_POST['comment'], $_POST['user'])) {
657 global $wpdb;
658 $table_name = $wpdb->prefix . 'project_timer';
659
660 $date = sanitize_text_field($_POST['date']);
661 $time_spent = sanitize_text_field($_POST['time_spent']);
662 $comment = sanitize_textarea_field($_POST['comment']);
663 $user = sanitize_text_field($_POST['user']);
664
665 $wpdb->delete($table_name, [
666 'date' => $date,
667 'time_spent' => $time_spent,
668 'comment' => $comment,
669 'user' => $user
670 ]);
671
672 wp_send_json_success();
673 } else {
674 wp_send_json_error();
675 }
676}
677
678function update_project_timer_total_time() {
679 check_ajax_referer('update_project_timer_total_time', '_wpnonce');
680
681 if (isset($_POST['total_time'])) {
682 $total_time = intval($_POST['total_time']);
683 update_option('project_timer_total_time', $total_time);
684 wp_send_json_success();
685 } else {
686 wp_send_json_error();
687 }
688}
689
690function clear_project_timer_log() {
691 check_ajax_referer('clear_project_timer_log', '_wpnonce');
692
693 global $wpdb;
694 $table_name = $wpdb->prefix . 'project_timer';
695 $wpdb->query("TRUNCATE TABLE $table_name");
696
697 update_option('project_timer_total_time', 0);
698
699 wp_send_json_success();
700}