From 013c1a74070732ab1df02661a50449c48e52a17e Mon Sep 17 00:00:00 2001 From: Damien Boisvert Date: Fri, 1 Aug 2025 19:46:38 -0700 Subject: [PATCH 1/9] feat: volunteer hour tracking --- _config.yml | 1 + _layouts/default.html | 6 +- _sass/_volunteer-hours.scss | 66 +++++++ assets/css/main.scss | 1 + assets/js/volunteer-hours.js | 325 +++++++++++++++++++++++++++++++++++ pages/hourTool.html | 264 ++++++++++++++++++++++++++++ 6 files changed, 662 insertions(+), 1 deletion(-) create mode 100644 _sass/_volunteer-hours.scss create mode 100644 assets/js/volunteer-hours.js create mode 100644 pages/hourTool.html diff --git a/_config.yml b/_config.yml index 12b4277..8c6387a 100644 --- a/_config.yml +++ b/_config.yml @@ -10,6 +10,7 @@ admin_email: damien@alphagame.dev # TODO: Add this at launch. # tracking_id: +# force_backend_url: "http://192.168.0.167:8000/api" # Temporary for local testing FIXME: Remove this line permalink: /news/:year/:month/:title description: >- # this means to ignore newlines until "baseurl:" St. Anthony de Padua Catholic Parish Confirmation Program, guiding youth diff --git a/_layouts/default.html b/_layouts/default.html index 18f80a0..665d6b2 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -10,7 +10,11 @@ {% if site.tracking_id %} diff --git a/_sass/_volunteer-hours.scss b/_sass/_volunteer-hours.scss new file mode 100644 index 0000000..a9cb8e8 --- /dev/null +++ b/_sass/_volunteer-hours.scss @@ -0,0 +1,66 @@ +/* + * Styles for Volunteer Hours Tracking Tool + */ + +.volunteer-hours { + &-tabs { + .nav-link { + color: #495057; + + &.active { + font-weight: bold; + } + } + } + + &-card { + border-radius: 10px; + overflow: hidden; + + .card-header { + border-bottom: 0; + } + } + + #statsContent { + .card { + transition: all 0.3s ease; + + &:hover { + transform: translateY(-3px); + box-shadow: 0 4px 8px rgba(0,0,0,0.1); + } + } + + h5 { + color: #495057; + } + } + + #statsEmptyState { + color: #6c757d; + transition: all 0.3s ease; + + i { + opacity: 0.7; + } + } + + // Mobile optimizations + @media (max-width: 768px) { + .nav-tabs .nav-link { + padding: .5rem .75rem; + font-size: 0.9rem; + } + + .card-body { + padding: 1rem; + } + + #statsContent { + .row > div { + margin-bottom: 1rem; + } + } + } +} diff --git a/assets/css/main.scss b/assets/css/main.scss index 0c713de..f0fd972 100644 --- a/assets/css/main.scss +++ b/assets/css/main.scss @@ -18,6 +18,7 @@ @use "people"; @use "healthcheck"; @use "registration"; +@use "volunteer-hours"; // note - for some gosh darn reason, if I include the following blockcomment at the top, // it will repeat the comment a million times in the compiled CSS. diff --git a/assets/js/volunteer-hours.js b/assets/js/volunteer-hours.js new file mode 100644 index 0000000..aeec2f9 --- /dev/null +++ b/assets/js/volunteer-hours.js @@ -0,0 +1,325 @@ +/** + * Volunteer Hours Tracking Tool + * + * Handles all functionality for the volunteer hours tracking system including: + * - Logging volunteer hours + * - Creating new volunteer accounts + * - Viewing volunteer hour logs + */ + +document.addEventListener('DOMContentLoaded', function() { + const backendURL = window.backendBaseURL + '/volunteer_hours'; + + // ========================================== + // Helper Functions + // ========================================== + + /** + * Makes a fetch request to the API with error handling + * @param {string} endpoint - API endpoint to fetch from + * @param {Object} options - Fetch options (method, headers, body) + * @returns {Promise} - Promise that resolves to the JSON response + */ + function fetchWithErrorHandling(endpoint, options = {}) { + return fetch(backendURL + endpoint, options) + .then(response => { + if (!response.ok) { + return response.json().then(errorData => { + throw new Error(errorData.error || `Failed request - ${response.statusText}`); + }).catch(e => { + // If response.json() fails, just use the status text + if (e instanceof SyntaxError) { + throw new Error(`Failed request: ${response.statusText}`); + } + throw e; + }); + } + return response.json(); + }); + } + + /** + * Shows a message to the user and hides it after 5 seconds + * @param {string} elementId - ID of the element to show message in + * @param {string} message - Message to display + */ + function showMessage(elementId, message) { + const element = document.getElementById(elementId); + element.textContent = message; + element.classList.remove('d-none'); + + // Hide message after 5 seconds + setTimeout(() => { + element.classList.add('d-none'); + }, 5000); + } + + /** + * Formats a date string to a readable format + * @param {string} dateString - Date string in YYYY-MM-DD format + * @returns {string} - Formatted date string + */ + function formatDate(dateString) { + const dateObj = new Date(dateString); + return dateObj.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); + } + + /** + * Populates a select element with user options + * @param {string} selectId - ID of the select element to populate + * @param {Array} users - Array of user objects + * @param {string} defaultText - Default option text + */ + function populateUserSelect(selectId, users, defaultText = "Select volunteer...") { + const selectElement = document.getElementById(selectId); + // Clear existing options + selectElement.innerHTML = ``; + + // Add users to dropdown + users.forEach(user => { + const option = document.createElement('option'); + option.value = user.id; + option.textContent = user.name; + selectElement.appendChild(option); + }); + } + + // ========================================== + // Main Code + // ========================================== + + // Load users for dropdown + loadUsers(); + + // Set default date to today + const today = new Date().toISOString().split('T')[0]; + document.getElementById('dateInput').value = today; + + // Event listeners for forms + document.getElementById('logHoursForm').addEventListener('submit', logHours); + document.getElementById('createUserForm').addEventListener('submit', createUser); + + // Load hours data when view tab is shown + document.getElementById('view-hours-tab').addEventListener('click', loadHoursData); + + // Setup stats tab functionality + document.getElementById('user-stats-tab').addEventListener('click', function() { + // Populate the stats user dropdown if it's empty + const statsUserSelect = document.getElementById('statsUserSelect'); + if (statsUserSelect.options.length <= 1) { + populateStatsUserDropdown(); + } + }); + + // Add event listener for the load stats button + document.getElementById('loadStatsBtn').addEventListener('click', loadUserStats); + + // Function to load users + function loadUsers() { + fetchWithErrorHandling('/users') + .then(users => { + populateUserSelect('userSelect', users); + }) + .catch(error => { + console.error('Error loading users:', error); + }); + } + + // Function to log hours + function logHours(event) { + event.preventDefault(); + + const userId = document.getElementById('userSelect').value; + const date = document.getElementById('dateInput').value; + const hours = document.getElementById('hoursInput').value; + const notes = document.getElementById('notesInput').value; + + // Validate form data + if (!userId || !date || !hours) { + showMessage('logHoursError', 'Please fill out all required fields.'); + return; + } + + const hoursData = { + user_id: parseInt(userId), + date: date, + hours: parseFloat(hours), + notes: notes + }; + + fetchWithErrorHandling('/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(hoursData) + }) + .then(data => { + // Show success message + showMessage('logHoursSuccess', 'Hours logged successfully!'); + + // Reset form + document.getElementById('logHoursForm').reset(); + document.getElementById('dateInput').value = today; + }) + .catch(error => { + console.error('Error logging hours:', error); + showMessage('logHoursError', error.message || 'Error logging hours. Please try again.'); + }); + } + + // Function to create user + function createUser(event) { + event.preventDefault(); + + const name = document.getElementById('nameInput').value; + const email = document.getElementById('emailInput').value; + const phone = document.getElementById('phoneInput').value; + + // Validate form data + if (!name || !email) { + showMessage('createUserError', 'Please fill out all required fields.'); + return; + } + + const userData = { + name: name, + email: email, + phone: phone + }; + + fetchWithErrorHandling('/users', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(userData) + }) + .then(data => { + // Show success message + showMessage('createUserSuccess', 'Volunteer registered successfully!'); + + // Reset form + document.getElementById('createUserForm').reset(); + + // Reload users dropdown + loadUsers(); + }) + .catch(error => { + console.error('Error creating user:', error); + showMessage('createUserError', error.message || 'Error registering volunteer. Please try again.'); + }); + } + + // Function to load hours data + function loadHoursData() { + fetchWithErrorHandling('/all') + .then(hoursData => { + const tableBody = document.getElementById('hoursTable').querySelector('tbody'); + tableBody.innerHTML = ''; + + if (hoursData.length === 0) { + document.getElementById('noHoursData').classList.remove('d-none'); + return; + } + + document.getElementById('noHoursData').classList.add('d-none'); + + // Sort data by date (newest first) + hoursData.sort((a, b) => new Date(b.date) - new Date(a.date)); + + // Add rows to table + hoursData.forEach(entry => { + const row = document.createElement('tr'); + + row.innerHTML = ` + ${entry.name} + ${formatDate(entry.date)} + ${entry.hours} + ${entry.notes || ''} + `; + + tableBody.appendChild(row); + }); + }) + .catch(error => { + console.error('Error loading hours data:', error); + document.getElementById('viewHoursError').classList.remove('d-none'); + document.getElementById('viewHoursError').textContent = error.message || 'Error retrieving volunteer hours. Please try again.'; + }); + } + // Function to populate the stats user dropdown + function populateStatsUserDropdown() { + fetchWithErrorHandling('/users') + .then(users => { + populateUserSelect('statsUserSelect', users, 'Choose volunteer...'); + }) + .catch(error => { + console.error('Error loading users for stats:', error); + showMessage('statsError', error.message || 'Error loading volunteers. Please try again.'); + }); + } + + // Function to load user stats + function loadUserStats() { + const userId = document.getElementById('statsUserSelect').value; + + if (!userId) { + showMessage('statsError', 'Please select a volunteer to view their stats.'); + return; + } + + // Show loading state + document.getElementById('statsContent').classList.add('d-none'); + document.getElementById('statsEmptyState').classList.add('d-none'); + document.getElementById('statsLoading').classList.remove('d-none'); + document.getElementById('statsError').classList.add('d-none'); + + fetchWithErrorHandling('/view/' + userId) + .then(userData => { + // Hide loading, show content + document.getElementById('statsLoading').classList.add('d-none'); + document.getElementById('statsContent').classList.remove('d-none'); + + // Update the stats display + document.getElementById('statsName').textContent = userData.name; + document.getElementById('statsTotalHours').textContent = userData.total_hours + ' hours'; + + // Populate the history table + const tableBody = document.getElementById('userHistoryTable').querySelector('tbody'); + tableBody.innerHTML = ''; + + if (userData.history.length === 0) { + const row = document.createElement('tr'); + row.innerHTML = 'No volunteer hours recorded yet'; + tableBody.appendChild(row); + } else { + // Sort history by date (newest first) + userData.history.sort((a, b) => new Date(b.date) - new Date(a.date)); + + // Add rows to table + userData.history.forEach(entry => { + const row = document.createElement('tr'); + + row.innerHTML = ` + ${formatDate(entry.date)} + ${entry.hours} + ${entry.notes || ''} + `; + + tableBody.appendChild(row); + }); + } + }) + .catch(error => { + console.error('Error loading volunteer stats:', error); + document.getElementById('statsLoading').classList.add('d-none'); + document.getElementById('statsEmptyState').classList.remove('d-none'); + showMessage('statsError', error.message || 'Error retrieving volunteer stats. Please try again.'); + }); + } +}); diff --git a/pages/hourTool.html b/pages/hourTool.html new file mode 100644 index 0000000..262b770 --- /dev/null +++ b/pages/hourTool.html @@ -0,0 +1,264 @@ +--- +layout: default +title: Volunteer Hour Tracking +permalink: /hours +--- + +
+ + + + +
+ +
+
+
+
Log Volunteer Hours
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ Hours logged successfully! +
+
+ Error logging hours. Please try again. +
+
+
+
+ + +
+
+
+
Register New Volunteer
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ Volunteer registered successfully! +
+
+ Error registering volunteer. Please try again. +
+
+
+
+ + +
+
+
+
Volunteer Hours Log
+
+
+
+ + + + + + + + + + + + +
NameDateHoursNotes
+
+
+ No volunteer hours have been logged yet. +
+
+ Error retrieving volunteer hours. Please try again. +
+
+
+
+
+ + +
+
+
+
My Volunteer Stats
+
+
+
+ +
+ + +
+
+ + +
+
+
+
+
+
Volunteer Name
+

-

+
+
+
+
+
+
+
Total Hours
+

-

+
+
+
+
+ +
Volunteer History
+
+ + + + + + + + + + + +
DateHoursNotes
+
+
+ + +
+ +

Select a volunteer to view their stats

+
+ + +
+
+ Loading... +
+

Loading volunteer stats...

+
+ + +
+ Error retrieving volunteer stats. Please try again. +
+
+
+
+
+ + + + From f88ccf1c59fd0d8f848474cf0be82bb1fb3ebf1d Mon Sep 17 00:00:00 2001 From: Damien Boisvert Date: Sat, 2 Aug 2025 21:34:35 -0700 Subject: [PATCH 2/9] feat: volunteer page: show more information --- _config.yml | 1 + _sass/_volunteer-hours.scss | 32 +++++++++++ assets/js/volunteer-hours.js | 102 +++++++++++++++++++++++++++++++++-- pages/hourTool.html | 73 ++++++++++++------------- 4 files changed, 168 insertions(+), 40 deletions(-) diff --git a/_config.yml b/_config.yml index 8c6387a..babba3d 100644 --- a/_config.yml +++ b/_config.yml @@ -21,6 +21,7 @@ description: >- # this means to ignore newlines until "baseurl:" instagram_username: stanthonyyouth.novato publisher: "St. Anthony de Padua Catholic Parish" + plugins: - jekyll-feed - jekyll-sitemap diff --git a/_sass/_volunteer-hours.scss b/_sass/_volunteer-hours.scss index a9cb8e8..aeae0fb 100644 --- a/_sass/_volunteer-hours.scss +++ b/_sass/_volunteer-hours.scss @@ -63,4 +63,36 @@ } } } + + // Footer styles (only show on computer horizontal screen) + // Card styles + &-card { + border-radius: 10px; + overflow: hidden; + + .card-header { + border-bottom: 0; + } + + .card-footer { + padding: 0.5rem 1rem; + border-top: 1px solid rgba(0,0,0,0.05); + } + } + + // Footer styles + &-footer { + font-size: 0.75rem; + opacity: 0.7; + font-style: italic; + transition: opacity 0.3s ease; + + .d-block { + line-height: 1.4; + } + + &:hover { + opacity: 1; + } + } } diff --git a/assets/js/volunteer-hours.js b/assets/js/volunteer-hours.js index aeec2f9..bd0f492 100644 --- a/assets/js/volunteer-hours.js +++ b/assets/js/volunteer-hours.js @@ -7,24 +7,118 @@ * - Viewing volunteer hour logs */ +var apiRequests = { + total: 0, + success: 0, + failed: 0, + post: 0 +}; + +function cleanVersion(version) { + // remove .dXXXXXXXX, if present + const cleaned = version.replace(/\.d[0-9a-f]+$/, ''); + console.log(`cleanVersion: ${version} -> ${cleaned}`); + return cleaned; +} + +function wrappedFetch(endpoint, options = {}) { + // Ensure headers exist in options + if (!options.headers) { + options.headers = {}; + } + + // Track POST requests + if (options.method === 'POST') { + apiRequests.post++; + } + + return fetch(endpoint, options) + .then(response => { + apiRequests.success++; + apiRequests.total = apiRequests.success + apiRequests.failed; + + // Update all API request counters + updateAllElements('apiRequests', apiRequests.total); + updateAllElements('postRequests', apiRequests.post); + updateAllElements('failedRequests', apiRequests.failed); + return response; + }) + .catch(error => { + // Track failed requests + apiRequests.failed++; + apiRequests.total = apiRequests.success + apiRequests.failed; + + // Update counters + updateAllElements('apiRequests', apiRequests.total); + updateAllElements('failedRequests', apiRequests.failed); + + // Re-throw the error for handling elsewhere + throw error; + }); +} + +// Helper function to update all elements with the same base ID +function updateAllElements(baseId, value) { + // Find all elements that start with the baseId + for (let i = 1; i <= 4; i++) { + const element = document.getElementById(baseId + i); + if (element) { + element.textContent = value; + } + } + // Also update the original element if it exists (for backward compatibility) + const originalElement = document.getElementById(baseId); + if (originalElement) { + originalElement.textContent = value; + } +} + document.addEventListener('DOMContentLoaded', function() { const backendURL = window.backendBaseURL + '/volunteer_hours'; + wrappedFetch(window.backendBaseURL + "/healthcheck?src=VolunteerHoursTool&c=1") + .then(response => { + return response.json(); + }) + .then(data => { + // Update all version elements + updateAllElements('backendVersion', cleanVersion(data.version) || 'Unknown'); + }) + .catch(error => { + console.error('Error fetching backend version:', error); + }); + // ========================================== // Helper Functions // ========================================== /** - * Makes a fetch request to the API with error handling + * Makes a fetch request to the API with error handling and no caching * @param {string} endpoint - API endpoint to fetch from * @param {Object} options - Fetch options (method, headers, body) * @returns {Promise} - Promise that resolves to the JSON response */ function fetchWithErrorHandling(endpoint, options = {}) { - return fetch(backendURL + endpoint, options) + // Ensure headers exist in options + if (!options.headers) { + options.headers = {}; + } + + // Add cache control headers to prevent caching + options.headers = { + ...options.headers, + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + }; + + return wrappedFetch(backendURL + endpoint, options) .then(response => { if (!response.ok) { return response.json().then(errorData => { + // Track failed requests in fetchWithErrorHandling too + apiRequests.failed++; + updateAllElements('failedRequests', apiRequests.failed); throw new Error(errorData.error || `Failed request - ${response.statusText}`); }).catch(e => { // If response.json() fails, just use the status text @@ -235,7 +329,7 @@ document.addEventListener('DOMContentLoaded', function() { // Add rows to table hoursData.forEach(entry => { const row = document.createElement('tr'); - + row.setAttribute('title', `ID ${entry.id}`); row.innerHTML = ` ${entry.name} ${formatDate(entry.date)} @@ -304,7 +398,7 @@ document.addEventListener('DOMContentLoaded', function() { // Add rows to table userData.history.forEach(entry => { const row = document.createElement('tr'); - + row.setAttribute('title', `ID ${entry.id}`); row.innerHTML = ` ${formatDate(entry.date)} ${entry.hours} diff --git a/pages/hourTool.html b/pages/hourTool.html index 262b770..5ff372a 100644 --- a/pages/hourTool.html +++ b/pages/hourTool.html @@ -3,42 +3,7 @@ title: Volunteer Hour Tracking permalink: /hours --- -
@@ -126,7 +100,7 @@
Register New Volunteer
- +
@@ -141,6 +115,15 @@
Register New Volunteer
Error registering volunteer. Please try again.
+ + + @@ -173,6 +156,15 @@
Volunteer Hours Log
Error retrieving volunteer hours. Please try again.
+ + + @@ -254,6 +246,15 @@
Volunteer History
Error retrieving volunteer stats. Please try again.
+ + + From 6c68bf7e86a61e7aab60a0a8a3df1e55a7c5d349 Mon Sep 17 00:00:00 2001 From: Damien Boisvert Date: Sat, 2 Aug 2025 21:50:44 -0700 Subject: [PATCH 3/9] refactor: Put it all in reusable components --- _includes/volunteer-hours/card-footer.html | 8 + .../volunteer-hours/create-user-form.html | 26 ++ _includes/volunteer-hours/log-hours-form.html | 33 ++ _includes/volunteer-hours/tab-card.html | 10 + _includes/volunteer-hours/tabs-nav.html | 23 ++ _includes/volunteer-hours/user-stats.html | 70 +++++ .../volunteer-hours/view-hours-table.html | 22 ++ pages/hourTool.html | 283 +++--------------- 8 files changed, 239 insertions(+), 236 deletions(-) create mode 100644 _includes/volunteer-hours/card-footer.html create mode 100644 _includes/volunteer-hours/create-user-form.html create mode 100644 _includes/volunteer-hours/log-hours-form.html create mode 100644 _includes/volunteer-hours/tab-card.html create mode 100644 _includes/volunteer-hours/tabs-nav.html create mode 100644 _includes/volunteer-hours/user-stats.html create mode 100644 _includes/volunteer-hours/view-hours-table.html diff --git a/_includes/volunteer-hours/card-footer.html b/_includes/volunteer-hours/card-footer.html new file mode 100644 index 0000000..3856e15 --- /dev/null +++ b/_includes/volunteer-hours/card-footer.html @@ -0,0 +1,8 @@ + + diff --git a/_includes/volunteer-hours/create-user-form.html b/_includes/volunteer-hours/create-user-form.html new file mode 100644 index 0000000..c1331eb --- /dev/null +++ b/_includes/volunteer-hours/create-user-form.html @@ -0,0 +1,26 @@ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ Volunteer registered successfully! +
+
+ Error registering volunteer. Please try again. +
diff --git a/_includes/volunteer-hours/log-hours-form.html b/_includes/volunteer-hours/log-hours-form.html new file mode 100644 index 0000000..7b45a43 --- /dev/null +++ b/_includes/volunteer-hours/log-hours-form.html @@ -0,0 +1,33 @@ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ Hours logged successfully! +
+
+ Error logging hours. Please try again. +
diff --git a/_includes/volunteer-hours/tab-card.html b/_includes/volunteer-hours/tab-card.html new file mode 100644 index 0000000..7a41ce7 --- /dev/null +++ b/_includes/volunteer-hours/tab-card.html @@ -0,0 +1,10 @@ + +
+
+
{{ include.title }}
+
+
+ {{ include.content }} +
+ {% include volunteer-hours/card-footer.html id=include.footer_id %} +
diff --git a/_includes/volunteer-hours/tabs-nav.html b/_includes/volunteer-hours/tabs-nav.html new file mode 100644 index 0000000..4108f60 --- /dev/null +++ b/_includes/volunteer-hours/tabs-nav.html @@ -0,0 +1,23 @@ + + diff --git a/_includes/volunteer-hours/user-stats.html b/_includes/volunteer-hours/user-stats.html new file mode 100644 index 0000000..8285c7d --- /dev/null +++ b/_includes/volunteer-hours/user-stats.html @@ -0,0 +1,70 @@ + +
+ +
+ + +
+
+ + +
+
+
+
+
+
Volunteer Name
+

-

+
+
+
+
+
+
+
Total Hours
+

-

+
+
+
+
+ +
Volunteer History
+
+ + + + + + + + + + + +
DateHoursNotes
+
+
+ + +
+ +

Select a volunteer to view their stats

+
+ + +
+
+ Loading... +
+

Loading volunteer stats...

+
+ + +
+ Error retrieving volunteer stats. Please try again. +
diff --git a/_includes/volunteer-hours/view-hours-table.html b/_includes/volunteer-hours/view-hours-table.html new file mode 100644 index 0000000..1b22f4d --- /dev/null +++ b/_includes/volunteer-hours/view-hours-table.html @@ -0,0 +1,22 @@ + +
+ + + + + + + + + + + + +
NameDateHoursNotes
+
+
+ No volunteer hours have been logged yet. +
+
+ Error retrieving volunteer hours. Please try again. +
diff --git a/pages/hourTool.html b/pages/hourTool.html index 5ff372a..b763731 100644 --- a/pages/hourTool.html +++ b/pages/hourTool.html @@ -5,261 +5,72 @@ ---
- - + + {% include volunteer-hours/tabs-nav.html %}
-
-
-
Log Volunteer Hours
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
-
- Hours logged successfully! -
-
- Error logging hours. Please try again. -
- - - -
-
+ {% capture log_hours_content %} + {% include volunteer-hours/log-hours-form.html %} + {% endcapture %} + + {% include volunteer-hours/tab-card.html + title="Log Volunteer Hours" + icon_class="fas fa-clock" + header_class="bg-primary text-white" + content=log_hours_content + footer_id="1" + %}
-
-
-
Register New Volunteer
-
-
-
-
- - -
-
- - -
-
- - -
-
- -
-
-
- Volunteer registered successfully! -
-
- Error registering volunteer. Please try again. -
- - - -
-
+ {% capture create_user_content %} + {% include volunteer-hours/create-user-form.html %} + {% endcapture %} + + {% include volunteer-hours/tab-card.html + title="Register New Volunteer" + icon_class="fas fa-user-plus" + header_class="bg-success text-white" + content=create_user_content + footer_id="2" + %}
-
-
-
Volunteer Hours Log
-
-
-
- - - - - - - - - - - - -
NameDateHoursNotes
-
-
- No volunteer hours have been logged yet. -
-
- Error retrieving volunteer hours. Please try again. -
- - - -
-
+ {% capture view_hours_content %} + {% include volunteer-hours/view-hours-table.html %} + {% endcapture %} + + {% include volunteer-hours/tab-card.html + title="Volunteer Hours Log" + icon_class="fas fa-table" + header_class="bg-info text-white" + content=view_hours_content + footer_id="3" + %}
-
-
-
-
My Volunteer Stats
-
-
-
- -
- - -
-
- - -
-
-
-
-
-
Volunteer Name
-

-

-
-
-
-
-
-
-
Total Hours
-

-

-
-
-
-
- -
Volunteer History
-
- - - - - - - - - - - -
DateHoursNotes
-
-
- - -
- -

Select a volunteer to view their stats

-
- - -
-
- Loading... -
-

Loading volunteer stats...

-
- - -
- Error retrieving volunteer stats. Please try again. -
- - - -
-
+ {% capture user_stats_content %} + {% include volunteer-hours/user-stats.html %} + {% endcapture %} + + {% include volunteer-hours/tab-card.html + title="My Volunteer Stats" + icon_class="fas fa-chart-bar" + header_class="bg-purple text-white" + header_style="background-color: #6f42c1;" + content=user_stats_content + footer_id="4" + %}
- From b3f5ee2876c966c1c1c24065e2a16a89a54d7170 Mon Sep 17 00:00:00 2001 From: Damien Boisvert Date: Sat, 2 Aug 2025 21:59:24 -0700 Subject: [PATCH 4/9] feat: update user list upon tab change --- assets/js/volunteer-hours.js | 54 ++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/assets/js/volunteer-hours.js b/assets/js/volunteer-hours.js index bd0f492..0282b03 100644 --- a/assets/js/volunteer-hours.js +++ b/assets/js/volunteer-hours.js @@ -88,6 +88,12 @@ document.addEventListener('DOMContentLoaded', function() { console.error('Error fetching backend version:', error); }); + // Track the currently selected users in each dropdown + const selectedUsers = { + userSelect: '', + statsUserSelect: '' + }; + // ========================================== // Helper Functions // ========================================== @@ -170,6 +176,10 @@ document.addEventListener('DOMContentLoaded', function() { */ function populateUserSelect(selectId, users, defaultText = "Select volunteer...") { const selectElement = document.getElementById(selectId); + // Remember currently selected value if any + const currentValue = selectElement.value; + selectedUsers[selectId] = currentValue; + // Clear existing options selectElement.innerHTML = ``; @@ -178,8 +188,19 @@ document.addEventListener('DOMContentLoaded', function() { const option = document.createElement('option'); option.value = user.id; option.textContent = user.name; + + // If this was the previously selected value, keep it selected + if (user.id == currentValue) { + option.selected = true; + } + selectElement.appendChild(option); }); + + // If there was a previously selected value that's no longer in the list + if (currentValue && selectElement.value !== currentValue) { + console.log(`Previously selected user (${currentValue}) no longer available.`); + } } // ========================================== @@ -197,18 +218,32 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('logHoursForm').addEventListener('submit', logHours); document.getElementById('createUserForm').addEventListener('submit', createUser); + // Add tab change event listeners to all tabs + const tabButtons = document.querySelectorAll('[data-bs-toggle="tab"]'); + tabButtons.forEach(button => { + button.addEventListener('shown.bs.tab', function(event) { + // Get the newly activated tab + const activeTab = event.target.getAttribute('data-bs-target'); + console.log(`Tab changed to: ${activeTab}`); + + // Reload the appropriate dropdown based on which tab is now active + switch(activeTab) { + case '#log-hours': + loadUsers(); // Reload the users dropdown for logging hours + break; + case '#view-hours': + loadHoursData(); // Load hours data when view tab is shown + break; + case '#user-stats': + populateStatsUserDropdown(); // Reload the stats user dropdown + break; + } + }); + }); + // Load hours data when view tab is shown document.getElementById('view-hours-tab').addEventListener('click', loadHoursData); - // Setup stats tab functionality - document.getElementById('user-stats-tab').addEventListener('click', function() { - // Populate the stats user dropdown if it's empty - const statsUserSelect = document.getElementById('statsUserSelect'); - if (statsUserSelect.options.length <= 1) { - populateStatsUserDropdown(); - } - }); - // Add event listener for the load stats button document.getElementById('loadStatsBtn').addEventListener('click', loadUserStats); @@ -216,6 +251,7 @@ document.addEventListener('DOMContentLoaded', function() { function loadUsers() { fetchWithErrorHandling('/users') .then(users => { + // Populate the log hours dropdown populateUserSelect('userSelect', users); }) .catch(error => { From 4034b0883ddd9dd4e7aff831abe66eb1cb4f7daa Mon Sep 17 00:00:00 2001 From: Damien Boisvert Date: Sat, 2 Aug 2025 22:05:01 -0700 Subject: [PATCH 5/9] refactor: use window.location.hash instead --- assets/js/volunteer-hours.js | 75 ++++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 15 deletions(-) diff --git a/assets/js/volunteer-hours.js b/assets/js/volunteer-hours.js index 0282b03..5d1ff57 100644 --- a/assets/js/volunteer-hours.js +++ b/assets/js/volunteer-hours.js @@ -93,6 +93,46 @@ document.addEventListener('DOMContentLoaded', function() { userSelect: '', statsUserSelect: '' }; + + // Tab management object + const tabManager = { + // Get the current tab ID from URL hash or default to 'log-hours' + getCurrentTabId: function() { + if (window.location.hash) { + return window.location.hash.substring(1); // Remove the # from the hash + } + return 'log-hours'; // Default tab + }, + + // Update URL hash without scrolling + setTabInUrl: function(tabId) { + history.replaceState(null, null, `#${tabId}`); + }, + + // Activate a tab by ID + activateTab: function(tabId) { + const tabToActivate = document.querySelector(`[data-bs-target="#${tabId}"]`); + if (tabToActivate) { + const tab = new bootstrap.Tab(tabToActivate); + tab.show(); + } + }, + + // Handle tab content loading based on tab ID + loadTabContent: function(tabId) { + switch(tabId) { + case 'log-hours': + loadUsers(); + break; + case 'view-hours': + loadHoursData(); + break; + case 'user-stats': + populateStatsUserDropdown(); + break; + } + } + }; // ========================================== // Helper Functions @@ -214,6 +254,17 @@ document.addEventListener('DOMContentLoaded', function() { const today = new Date().toISOString().split('T')[0]; document.getElementById('dateInput').value = today; + // Initialize tab from URL hash if present + const initialTabId = tabManager.getCurrentTabId(); + if (initialTabId !== 'log-hours') { // Only if not the default tab + // Use setTimeout to avoid scroll behavior + setTimeout(() => { + window.scrollTo(0, 0); // Ensure no scrolling + tabManager.activateTab(initialTabId); + tabManager.loadTabContent(initialTabId); + }, 0); + } + // Event listeners for forms document.getElementById('logHoursForm').addEventListener('submit', logHours); document.getElementById('createUserForm').addEventListener('submit', createUser); @@ -224,25 +275,19 @@ document.addEventListener('DOMContentLoaded', function() { button.addEventListener('shown.bs.tab', function(event) { // Get the newly activated tab const activeTab = event.target.getAttribute('data-bs-target'); - console.log(`Tab changed to: ${activeTab}`); + const tabId = activeTab.substring(1); // Remove the # from the selector + console.log(`Tab changed to: ${tabId}`); - // Reload the appropriate dropdown based on which tab is now active - switch(activeTab) { - case '#log-hours': - loadUsers(); // Reload the users dropdown for logging hours - break; - case '#view-hours': - loadHoursData(); // Load hours data when view tab is shown - break; - case '#user-stats': - populateStatsUserDropdown(); // Reload the stats user dropdown - break; - } + // Update URL hash without scrolling + tabManager.setTabInUrl(tabId); + + // Load the appropriate content for this tab + tabManager.loadTabContent(tabId); }); }); - // Load hours data when view tab is shown - document.getElementById('view-hours-tab').addEventListener('click', loadHoursData); + // Remove duplicate event listeners as they're now handled by tabManager + // The "click" handlers aren't needed as we already have "shown.bs.tab" handlers // Add event listener for the load stats button document.getElementById('loadStatsBtn').addEventListener('click', loadUserStats); From 6682f3a194b70177d071c9d17fc4d791ade4f60e Mon Sep 17 00:00:00 2001 From: Damien Boisvert Date: Sat, 2 Aug 2025 22:18:47 -0700 Subject: [PATCH 6/9] feat: better animations that make more sense --- _sass/_volunteer-hours.scss | 65 +++++++++++++++++++++++++++++ assets/js/volunteer-hours.js | 80 ++++++++++++++++++++++++++++++++++-- 2 files changed, 142 insertions(+), 3 deletions(-) diff --git a/_sass/_volunteer-hours.scss b/_sass/_volunteer-hours.scss index aeae0fb..4fa18b2 100644 --- a/_sass/_volunteer-hours.scss +++ b/_sass/_volunteer-hours.scss @@ -3,12 +3,71 @@ */ .volunteer-hours { + /* Custom tab transitions */ + .tab-content { + .tab-pane { + /* Override Bootstrap's default fade transition */ + display: block !important; + position: relative; + opacity: 0; + visibility: hidden; + height: auto; + transition: transform 0.35s ease-out, opacity 0.25s ease-out; + transform: translateX(20px); + + /* Position tabs as absolute to prevent layout jumps */ + position: absolute; + width: 100%; + + /* Active tab styling */ + &.active { + opacity: 1; + visibility: visible; + transform: translateX(0); + position: relative; + z-index: 5; + } + + /* Add directional animations based on tab order */ + &.sliding-left { + transform: translateX(-20px); + } + + &.sliding-right { + transform: translateX(20px); + } + } + } + &-tabs { .nav-link { color: #495057; + position: relative; + transition: all 0.2s ease; + + /* Animated underline indicator for tabs */ + &::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 2px; + background: currentColor; + transition: all 0.3s ease; + transform: translateX(-50%); + } &.active { font-weight: bold; + + &::after { + width: 80%; + } + } + + &:hover:not(.active)::after { + width: 40%; } } } @@ -16,6 +75,12 @@ &-card { border-radius: 10px; overflow: hidden; + transition: transform 0.3s ease, box-shadow 0.3s ease; + + &:hover { + transform: translateY(-3px); + box-shadow: 0 5px 15px rgba(0,0,0,0.08); + } .card-header { border-bottom: 0; diff --git a/assets/js/volunteer-hours.js b/assets/js/volunteer-hours.js index 5d1ff57..2f6b08f 100644 --- a/assets/js/volunteer-hours.js +++ b/assets/js/volunteer-hours.js @@ -96,6 +96,13 @@ document.addEventListener('DOMContentLoaded', function() { // Tab management object const tabManager = { + // Keep track of current and previous tab for direction-aware animations + previousTabId: null, + currentTabId: 'log-hours', + + // Tab order for determining animation direction + tabOrder: ['log-hours', 'create-user', 'view-hours', 'user-stats'], + // Get the current tab ID from URL hash or default to 'log-hours' getCurrentTabId: function() { if (window.location.hash) { @@ -109,10 +116,56 @@ document.addEventListener('DOMContentLoaded', function() { history.replaceState(null, null, `#${tabId}`); }, - // Activate a tab by ID + // Determine if we're moving left or right in tab order + getAnimationDirection: function(newTabId) { + const previousIndex = this.tabOrder.indexOf(this.currentTabId); + const newIndex = this.tabOrder.indexOf(newTabId); + + // Update tab tracking + this.previousTabId = this.currentTabId; + this.currentTabId = newTabId; + + return (newIndex > previousIndex) ? 'right' : 'left'; + }, + + // Apply sliding animation classes based on direction + animateTabTransition: function(newTabId) { + const direction = this.getAnimationDirection(newTabId); + const newTab = document.getElementById(newTabId); + const oldTab = document.getElementById(this.previousTabId); + + if (oldTab && newTab) { + // First remove any existing animation classes + document.querySelectorAll('.tab-pane').forEach(pane => { + pane.classList.remove('sliding-left', 'sliding-right'); + }); + + // Set direction classes for nice animation + if (direction === 'right') { + oldTab.classList.add('sliding-left'); + newTab.classList.add('sliding-right'); + } else { + oldTab.classList.add('sliding-right'); + newTab.classList.add('sliding-left'); + } + + // Remove animation classes after transition completes + setTimeout(() => { + document.querySelectorAll('.tab-pane').forEach(pane => { + pane.classList.remove('sliding-left', 'sliding-right'); + }); + }, 400); + } + }, + + // Activate a tab by ID with animation activateTab: function(tabId) { const tabToActivate = document.querySelector(`[data-bs-target="#${tabId}"]`); if (tabToActivate) { + // Set up animation before tab changes + this.animateTabTransition(tabId); + + // Show the tab const tab = new bootstrap.Tab(tabToActivate); tab.show(); } @@ -256,12 +309,22 @@ document.addEventListener('DOMContentLoaded', function() { // Initialize tab from URL hash if present const initialTabId = tabManager.getCurrentTabId(); + + // Set current tab in tab manager to ensure animations work correctly + tabManager.currentTabId = initialTabId; + if (initialTabId !== 'log-hours') { // Only if not the default tab // Use setTimeout to avoid scroll behavior setTimeout(() => { window.scrollTo(0, 0); // Ensure no scrolling - tabManager.activateTab(initialTabId); - tabManager.loadTabContent(initialTabId); + + // For initial load, we just activate the tab without animation + const tabToActivate = document.querySelector(`[data-bs-target="#${initialTabId}"]`); + if (tabToActivate) { + const tab = new bootstrap.Tab(tabToActivate); + tab.show(); + tabManager.loadTabContent(initialTabId); + } }, 0); } @@ -272,6 +335,17 @@ document.addEventListener('DOMContentLoaded', function() { // Add tab change event listeners to all tabs const tabButtons = document.querySelectorAll('[data-bs-toggle="tab"]'); tabButtons.forEach(button => { + // Listen for the 'show.bs.tab' event which fires BEFORE tab is shown + button.addEventListener('show.bs.tab', function(event) { + // Get the newly activated tab + const activeTab = event.target.getAttribute('data-bs-target'); + const tabId = activeTab.substring(1); // Remove the # from the selector + + // Set up animation before the tab change happens + tabManager.animateTabTransition(tabId); + }); + + // Listen for the 'shown.bs.tab' event which fires AFTER tab is shown button.addEventListener('shown.bs.tab', function(event) { // Get the newly activated tab const activeTab = event.target.getAttribute('data-bs-target'); From f0ca19f509ae9e865694b12d80f00f20be60c038 Mon Sep 17 00:00:00 2001 From: Damien Boisvert Date: Sun, 3 Aug 2025 13:19:53 -0700 Subject: [PATCH 7/9] feat: you can now delete hour entires --- .../volunteer-hours/view-hours-table.html | 7 ++ assets/js/volunteer-hours.js | 79 ++++++++++++++++++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/_includes/volunteer-hours/view-hours-table.html b/_includes/volunteer-hours/view-hours-table.html index 1b22f4d..db2538a 100644 --- a/_includes/volunteer-hours/view-hours-table.html +++ b/_includes/volunteer-hours/view-hours-table.html @@ -7,6 +7,7 @@ Date Hours Notes + Actions @@ -20,3 +21,9 @@
Error retrieving volunteer hours. Please try again.
+
+ Entry deleted successfully. +
+
+ Error deleting entry. Please try again. +
diff --git a/assets/js/volunteer-hours.js b/assets/js/volunteer-hours.js index 2f6b08f..a38101d 100644 --- a/assets/js/volunteer-hours.js +++ b/assets/js/volunteer-hours.js @@ -238,6 +238,12 @@ document.addEventListener('DOMContentLoaded', function() { */ function showMessage(elementId, message) { const element = document.getElementById(elementId); + + if (!element) { + console.error(`Element with ID "${elementId}" not found for showing message: ${message}`); + return; + } + element.textContent = message; element.classList.remove('d-none'); @@ -468,7 +474,15 @@ document.addEventListener('DOMContentLoaded', function() { function loadHoursData() { fetchWithErrorHandling('/all') .then(hoursData => { - const tableBody = document.getElementById('hoursTable').querySelector('tbody'); + const hoursTable = document.getElementById('hoursTable'); + + // Check if the table exists before proceeding + if (!hoursTable) { + console.error('Hours table element not found'); + return; + } + + const tableBody = hoursTable.querySelector('tbody'); tableBody.innerHTML = ''; if (hoursData.length === 0) { @@ -490,10 +504,18 @@ document.addEventListener('DOMContentLoaded', function() { ${formatDate(entry.date)} ${entry.hours} ${entry.notes || ''} + + + `; tableBody.appendChild(row); }); + + // Add event listeners to delete buttons + attachDeleteEventListeners(); }) .catch(error => { console.error('Error loading hours data:', error); @@ -513,6 +535,61 @@ document.addEventListener('DOMContentLoaded', function() { }); } + // Function to attach event listeners to delete buttons + function attachDeleteEventListeners() { + const deleteButtons = document.querySelectorAll('.delete-hours-btn'); + + if (deleteButtons.length === 0) { + console.log('No delete buttons found to attach listeners to'); + return; + } + + deleteButtons.forEach(button => { + button.addEventListener('click', function(event) { + event.preventDefault(); + const entryId = this.getAttribute('data-id'); + + // Confirm before deleting + if (confirm('Are you sure you want to delete this volunteer hours entry? This action cannot be undone.')) { + deleteHoursEntry(entryId); + } + }); + }); + } + + // Function to delete a volunteer hours entry + function deleteHoursEntry(entryId) { + // Hide any previous messages + const successElement = document.getElementById('deleteHoursSuccess'); + const errorElement = document.getElementById('deleteHoursError'); + + if (successElement) { + successElement.classList.add('d-none'); + } + + if (errorElement) { + errorElement.classList.add('d-none'); + } + + fetchWithErrorHandling(`/delete/${entryId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(data => { + // Show success message + showMessage('deleteHoursSuccess', data.message || 'Entry deleted successfully.'); + + // Reload hours data to refresh the table + loadHoursData(); + }) + .catch(error => { + console.error('Error deleting hours entry:', error); + showMessage('deleteHoursError', error.message || 'Error deleting entry. Please try again.'); + }); + } + // Function to load user stats function loadUserStats() { const userId = document.getElementById('statsUserSelect').value; From 1732d36cf40c3816269983db99911d0e2d6143c7 Mon Sep 17 00:00:00 2001 From: Damien Boisvert Date: Sun, 3 Aug 2025 13:40:32 -0700 Subject: [PATCH 8/9] feat: can edit logs --- _sass/_base.scss | 6 + _sass/_footer.scss | 3 + _sass/_volunteer-hours.scss | 98 ++++++++++++++++ assets/js/volunteer-hours.js | 212 ++++++++++++++++++++++++++++++----- 4 files changed, 293 insertions(+), 26 deletions(-) diff --git a/_sass/_base.scss b/_sass/_base.scss index 6fcd155..1dc78b0 100644 --- a/_sass/_base.scss +++ b/_sass/_base.scss @@ -8,6 +8,12 @@ @use "sass:color"; // Base Styles +html, body { + overflow-x: hidden; // Prevent horizontal scrolling + width: 100%; + position: relative; +} + body { font-family: $font-secondary; color: #333; diff --git a/_sass/_footer.scss b/_sass/_footer.scss index 78546e3..983cb71 100644 --- a/_sass/_footer.scss +++ b/_sass/_footer.scss @@ -9,6 +9,9 @@ .site-footer { background-color: #1a1a1a; color: rgb(255 255 255 / 80%); + width: 100%; + max-width: 100%; + box-sizing: border-box; } .footer-links { diff --git a/_sass/_volunteer-hours.scss b/_sass/_volunteer-hours.scss index 4fa18b2..34a768b 100644 --- a/_sass/_volunteer-hours.scss +++ b/_sass/_volunteer-hours.scss @@ -3,6 +3,10 @@ */ .volunteer-hours { + width: 100%; + max-width: 100%; + box-sizing: border-box; + overflow-x: hidden; /* Custom tab transitions */ .tab-content { .tab-pane { @@ -72,6 +76,64 @@ } } + // Action buttons in tables + .action-buttons { + .btn { + margin: 0 3px; + padding: 0.25rem 0.5rem; + + &:hover { + transform: translateY(-2px); + transition: transform 0.2s ease; + } + } + } + + // Modal fixes + .modal-backdrop { + opacity: 0.6; + z-index: 1040; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: #000; + } + + .modal { + z-index: 1050; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow-x: hidden; + overflow-y: auto; + outline: 0; + + &-dialog { + margin: 1.75rem auto; + max-height: calc(100vh - 3.5rem); + overflow-y: auto; + position: relative; + } + + &-content { + position: relative; + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + } + + &.fade .modal-dialog { + transition: transform 0.3s ease-out; + transform: translate(0, -50px); + } + + &.show .modal-dialog { + transform: none; + } + } + &-card { border-radius: 10px; overflow: hidden; @@ -111,6 +173,42 @@ } } + // Handle table overflow + .table-responsive { + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + + // Make sure the table doesn't force the page to be wider + table { + width: 100%; + max-width: 100%; + } + } + + // Inline editing styles + .hours-row { + transition: background-color 0.3s ease; + + &[data-mode="edit"] { + background-color: rgba(0, 123, 255, 0.05); + } + + td { + vertical-align: middle; + } + + .form-control-sm { + padding: 0.25rem 0.5rem; + height: calc(1.5em + 0.5rem + 2px); + font-size: 0.875rem; + } + + .edit-mode-buttons, .view-mode-buttons { + white-space: nowrap; + } + } + // Mobile optimizations @media (max-width: 768px) { .nav-tabs .nav-link { diff --git a/assets/js/volunteer-hours.js b/assets/js/volunteer-hours.js index a38101d..d5cb2a5 100644 --- a/assets/js/volunteer-hours.js +++ b/assets/js/volunteer-hours.js @@ -372,6 +372,8 @@ document.addEventListener('DOMContentLoaded', function() { // Add event listener for the load stats button document.getElementById('loadStatsBtn').addEventListener('click', loadUserStats); + // No need for modal initialization anymore since we're using inline editing + // Function to load users function loadUsers() { fetchWithErrorHandling('/users') @@ -498,24 +500,39 @@ document.addEventListener('DOMContentLoaded', function() { // Add rows to table hoursData.forEach(entry => { const row = document.createElement('tr'); - row.setAttribute('title', `ID ${entry.id}`); + row.setAttribute('data-id', entry.id); + row.setAttribute('data-mode', 'view'); + row.className = 'hours-row'; row.innerHTML = ` - ${entry.name} - ${formatDate(entry.date)} - ${entry.hours} - ${entry.notes || ''} - - + ${entry.name} + ${formatDate(entry.date)} + ${entry.hours} + ${entry.notes || ''} + +
+ + +
+ `; tableBody.appendChild(row); }); - // Add event listeners to delete buttons - attachDeleteEventListeners(); + // Add event listeners to action buttons + attachActionEventListeners(); }) .catch(error => { console.error('Error loading hours data:', error); @@ -535,26 +552,64 @@ document.addEventListener('DOMContentLoaded', function() { }); } - // Function to attach event listeners to delete buttons - function attachDeleteEventListeners() { + // Function to attach event listeners to action buttons + function attachActionEventListeners() { + // Attach listeners to delete buttons const deleteButtons = document.querySelectorAll('.delete-hours-btn'); - if (deleteButtons.length === 0) { - console.log('No delete buttons found to attach listeners to'); - return; + if (deleteButtons.length > 0) { + deleteButtons.forEach(button => { + button.addEventListener('click', function(event) { + event.preventDefault(); + const row = this.closest('tr'); + const entryId = row.getAttribute('data-id'); + + // Confirm before deleting + if (confirm('Are you sure you want to delete this volunteer hours entry? This action cannot be undone.')) { + deleteHoursEntry(entryId); + } + }); + }); } - deleteButtons.forEach(button => { - button.addEventListener('click', function(event) { - event.preventDefault(); - const entryId = this.getAttribute('data-id'); - - // Confirm before deleting - if (confirm('Are you sure you want to delete this volunteer hours entry? This action cannot be undone.')) { - deleteHoursEntry(entryId); - } + // Attach listeners to edit row buttons + const editButtons = document.querySelectorAll('.edit-row-btn'); + + if (editButtons.length > 0) { + editButtons.forEach(button => { + button.addEventListener('click', function(event) { + event.preventDefault(); + const row = this.closest('tr'); + switchToEditMode(row); + }); }); - }); + } + + // Attach listeners to save row buttons + const saveButtons = document.querySelectorAll('.save-row-btn'); + + if (saveButtons.length > 0) { + saveButtons.forEach(button => { + button.addEventListener('click', function(event) { + event.preventDefault(); + const row = this.closest('tr'); + saveRowChanges(row); + }); + }); + } + + // Attach listeners to cancel edit buttons + const cancelButtons = document.querySelectorAll('.cancel-edit-btn'); + + if (cancelButtons.length > 0) { + cancelButtons.forEach(button => { + button.addEventListener('click', function(event) { + event.preventDefault(); + const row = this.closest('tr'); + cancelRowEdit(row); + }); + }); + } } // Function to delete a volunteer hours entry @@ -590,6 +645,111 @@ document.addEventListener('DOMContentLoaded', function() { }); } + // Function to switch a row to edit mode + function switchToEditMode(row) { + // Set row to edit mode + row.setAttribute('data-mode', 'edit'); + + // Get the current values from the cells + const dateCell = row.querySelector('.date-cell'); + const hoursCell = row.querySelector('.hours-cell'); + const notesCell = row.querySelector('.notes-cell'); + + const date = dateCell.getAttribute('data-date'); + const hours = hoursCell.getAttribute('data-hours'); + const notes = notesCell.getAttribute('data-notes'); + + // Replace content with form fields + dateCell.innerHTML = ``; + hoursCell.innerHTML = ``; + notesCell.innerHTML = ``; + + // Show edit mode buttons, hide view mode buttons + const viewBtns = row.querySelector('.view-mode-buttons'); + const editBtns = row.querySelector('.edit-mode-buttons'); + + viewBtns.style.display = 'none'; + editBtns.style.display = 'block'; + } + + // Function to save changes made to a row + function saveRowChanges(row) { + // Get the entry ID from the row + const entryId = row.getAttribute('data-id'); + + // Get the updated values from the input fields + const dateInput = row.querySelector('.edit-date'); + const hoursInput = row.querySelector('.edit-hours'); + const notesInput = row.querySelector('.edit-notes'); + + const date = dateInput.value; + const hours = parseFloat(hoursInput.value); + const notes = notesInput.value; + + // Validate the input + if (!date || isNaN(hours) || hours <= 0) { + alert('Please enter valid date and hours.'); + return; + } + + const updatedData = { + date: date, + hours: hours, + notes: notes + }; + + // Make API request to update the entry + fetchWithErrorHandling(`/edit/${entryId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(updatedData) + }) + .then(data => { + // Show success message + showMessage('deleteHoursSuccess', 'Hours updated successfully!'); + + // Reload hours data to refresh the table + loadHoursData(); + }) + .catch(error => { + console.error('Error updating hours entry:', error); + showMessage('deleteHoursError', error.message || 'Error updating hours. Please try again.'); + + // Switch back to view mode with old values + cancelRowEdit(row); + }); + } + + // Function to cancel editing a row + function cancelRowEdit(row) { + // Set row back to view mode + row.setAttribute('data-mode', 'view'); + + // Get the cells + const dateCell = row.querySelector('.date-cell'); + const hoursCell = row.querySelector('.hours-cell'); + const notesCell = row.querySelector('.notes-cell'); + + // Get the original values + const date = dateCell.getAttribute('data-date'); + const hours = hoursCell.getAttribute('data-hours'); + const notes = notesCell.getAttribute('data-notes'); + + // Restore the original content + dateCell.innerHTML = formatDate(date); + hoursCell.innerHTML = hours; + notesCell.innerHTML = notes || ''; + + // Show view mode buttons, hide edit mode buttons + const viewBtns = row.querySelector('.view-mode-buttons'); + const editBtns = row.querySelector('.edit-mode-buttons'); + + viewBtns.style.display = 'block'; + editBtns.style.display = 'none'; + } + // Function to load user stats function loadUserStats() { const userId = document.getElementById('statsUserSelect').value; From ad21a3d6ce7bc5ccdb8d47788a49caa8cb69cac7 Mon Sep 17 00:00:00 2001 From: Damien Boisvert Date: Sun, 3 Aug 2025 13:44:42 -0700 Subject: [PATCH 9/9] feat: spinner on edit --- _sass/_volunteer-hours.scss | 11 +++++++++++ assets/js/volunteer-hours.js | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/_sass/_volunteer-hours.scss b/_sass/_volunteer-hours.scss index 34a768b..c6f1734 100644 --- a/_sass/_volunteer-hours.scss +++ b/_sass/_volunteer-hours.scss @@ -207,6 +207,17 @@ .edit-mode-buttons, .view-mode-buttons { white-space: nowrap; } + + // Button loading states + .save-row-btn { + min-width: 34px; // Prevent button size changing when spinner shows + + .spinner-border-sm { + width: 1rem; + height: 1rem; + border-width: 0.15em; + } + } } // Mobile optimizations diff --git a/assets/js/volunteer-hours.js b/assets/js/volunteer-hours.js index d5cb2a5..1c05e55 100644 --- a/assets/js/volunteer-hours.js +++ b/assets/js/volunteer-hours.js @@ -76,7 +76,7 @@ function updateAllElements(baseId, value) { document.addEventListener('DOMContentLoaded', function() { const backendURL = window.backendBaseURL + '/volunteer_hours'; - wrappedFetch(window.backendBaseURL + "/healthcheck?src=VolunteerHoursTool&c=1") + wrappedwindow.backendBaseURL + "/healthcheck?src=VolunteerHoursTool&c=1") .then(response => { return response.json(); }) @@ -519,7 +519,10 @@ document.addEventListener('DOMContentLoaded', function() {