Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/add-update-meeting-tool.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
---

feat: Add update_meeting tool to Addie

Added a new tool that allows updating existing meetings through conversation:
- Change title, description, or agenda
- Reschedule to a new time (properly handles timezone conversion)
- Update duration

Updates are synchronized to Zoom and Google Calendar when those integrations
are configured. This enables fixing meeting times or details without having
to cancel and recreate.
2 changes: 2 additions & 0 deletions .changeset/dirty-hoops-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
11 changes: 11 additions & 0 deletions .changeset/fix-meetings-section-display.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
---

fix: Display meetings section on committee pages and add admin edit

Fixed a bug where the meetings section wasn't showing on committee detail pages.
The API returns `{ meetings: [...] }` but the code was calling `.filter()` directly
on the response object instead of `response.meetings`.

Also added edit functionality to the admin meetings page - admins can now click
"Edit" to reschedule or update meeting details (title, description, time).
84 changes: 72 additions & 12 deletions server/public/admin-meetings.html
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ <h2 id="detailsTitle">Meeting Details</h2>
<div id="detailsContent"></div>
<div class="modal-buttons">
<button type="button" class="btn btn-secondary" onclick="closeDetailsModal()">Close</button>
<button type="button" class="btn btn-primary" id="editMeetingBtn" onclick="editMeeting()">Edit</button>
<button type="button" class="btn btn-danger" id="cancelMeetingBtn" onclick="cancelMeeting()">Cancel Meeting</button>
</div>
</div>
Expand Down Expand Up @@ -633,56 +634,112 @@ <h3>${escapeHtml(s.title)}</h3>
document.getElementById('meetingModal').style.display = 'flex';
}

// Edit existing meeting
function editMeeting() {
if (!viewingMeeting) return;

const m = viewingMeeting;
const date = new Date(m.start_time);
const tz = m.timezone || 'America/New_York';

// Format date/time in the meeting's timezone for editing
const dateFormatter = new Intl.DateTimeFormat('en-CA', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit' });
const timeFormatter = new Intl.DateTimeFormat('en-US', { timeZone: tz, hour: '2-digit', minute: '2-digit', hour12: false });
const formattedDate = dateFormatter.format(date); // YYYY-MM-DD format
const formattedTime = timeFormatter.format(date).replace(/^24:/, '00:'); // HH:MM format

document.getElementById('modalTitle').textContent = 'Edit Meeting';
document.getElementById('meetingId').value = m.id;
document.getElementById('workingGroupId').value = m.working_group_id;
document.getElementById('title').value = m.title;
document.getElementById('meetingDate').value = formattedDate;
document.getElementById('meetingTime').value = formattedTime;
document.getElementById('timezone').value = tz;
document.getElementById('description').value = m.description || m.agenda || '';

// Calculate duration from start_time and end_time
if (m.end_time) {
const endDate = new Date(m.end_time);
const durationMs = endDate.getTime() - date.getTime();
document.getElementById('duration').value = Math.round(durationMs / 60000);
} else {
document.getElementById('duration').value = 60;
}

// Hide invite mode for edits (can't change invites after creation)
document.getElementById('inviteMode').parentElement.style.display = 'none';

document.getElementById('saveBtn').textContent = 'Save Changes';
closeDetailsModal();
document.getElementById('meetingModal').style.display = 'flex';
}

function closeModal() {
document.getElementById('meetingModal').style.display = 'none';
// Reset invite mode visibility
document.getElementById('inviteMode').parentElement.style.display = 'block';
document.getElementById('saveBtn').textContent = 'Schedule';
}

// Save meeting
// Save meeting (create or update)
async function saveMeeting(event) {
event.preventDefault();

const meetingId = document.getElementById('meetingId').value;
const isUpdate = !!meetingId;

const date = document.getElementById('meetingDate').value;
const time = document.getElementById('meetingTime').value;
const timezone = document.getElementById('timezone').value;
const durationMinutes = parseInt(document.getElementById('duration').value) || 60;

// Construct ISO datetime
const startTime = new Date(`${date}T${time}`);
const endTime = new Date(startTime.getTime() + durationMinutes * 60000);

const data = {
working_group_id: document.getElementById('workingGroupId').value,
title: document.getElementById('title').value.trim(),
description: document.getElementById('description').value.trim() || null,
start_time: startTime.toISOString(),
duration_minutes: parseInt(document.getElementById('duration').value) || 60,
end_time: endTime.toISOString(),
timezone: timezone,
invite_mode: document.getElementById('inviteMode').value,
};

// Only include these fields for new meetings
if (!isUpdate) {
data.working_group_id = document.getElementById('workingGroupId').value;
data.duration_minutes = durationMinutes;
data.invite_mode = document.getElementById('inviteMode').value;
}

const saveBtn = document.getElementById('saveBtn');
saveBtn.disabled = true;
saveBtn.textContent = 'Scheduling...';
saveBtn.textContent = isUpdate ? 'Saving...' : 'Scheduling...';

try {
const response = await fetch('/api/admin/meetings', {
method: 'POST',
const url = isUpdate ? `/api/admin/meetings/${meetingId}` : '/api/admin/meetings';
const method = isUpdate ? 'PUT' : 'POST';

const response = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});

if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to schedule meeting');
throw new Error(error.message || `Failed to ${isUpdate ? 'update' : 'schedule'} meeting`);
}

closeModal();
loadMeetings();
} catch (error) {
console.error('Error scheduling meeting:', error);
alert('Failed to schedule meeting: ' + error.message);
console.error(`Error ${isUpdate ? 'updating' : 'scheduling'} meeting:`, error);
alert(`Failed to ${isUpdate ? 'update' : 'schedule'} meeting: ` + error.message);
}

saveBtn.disabled = false;
saveBtn.textContent = 'Schedule';
saveBtn.textContent = isUpdate ? 'Save Changes' : 'Schedule';
}

// View meeting details
Expand Down Expand Up @@ -731,7 +788,10 @@ <h3>${escapeHtml(s.title)}</h3>
</div>
`;

document.getElementById('cancelMeetingBtn').style.display = m.status === 'scheduled' ? 'block' : 'none';
// Only show Edit and Cancel buttons for scheduled meetings
const isScheduled = m.status === 'scheduled';
document.getElementById('editMeetingBtn').style.display = isScheduled ? 'inline-block' : 'none';
document.getElementById('cancelMeetingBtn').style.display = isScheduled ? 'inline-block' : 'none';
document.getElementById('detailsModal').style.display = 'flex';
} catch (error) {
console.error('Error loading meeting:', error);
Expand Down
6 changes: 3 additions & 3 deletions server/public/working-groups/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -1674,10 +1674,10 @@ <h3 class="events-subsection-title">Recent Events</h3>
const response = await fetch(`/api/meetings?working_group_id=${currentGroup.id}`);
if (!response.ok) return;

const meetings = await response.json();
// Filter to only upcoming meetings
const data = await response.json();
// API returns { meetings: [...] } - filter to scheduled upcoming meetings
const now = new Date();
const upcoming = meetings.filter(m => new Date(m.start_time) > now && m.status === 'scheduled');
const upcoming = (data.meetings || []).filter(m => new Date(m.start_time) > now && m.status === 'scheduled');

const meetingsSection = document.getElementById('meetingsSection');
const meetingsList = document.getElementById('meetingsList');
Expand Down
Loading