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. +
+
+
+
+
+ + + +