Add room size setting, constraint management with grouped color-coded tags
This commit is contained in:
@@ -56,6 +56,7 @@ export async function api(method, path, body) {
|
||||
if (!res.ok) {
|
||||
throw new Error(await res.text());
|
||||
}
|
||||
if (res.status === 204) return null;
|
||||
return res.json();
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,12 @@
|
||||
padding: 0 0.2rem;
|
||||
}
|
||||
.input-action:hover { opacity: 1; }
|
||||
#trip-settings { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; }
|
||||
#room-size { width: 3.5rem; font-size: 0.85rem; padding: 0.2rem; border: 1px solid var(--wa-color-neutral-300, #ccc); border-radius: 0.25rem; }
|
||||
.constraint-group { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem; margin-bottom: 0.2rem; }
|
||||
.constraint-level { font-size: 0.7rem; font-weight: bold; opacity: 0.6; }
|
||||
.constraint-add { display: flex; gap: 0.5rem; align-items: center; margin-top: 0.3rem; }
|
||||
.constraint-add select { font-size: 0.75rem; padding: 0.15rem; border: 1px solid var(--wa-color-neutral-300, #ccc); border-radius: 0.25rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -66,6 +72,10 @@
|
||||
<wa-button variant="neutral" size="small" id="logout-btn">Switch User</wa-button>
|
||||
</div>
|
||||
<h2 id="trip-name"></h2>
|
||||
<div id="trip-settings">
|
||||
<label for="room-size">Students per room:</label>
|
||||
<input id="room-size" type="number" min="1">
|
||||
</div>
|
||||
<div id="students"></div>
|
||||
<wa-details summary="Add Student">
|
||||
<div class="add-form">
|
||||
|
||||
111
static/trip.js
111
static/trip.js
@@ -14,15 +14,31 @@ try {
|
||||
}
|
||||
|
||||
document.getElementById('trip-name').textContent = trip.name;
|
||||
document.getElementById('room-size').value = trip.room_size;
|
||||
document.getElementById('main').style.display = 'block';
|
||||
document.getElementById('logout-btn').addEventListener('click', logout);
|
||||
document.getElementById('room-size').addEventListener('change', async () => {
|
||||
const size = parseInt(document.getElementById('room-size').value);
|
||||
if (size >= 1) await api('PATCH', '/api/trips/' + tripID, { room_size: size });
|
||||
});
|
||||
|
||||
async function loadStudents() {
|
||||
const students = await api('GET', '/api/trips/' + tripID + '/students');
|
||||
const [students, constraints] = await Promise.all([
|
||||
api('GET', '/api/trips/' + tripID + '/students'),
|
||||
api('GET', '/api/trips/' + tripID + '/constraints')
|
||||
]);
|
||||
const container = document.getElementById('students');
|
||||
const openStates = {};
|
||||
for (const card of container.children) {
|
||||
const sid = card.dataset.studentId;
|
||||
if (!sid) continue;
|
||||
openStates[sid] = {};
|
||||
for (const det of card.querySelectorAll('wa-details')) openStates[sid][det.summary] = det.open;
|
||||
}
|
||||
container.innerHTML = '';
|
||||
for (const student of students) {
|
||||
const card = document.createElement('wa-card');
|
||||
card.dataset.studentId = student.id;
|
||||
|
||||
const nameRow = document.createElement('div');
|
||||
nameRow.style.display = 'flex';
|
||||
@@ -84,6 +100,99 @@ async function loadStudents() {
|
||||
|
||||
card.appendChild(details);
|
||||
|
||||
const cDetails = document.createElement('wa-details');
|
||||
cDetails.summary = 'Constraints';
|
||||
|
||||
const kindVariant = { must: 'success', prefer: 'brand', prefer_not: 'warning', must_not: 'danger' };
|
||||
const kindLabels = { must: 'Must', prefer: 'Prefer', prefer_not: 'Prefer Not', must_not: 'Must Not' };
|
||||
const kindOrder = { must: 0, prefer: 1, prefer_not: 2, must_not: 3 };
|
||||
const myConstraints = constraints.filter(c => c.student_a_id === student.id || c.student_b_id === student.id);
|
||||
|
||||
for (const level of ['admin', 'parent', 'student']) {
|
||||
const lc = myConstraints.filter(c => c.level === level);
|
||||
if (lc.length === 0) continue;
|
||||
lc.sort((a, b) => {
|
||||
const kd = kindOrder[a.kind] - kindOrder[b.kind];
|
||||
if (kd !== 0) return kd;
|
||||
const na = a.student_a_id === student.id ? a.student_b_name : a.student_a_name;
|
||||
const nb = b.student_a_id === student.id ? b.student_b_name : b.student_a_name;
|
||||
return na.localeCompare(nb);
|
||||
});
|
||||
const group = document.createElement('div');
|
||||
group.className = 'constraint-group';
|
||||
const levelLabel = document.createElement('span');
|
||||
levelLabel.className = 'constraint-level';
|
||||
levelLabel.textContent = level.charAt(0).toUpperCase() + level.slice(1);
|
||||
group.appendChild(levelLabel);
|
||||
for (const c of lc) {
|
||||
const otherName = c.student_a_id === student.id ? c.student_b_name : c.student_a_name;
|
||||
const tag = document.createElement('wa-tag');
|
||||
tag.size = 'small';
|
||||
tag.variant = kindVariant[c.kind];
|
||||
tag.setAttribute('with-remove', '');
|
||||
tag.textContent = kindLabels[c.kind] + ': ' + otherName;
|
||||
tag.addEventListener('wa-remove', async () => {
|
||||
await api('DELETE', '/api/trips/' + tripID + '/constraints/' + c.id);
|
||||
loadStudents();
|
||||
});
|
||||
group.appendChild(tag);
|
||||
}
|
||||
cDetails.appendChild(group);
|
||||
}
|
||||
|
||||
const addRow = document.createElement('div');
|
||||
addRow.className = 'constraint-add';
|
||||
const studentSelect = document.createElement('select');
|
||||
const defaultOpt = document.createElement('option');
|
||||
defaultOpt.value = '';
|
||||
defaultOpt.textContent = 'Student\u2026';
|
||||
studentSelect.appendChild(defaultOpt);
|
||||
for (const other of students) {
|
||||
if (other.id === student.id) continue;
|
||||
const opt = document.createElement('option');
|
||||
opt.value = other.id;
|
||||
opt.textContent = other.name;
|
||||
studentSelect.appendChild(opt);
|
||||
}
|
||||
const kindSelect = document.createElement('select');
|
||||
for (const kind of ['must', 'prefer', 'prefer_not', 'must_not']) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = kind;
|
||||
opt.textContent = kindLabels[kind];
|
||||
kindSelect.appendChild(opt);
|
||||
}
|
||||
const levelSelect = document.createElement('select');
|
||||
for (const level of ['student', 'parent', 'admin']) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = level;
|
||||
opt.textContent = level.charAt(0).toUpperCase() + level.slice(1);
|
||||
levelSelect.appendChild(opt);
|
||||
}
|
||||
const cAddBtn = document.createElement('button');
|
||||
cAddBtn.className = 'input-action';
|
||||
cAddBtn.textContent = '+';
|
||||
cAddBtn.addEventListener('click', async () => {
|
||||
const otherID = parseInt(studentSelect.value);
|
||||
if (!otherID) return;
|
||||
await api('POST', '/api/trips/' + tripID + '/constraints', {
|
||||
student_a_id: student.id,
|
||||
student_b_id: otherID,
|
||||
kind: kindSelect.value,
|
||||
level: levelSelect.value
|
||||
});
|
||||
loadStudents();
|
||||
});
|
||||
addRow.appendChild(studentSelect);
|
||||
addRow.appendChild(kindSelect);
|
||||
addRow.appendChild(levelSelect);
|
||||
addRow.appendChild(cAddBtn);
|
||||
cDetails.appendChild(addRow);
|
||||
|
||||
card.appendChild(cDetails);
|
||||
|
||||
const saved = openStates[student.id];
|
||||
if (saved) for (const det of card.querySelectorAll('wa-details')) if (saved[det.summary]) det.open = true;
|
||||
|
||||
container.appendChild(card);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user