Added calendar view to the shifts
This commit is contained in:
parent
e0562904a8
commit
ff3e1e868b
@ -15,8 +15,8 @@ A containerized web application that visualizes geographic data from NocoDB on a
|
||||
- 🎯 Configurable map start location
|
||||
- 📋 Walk Sheet generator for door-to-door canvassing
|
||||
- 🔗 QR code integration for digital resources
|
||||
- <20> Volunteer shift management system
|
||||
- ✋ User shift signup and cancellation
|
||||
- <20> Volunteer shift management system with calendar and grid views
|
||||
- ✋ User shift signup and cancellation with color-coded calendar
|
||||
- 👥 Admin shift creation and management
|
||||
- <20>🐳 Docker containerization for easy deployment
|
||||
- 🆓 100% open source (no proprietary dependencies)
|
||||
@ -222,12 +222,19 @@ The application includes a comprehensive volunteer shift management system acces
|
||||
|
||||
### User Features
|
||||
|
||||
- **Dual View Options**: Toggle between grid view and calendar view for shift display
|
||||
- **Calendar View**: Interactive monthly calendar showing shifts with color-coded indicators:
|
||||
- Green: Shifts you've signed up for
|
||||
- Blue: Available shifts you can join
|
||||
- Gray: Full shifts (no spots available)
|
||||
- **View Available Shifts**: See all upcoming shifts with date, time, and capacity information
|
||||
- **Sign Up for Shifts**: One-click signup for available shifts
|
||||
- **My Shifts Dashboard**: View all your current shift signups
|
||||
- **Sign Up for Shifts**: One-click signup for available shifts (works in both views)
|
||||
- **My Shifts Dashboard**: View all your current shift signups at the top of the page
|
||||
- **Cancel Signups**: Cancel your shift signups when needed
|
||||
- **Date Filtering**: Filter shifts by specific dates
|
||||
- **Date Filtering**: Filter shifts by specific dates (applies to both views)
|
||||
- **Real-time Updates**: Shift availability updates dynamically
|
||||
- **Interactive Calendar**: Click on calendar shifts for detailed popup with signup options
|
||||
- **Calendar Navigation**: Navigate between months to view future shifts
|
||||
|
||||
### Admin Features
|
||||
|
||||
|
||||
@ -182,6 +182,196 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* View toggle and calendar styles */
|
||||
.shifts-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.view-toggle .btn {
|
||||
padding: 8px 15px;
|
||||
}
|
||||
|
||||
.view-toggle .btn.active {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Calendar View Styles */
|
||||
.calendar-view {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.calendar-header h3 {
|
||||
margin: 0;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 1px;
|
||||
background-color: #e0e0e0;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calendar-day-header {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: 12px 8px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
background-color: white;
|
||||
min-height: 120px;
|
||||
padding: 8px;
|
||||
position: relative;
|
||||
border: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.calendar-day.other-month {
|
||||
background-color: #f9f9f9;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.calendar-day.today {
|
||||
background-color: #fff3cd;
|
||||
}
|
||||
|
||||
.calendar-day-number {
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.calendar-day.other-month .calendar-day-number {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.calendar-shifts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.calendar-shift {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.75em;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.calendar-shift:hover {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.calendar-shift.my-shift {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
|
||||
.calendar-shift.full-shift {
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.calendar-shift.available-shift {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Calendar legend */
|
||||
.calendar-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.legend-color.my-shift {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
|
||||
.legend-color.available-shift {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.legend-color.full-shift {
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
/* Calendar shift popup/tooltip */
|
||||
.shift-popup {
|
||||
position: absolute;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 15px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 1000;
|
||||
min-width: 250px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.shift-popup h4 {
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.shift-popup p {
|
||||
margin: 5px 0;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.shift-popup .shift-actions {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.shifts-container {
|
||||
@ -237,13 +427,66 @@
|
||||
.filter-group {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.shifts-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* Prevent dropdown from being cut off */
|
||||
.shifts-grid {
|
||||
overflow: visible;
|
||||
.view-toggle {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.shift-card {
|
||||
overflow: visible;
|
||||
.calendar-day {
|
||||
min-height: 80px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.calendar-day-number {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.calendar-shift {
|
||||
font-size: 0.7em;
|
||||
padding: 1px 2px;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.calendar-header h3 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.calendar-legend {
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.shift-popup {
|
||||
max-width: 280px;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Extra small screens */
|
||||
@media (max-width: 480px) {
|
||||
.calendar-day {
|
||||
min-height: 60px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.calendar-shift {
|
||||
font-size: 0.65em;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.calendar-day-number {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
let currentUser = null;
|
||||
let allShifts = [];
|
||||
let mySignups = [];
|
||||
let currentView = 'grid'; // 'grid' or 'calendar'
|
||||
let currentCalendarDate = new Date(); // For calendar navigation
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
@ -8,6 +10,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
await loadShifts();
|
||||
await loadMySignups();
|
||||
setupEventListeners();
|
||||
initializeViewToggle();
|
||||
|
||||
// Add clear filters button handler
|
||||
const clearBtn = document.getElementById('clear-filters-btn');
|
||||
@ -118,6 +121,11 @@ function displayShifts(shifts) {
|
||||
|
||||
// Add event listeners after rendering
|
||||
setupShiftCardListeners();
|
||||
|
||||
// Update calendar view if it's currently active
|
||||
if (currentView === 'calendar') {
|
||||
renderCalendar();
|
||||
}
|
||||
}
|
||||
|
||||
function displayMySignups() {
|
||||
@ -157,6 +165,11 @@ function displayMySignups() {
|
||||
|
||||
// Add event listeners after rendering
|
||||
setupMySignupsListeners();
|
||||
|
||||
// Update calendar view if it's currently active
|
||||
if (currentView === 'calendar') {
|
||||
renderCalendar();
|
||||
}
|
||||
}
|
||||
|
||||
// New function to setup listeners for shift cards
|
||||
@ -447,13 +460,13 @@ function generateCalendarDropdown(shift) {
|
||||
</button>
|
||||
<div class="calendar-options" style="display: none;">
|
||||
<a href="${urls.google}" target="_blank" class="calendar-option" data-calendar-type="google">
|
||||
<img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1zbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyLjUgMkgxMy41QzE0LjMyODQgMiAxNSAyLjY3MTU3IDE1IDMuNVYxMi41QzE1IDEzLjMyODQgMTQuMzI4NCAxNCAxMy41IDE0SDIuNUMxLjY3MTU3IDE0IDEgMTMuMzI4NCAxIDEyLjVWMy41QzEgMi42NzE1NyAxLjY3MTU3IDIgMi41IDJIMy41IiBzdHJva2U9IiM0Mjg1RjQiIHN0cm9rZS13aWR0aD0iMS41Ii8+CjxwYXRoIGQ9Ik00IDFWM00xMiAxVjMiIHN0cm9rZT0iIzQyODVGNCIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8cGF0aCBkPSJNMSA1SDE1IiBzdHJva2U9IiM0Mjg1RjQiIHN0cm9rZS13aWR0aD0iMS41Ii8+Cjwvc3ZnPg==" alt="Google"> Google Calendar
|
||||
Google Calendar
|
||||
</a>
|
||||
<a href="${urls.outlook}" target="_blank" class="calendar-option" data-calendar-type="outlook">
|
||||
<img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1zbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDJIMlYxNEgxNFYyWiIgZmlsbD0iIzAwNzhENCIvPgo8cGF0aCBkPSJNOCA4VjE0SDE0VjhIOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik04IDJWOEgxNFYySDhaIiBmaWxsPSIjMDA3OEQ0Ii8+Cjwvc3ZnPg==" alt="Outlook"> Outlook
|
||||
Outlook
|
||||
</a>
|
||||
<a href="${urls.apple}" download="${urls.icsFilename}" class="calendar-option" data-calendar-type="apple">
|
||||
<img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1zbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzIDJIMy41QzIuNjcxNTcgMiAyIDIuNjcxNTcgMiAzLjVWMTIuNUMyIDEzLjMyODQgMi42NzE1NyAxNCAzLjUgMTRIMTIuNUMxMy4zMjg0IDE0IDE0IDEzLjMyODQgMTQgMTIuNVYzQzE0IDIuNDQ3NzIgMTMuNTUyMyAyIDEzIDJaIiBmaWxsPSIjRkY1NzMzIi8+CjxwYXRoIGQ9Ik00IDFWM00xMiAxVjMiIHN0cm9rZT0iI0ZGNTczMyIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPHBhdGggZD0iTTIgNUgxNCIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiLz4KPHRleHQgeD0iOCIgeT0iMTEiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSI2IiBmaWxsPSJ3aGl0ZSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+MzE8L3RleHQ+Cjwvc3ZnPg==" alt="Apple"> Apple Calendar
|
||||
Apple Calendar
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -547,6 +560,268 @@ function setupMySignupsListeners() {
|
||||
});
|
||||
}
|
||||
|
||||
// Calendar View Functions
|
||||
function initializeViewToggle() {
|
||||
const gridBtn = document.getElementById('grid-view-btn');
|
||||
const calendarBtn = document.getElementById('calendar-view-btn');
|
||||
const prevBtn = document.getElementById('prev-month');
|
||||
const nextBtn = document.getElementById('next-month');
|
||||
|
||||
if (gridBtn && calendarBtn) {
|
||||
gridBtn.addEventListener('click', () => switchView('grid'));
|
||||
calendarBtn.addEventListener('click', () => switchView('calendar'));
|
||||
|
||||
// Set initial active state
|
||||
gridBtn.classList.add('active');
|
||||
}
|
||||
|
||||
if (prevBtn && nextBtn) {
|
||||
prevBtn.addEventListener('click', () => navigateCalendar(-1));
|
||||
nextBtn.addEventListener('click', () => navigateCalendar(1));
|
||||
}
|
||||
}
|
||||
|
||||
function switchView(view) {
|
||||
const gridView = document.getElementById('shifts-grid');
|
||||
const calendarView = document.getElementById('calendar-view');
|
||||
const gridBtn = document.getElementById('grid-view-btn');
|
||||
const calendarBtn = document.getElementById('calendar-view-btn');
|
||||
|
||||
currentView = view;
|
||||
|
||||
if (view === 'calendar') {
|
||||
gridView.style.display = 'none';
|
||||
calendarView.style.display = 'block';
|
||||
gridBtn.classList.remove('active');
|
||||
calendarBtn.classList.add('active');
|
||||
renderCalendar();
|
||||
} else {
|
||||
gridView.style.display = 'grid';
|
||||
calendarView.style.display = 'none';
|
||||
gridBtn.classList.add('active');
|
||||
calendarBtn.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
function navigateCalendar(direction) {
|
||||
currentCalendarDate.setMonth(currentCalendarDate.getMonth() + direction);
|
||||
renderCalendar();
|
||||
}
|
||||
|
||||
function renderCalendar() {
|
||||
const year = currentCalendarDate.getFullYear();
|
||||
const month = currentCalendarDate.getMonth();
|
||||
|
||||
// Update header
|
||||
const monthYearHeader = document.getElementById('calendar-month-year');
|
||||
if (monthYearHeader) {
|
||||
const monthNames = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'
|
||||
];
|
||||
monthYearHeader.textContent = `${monthNames[month]} ${year}`;
|
||||
}
|
||||
|
||||
// Get calendar grid
|
||||
const calendarGrid = document.getElementById('calendar-grid');
|
||||
if (!calendarGrid) return;
|
||||
|
||||
calendarGrid.innerHTML = '';
|
||||
|
||||
// Add day headers
|
||||
const dayHeaders = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
dayHeaders.forEach(day => {
|
||||
const dayHeader = document.createElement('div');
|
||||
dayHeader.className = 'calendar-day-header';
|
||||
dayHeader.textContent = day;
|
||||
calendarGrid.appendChild(dayHeader);
|
||||
});
|
||||
|
||||
// Get first day of month and number of days
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
const daysInMonth = lastDay.getDate();
|
||||
const startingDayOfWeek = firstDay.getDay();
|
||||
|
||||
// Get previous month's last days
|
||||
const prevMonth = new Date(year, month, 0);
|
||||
const daysInPrevMonth = prevMonth.getDate();
|
||||
|
||||
// Add previous month's trailing days
|
||||
for (let i = startingDayOfWeek - 1; i >= 0; i--) {
|
||||
const dayNumber = daysInPrevMonth - i;
|
||||
const dayElement = createCalendarDay(dayNumber, true, new Date(year, month - 1, dayNumber));
|
||||
calendarGrid.appendChild(dayElement);
|
||||
}
|
||||
|
||||
// Add current month's days
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const currentDate = new Date(year, month, day);
|
||||
const dayElement = createCalendarDay(day, false, currentDate);
|
||||
calendarGrid.appendChild(dayElement);
|
||||
}
|
||||
|
||||
// Add next month's leading days to fill the grid
|
||||
const totalCells = calendarGrid.children.length;
|
||||
const remainingCells = 42 - totalCells; // 6 rows × 7 days
|
||||
for (let day = 1; day <= remainingCells; day++) {
|
||||
const dayElement = createCalendarDay(day, true, new Date(year, month + 1, day));
|
||||
calendarGrid.appendChild(dayElement);
|
||||
}
|
||||
}
|
||||
|
||||
function createCalendarDay(dayNumber, isOtherMonth, date) {
|
||||
const dayElement = document.createElement('div');
|
||||
dayElement.className = 'calendar-day';
|
||||
|
||||
if (isOtherMonth) {
|
||||
dayElement.classList.add('other-month');
|
||||
}
|
||||
|
||||
// Check if it's today
|
||||
const today = new Date();
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
dayElement.classList.add('today');
|
||||
}
|
||||
|
||||
// Add day number
|
||||
const dayNumberElement = document.createElement('div');
|
||||
dayNumberElement.className = 'calendar-day-number';
|
||||
dayNumberElement.textContent = dayNumber;
|
||||
dayElement.appendChild(dayNumberElement);
|
||||
|
||||
// Add shifts for this day
|
||||
const shiftsContainer = document.createElement('div');
|
||||
shiftsContainer.className = 'calendar-shifts';
|
||||
|
||||
const dateString = date.toISOString().split('T')[0];
|
||||
const dayShifts = allShifts.filter(shift => {
|
||||
const shiftDate = new Date(shift.Date);
|
||||
return shiftDate.toISOString().split('T')[0] === dateString;
|
||||
});
|
||||
|
||||
dayShifts.forEach(shift => {
|
||||
const shiftElement = createCalendarShift(shift);
|
||||
shiftsContainer.appendChild(shiftElement);
|
||||
});
|
||||
|
||||
dayElement.appendChild(shiftsContainer);
|
||||
return dayElement;
|
||||
}
|
||||
|
||||
function createCalendarShift(shift) {
|
||||
const shiftElement = document.createElement('div');
|
||||
shiftElement.className = 'calendar-shift';
|
||||
|
||||
// Determine shift type and color
|
||||
const isSignedUp = mySignups.some(signup => signup.shift_id === shift.ID);
|
||||
const isFull = shift['Current Volunteers'] >= shift['Max Volunteers'];
|
||||
|
||||
if (isSignedUp) {
|
||||
shiftElement.classList.add('my-shift');
|
||||
} else if (isFull) {
|
||||
shiftElement.classList.add('full-shift');
|
||||
} else {
|
||||
shiftElement.classList.add('available-shift');
|
||||
}
|
||||
|
||||
// Set shift text (time and title)
|
||||
const timeText = `${shift['Start Time']} ${shift.Title}`;
|
||||
shiftElement.textContent = timeText;
|
||||
shiftElement.title = `${shift.Title}\n${shift['Start Time']} - ${shift['End Time']}\n${shift.Location || 'TBD'}`;
|
||||
|
||||
// Add click handler
|
||||
shiftElement.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
showShiftPopup(shift, e.target);
|
||||
});
|
||||
|
||||
return shiftElement;
|
||||
}
|
||||
|
||||
function showShiftPopup(shift, targetElement) {
|
||||
// Remove any existing popup
|
||||
const existingPopup = document.querySelector('.shift-popup');
|
||||
if (existingPopup) {
|
||||
existingPopup.remove();
|
||||
}
|
||||
|
||||
const popup = document.createElement('div');
|
||||
popup.className = 'shift-popup';
|
||||
|
||||
const isSignedUp = mySignups.some(signup => signup.shift_id === shift.ID);
|
||||
const isFull = shift['Current Volunteers'] >= shift['Max Volunteers'];
|
||||
const shiftDate = new Date(shift.Date);
|
||||
|
||||
popup.innerHTML = `
|
||||
<h4>${escapeHtml(shift.Title)}</h4>
|
||||
<p>📅 ${shiftDate.toLocaleDateString()}</p>
|
||||
<p>⏰ ${shift['Start Time']} - ${shift['End Time']}</p>
|
||||
<p>📍 ${escapeHtml(shift.Location || 'TBD')}</p>
|
||||
<p>👥 ${shift['Current Volunteers']}/${shift['Max Volunteers']} volunteers</p>
|
||||
${shift.Description ? `<p>${escapeHtml(shift.Description)}</p>` : ''}
|
||||
<div class="shift-actions">
|
||||
${isSignedUp
|
||||
? `<button class="btn btn-danger btn-sm cancel-signup-btn" data-shift-id="${shift.ID}">Cancel Signup</button>`
|
||||
: isFull
|
||||
? '<button class="btn btn-secondary btn-sm" disabled>Shift Full</button>'
|
||||
: `<button class="btn btn-primary btn-sm signup-btn" data-shift-id="${shift.ID}">Sign Up</button>`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Position popup
|
||||
document.body.appendChild(popup);
|
||||
|
||||
const rect = targetElement.getBoundingClientRect();
|
||||
const popupRect = popup.getBoundingClientRect();
|
||||
|
||||
let left = rect.left + (rect.width / 2) - (popupRect.width / 2);
|
||||
let top = rect.bottom + 10;
|
||||
|
||||
// Adjust if popup goes off screen
|
||||
if (left < 10) left = 10;
|
||||
if (left + popupRect.width > window.innerWidth - 10) {
|
||||
left = window.innerWidth - popupRect.width - 10;
|
||||
}
|
||||
if (top + popupRect.height > window.innerHeight - 10) {
|
||||
top = rect.top - popupRect.height - 10;
|
||||
}
|
||||
|
||||
popup.style.left = `${left}px`;
|
||||
popup.style.top = `${top}px`;
|
||||
|
||||
// Add event listeners for buttons in popup
|
||||
const signupBtn = popup.querySelector('.signup-btn');
|
||||
const cancelBtn = popup.querySelector('.cancel-signup-btn');
|
||||
|
||||
if (signupBtn) {
|
||||
signupBtn.addEventListener('click', async () => {
|
||||
await signupForShift(shift.ID);
|
||||
popup.remove();
|
||||
});
|
||||
}
|
||||
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener('click', async () => {
|
||||
await cancelSignup(shift.ID);
|
||||
popup.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Close popup when clicking outside
|
||||
const closePopup = (e) => {
|
||||
if (!popup.contains(e.target)) {
|
||||
popup.remove();
|
||||
document.removeEventListener('click', closePopup);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', closePopup);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Keep the document click handler to close dropdowns when clicking outside
|
||||
document.addEventListener('click', function() {
|
||||
document.querySelectorAll('.calendar-options').forEach(opt => {
|
||||
|
||||
@ -31,7 +31,13 @@
|
||||
</div>
|
||||
|
||||
<div class="shifts-filters">
|
||||
<div class="shifts-header">
|
||||
<h2>Available Shifts</h2>
|
||||
<div class="view-toggle">
|
||||
<button class="btn btn-secondary btn-sm" id="grid-view-btn" data-view="grid">📋 List View</button>
|
||||
<button class="btn btn-secondary btn-sm" id="calendar-view-btn" data-view="calendar">📅 Calendar View</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="date-filter">Filter by Date:</label>
|
||||
<input type="date" id="date-filter" />
|
||||
@ -42,6 +48,31 @@
|
||||
<div class="shifts-grid" id="shifts-grid">
|
||||
<!-- Shifts will be loaded here -->
|
||||
</div>
|
||||
|
||||
<div class="calendar-view" id="calendar-view" style="display: none;">
|
||||
<div class="calendar-header">
|
||||
<button class="btn btn-secondary btn-sm" id="prev-month">‹ Previous</button>
|
||||
<h3 id="calendar-month-year"></h3>
|
||||
<button class="btn btn-secondary btn-sm" id="next-month">Next ›</button>
|
||||
</div>
|
||||
<div class="calendar-grid" id="calendar-grid">
|
||||
<!-- Calendar will be loaded here -->
|
||||
</div>
|
||||
<div class="calendar-legend">
|
||||
<div class="legend-item">
|
||||
<span class="legend-color my-shift"></span>
|
||||
<span>My Shifts</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-color available-shift"></span>
|
||||
<span>Available Shifts</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-color full-shift"></span>
|
||||
<span>Full Shifts</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="status-container" class="status-container"></div>
|
||||
|
||||
@ -100,7 +100,7 @@ CSS styles specific to the admin panel UI.
|
||||
|
||||
# app/public/css/shifts.css
|
||||
|
||||
CSS styles for the volunteer shifts page.
|
||||
CSS styles for the volunteer shifts page, including grid view, calendar view, and view toggle functionality.
|
||||
|
||||
# app/public/css/style.css
|
||||
|
||||
@ -120,7 +120,7 @@ Login page HTML for user authentication.
|
||||
|
||||
# app/public/shifts.html
|
||||
|
||||
Volunteer shifts management and signup page HTML.
|
||||
Volunteer shifts management and signup page HTML with both grid and calendar view options.
|
||||
|
||||
# app/public/js/admin.js
|
||||
|
||||
@ -152,7 +152,7 @@ Backup or legacy version of the main map JavaScript logic.
|
||||
|
||||
# app/public/js/shifts.js
|
||||
|
||||
JavaScript for volunteer shift signup, management, and UI logic. Updated to use shift titles directly from signup records.
|
||||
JavaScript for volunteer shift signup, management, and UI logic with both grid and calendar view functionality. Features include view toggling, calendar navigation, shift color-coding, and interactive shift popups. Updated to use shift titles directly from signup records.
|
||||
|
||||
# app/public/js/ui-controls.js
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user