Make constraints directional and add mismatch detection
This commit is contained in:
7
drop.sql
Normal file
7
drop.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
DROP TABLE IF EXISTS roommate_constraints;
|
||||||
|
DROP TABLE IF EXISTS parents;
|
||||||
|
DROP TABLE IF EXISTS students;
|
||||||
|
DROP TABLE IF EXISTS trip_admins;
|
||||||
|
DROP TABLE IF EXISTS trips;
|
||||||
|
DROP TYPE IF EXISTS constraint_level;
|
||||||
|
DROP TYPE IF EXISTS constraint_kind;
|
||||||
6
main.go
6
main.go
@@ -654,10 +654,6 @@ func handleCreateConstraint(db *sql.DB) http.HandlerFunc {
|
|||||||
http.Error(w, "invalid level", http.StatusBadRequest)
|
http.Error(w, "invalid level", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a, b := body.StudentAID, body.StudentBID
|
|
||||||
if a > b {
|
|
||||||
a, b = b, a
|
|
||||||
}
|
|
||||||
var id int64
|
var id int64
|
||||||
err := db.QueryRow(`
|
err := db.QueryRow(`
|
||||||
INSERT INTO roommate_constraints (student_a_id, student_b_id, kind, level)
|
INSERT INTO roommate_constraints (student_a_id, student_b_id, kind, level)
|
||||||
@@ -666,7 +662,7 @@ func handleCreateConstraint(db *sql.DB) http.HandlerFunc {
|
|||||||
JOIN students sb ON sb.id = $2 AND sb.trip_id = $5
|
JOIN students sb ON sb.id = $2 AND sb.trip_id = $5
|
||||||
WHERE sa.id = $1 AND sa.trip_id = $5
|
WHERE sa.id = $1 AND sa.trip_id = $5
|
||||||
ON CONFLICT (student_a_id, student_b_id, level) DO UPDATE SET kind = EXCLUDED.kind
|
ON CONFLICT (student_a_id, student_b_id, level) DO UPDATE SET kind = EXCLUDED.kind
|
||||||
RETURNING id`, a, b, body.Kind, body.Level, tripID).Scan(&id)
|
RETURNING id`, body.StudentAID, body.StudentBID, body.Kind, body.Level, tripID).Scan(&id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -43,6 +43,6 @@ CREATE TABLE IF NOT EXISTS roommate_constraints (
|
|||||||
student_b_id BIGINT NOT NULL REFERENCES students(id) ON DELETE CASCADE,
|
student_b_id BIGINT NOT NULL REFERENCES students(id) ON DELETE CASCADE,
|
||||||
kind constraint_kind NOT NULL,
|
kind constraint_kind NOT NULL,
|
||||||
level constraint_level NOT NULL,
|
level constraint_level NOT NULL,
|
||||||
CHECK(student_a_id < student_b_id),
|
CHECK(student_a_id != student_b_id),
|
||||||
UNIQUE(student_a_id, student_b_id, level)
|
UNIQUE(student_a_id, student_b_id, level)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
.constraint-add { display: flex; gap: 0.5rem; align-items: center; margin-top: 0.3rem; }
|
.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; }
|
.constraint-add select { font-size: 0.75rem; padding: 0.15rem; border: 1px solid var(--wa-color-neutral-300, #ccc); border-radius: 0.25rem; }
|
||||||
#conflicts { margin-bottom: 0.5rem; }
|
#conflicts { margin-bottom: 0.5rem; }
|
||||||
|
#mismatches { margin-bottom: 0.5rem; }
|
||||||
.conflict-row { margin-bottom: 0.2rem; }
|
.conflict-row { margin-bottom: 0.2rem; }
|
||||||
.conflict-icon { background: var(--wa-color-danger-50, #dc3545); color: white; border-radius: 0.15rem; padding: 0 0.15rem; font-size: 0.6rem; line-height: 1.2; vertical-align: middle; margin-right: 0.1rem; display: inline-block; }
|
.conflict-icon { background: var(--wa-color-danger-50, #dc3545); color: white; border-radius: 0.15rem; padding: 0 0.15rem; font-size: 0.6rem; line-height: 1.2; vertical-align: middle; margin-right: 0.1rem; display: inline-block; }
|
||||||
</style>
|
</style>
|
||||||
@@ -81,6 +82,7 @@
|
|||||||
<input id="room-size" type="number" min="1">
|
<input id="room-size" type="number" min="1">
|
||||||
</div>
|
</div>
|
||||||
<div id="conflicts"></div>
|
<div id="conflicts"></div>
|
||||||
|
<div id="mismatches"></div>
|
||||||
<div id="students"></div>
|
<div id="students"></div>
|
||||||
<wa-details summary="Add Student">
|
<wa-details summary="Add Student">
|
||||||
<div class="add-form">
|
<div class="add-form">
|
||||||
|
|||||||
@@ -57,6 +57,45 @@ async function loadStudents() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const kindSpan = (kind) => {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.textContent = kindLabels[kind];
|
||||||
|
span.style.color = kindColor[kind];
|
||||||
|
span.style.fontWeight = 'bold';
|
||||||
|
return span;
|
||||||
|
};
|
||||||
|
|
||||||
|
const allOveralls = {};
|
||||||
|
for (const s of students) {
|
||||||
|
const myC = constraints.filter(c => c.student_a_id === s.id);
|
||||||
|
const byPeer = {};
|
||||||
|
for (const c of myC) {
|
||||||
|
if (!byPeer[c.student_b_id]) byPeer[c.student_b_id] = {};
|
||||||
|
byPeer[c.student_b_id][c.level] = c;
|
||||||
|
}
|
||||||
|
allOveralls[s.id] = {};
|
||||||
|
for (const [peerId, levels] of Object.entries(byPeer)) {
|
||||||
|
const eff = levels.admin || levels.parent || levels.student;
|
||||||
|
if (eff) allOveralls[s.id][peerId] = eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mismatchList = [];
|
||||||
|
for (const s of students) {
|
||||||
|
for (const [bId, effA] of Object.entries(allOveralls[s.id])) {
|
||||||
|
if (!allOveralls[bId] || !allOveralls[bId][s.id]) continue;
|
||||||
|
const effB = allOveralls[bId][s.id];
|
||||||
|
if (isPositive(effA.kind) && !isPositive(effB.kind)) {
|
||||||
|
mismatchList.push({
|
||||||
|
nameA: s.name,
|
||||||
|
nameB: students.find(x => x.id === parseInt(bId)).name,
|
||||||
|
kindA: effA.kind,
|
||||||
|
kindB: effB.kind,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const conflictsEl = document.getElementById('conflicts');
|
const conflictsEl = document.getElementById('conflicts');
|
||||||
const conflictsWasOpen = conflictsEl.querySelector('wa-details')?.open;
|
const conflictsWasOpen = conflictsEl.querySelector('wa-details')?.open;
|
||||||
conflictsEl.innerHTML = '';
|
conflictsEl.innerHTML = '';
|
||||||
@@ -64,13 +103,6 @@ async function loadStudents() {
|
|||||||
const det = document.createElement('wa-details');
|
const det = document.createElement('wa-details');
|
||||||
det.summary = '\u26a0 Overrides (' + conflictList.length + ')';
|
det.summary = '\u26a0 Overrides (' + conflictList.length + ')';
|
||||||
if (conflictsWasOpen) det.open = true;
|
if (conflictsWasOpen) det.open = true;
|
||||||
const kindSpan = (kind) => {
|
|
||||||
const span = document.createElement('span');
|
|
||||||
span.textContent = kindLabels[kind];
|
|
||||||
span.style.color = kindColor[kind];
|
|
||||||
span.style.fontWeight = 'bold';
|
|
||||||
return span;
|
|
||||||
};
|
|
||||||
for (const conflict of conflictList) {
|
for (const conflict of conflictList) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'conflict-row';
|
div.className = 'conflict-row';
|
||||||
@@ -91,6 +123,25 @@ async function loadStudents() {
|
|||||||
conflictsEl.appendChild(det);
|
conflictsEl.appendChild(det);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mismatchesEl = document.getElementById('mismatches');
|
||||||
|
const mismatchesWasOpen = mismatchesEl.querySelector('wa-details')?.open;
|
||||||
|
mismatchesEl.innerHTML = '';
|
||||||
|
if (mismatchList.length > 0) {
|
||||||
|
const det = document.createElement('wa-details');
|
||||||
|
det.summary = '\u26a0 Mismatches (' + mismatchList.length + ')';
|
||||||
|
if (mismatchesWasOpen) det.open = true;
|
||||||
|
for (const m of mismatchList) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'conflict-row';
|
||||||
|
div.appendChild(document.createTextNode(m.nameA + ' \u2192 ' + m.nameB + ': '));
|
||||||
|
div.appendChild(kindSpan(m.kindA));
|
||||||
|
div.appendChild(document.createTextNode(' but ' + m.nameB + ' \u2192 ' + m.nameA + ': '));
|
||||||
|
div.appendChild(kindSpan(m.kindB));
|
||||||
|
det.appendChild(div);
|
||||||
|
}
|
||||||
|
mismatchesEl.appendChild(det);
|
||||||
|
}
|
||||||
|
|
||||||
const container = document.getElementById('students');
|
const container = document.getElementById('students');
|
||||||
const openStates = {};
|
const openStates = {};
|
||||||
for (const card of container.children) {
|
for (const card of container.children) {
|
||||||
@@ -167,26 +218,14 @@ async function loadStudents() {
|
|||||||
const cDetails = document.createElement('wa-details');
|
const cDetails = document.createElement('wa-details');
|
||||||
cDetails.summary = 'Constraints';
|
cDetails.summary = 'Constraints';
|
||||||
|
|
||||||
const myConstraints = constraints.filter(c => c.student_a_id === student.id || c.student_b_id === student.id);
|
const myConstraints = constraints.filter(c => c.student_a_id === student.id);
|
||||||
|
|
||||||
const byPeer = {};
|
const overall = Object.values(allOveralls[student.id] || {});
|
||||||
for (const c of myConstraints) {
|
|
||||||
const otherId = c.student_a_id === student.id ? c.student_b_id : c.student_a_id;
|
|
||||||
if (!byPeer[otherId]) byPeer[otherId] = {};
|
|
||||||
byPeer[otherId][c.level] = c;
|
|
||||||
}
|
|
||||||
const overall = [];
|
|
||||||
for (const levels of Object.values(byPeer)) {
|
|
||||||
const eff = levels.admin || levels.parent || levels.student;
|
|
||||||
if (eff) overall.push(eff);
|
|
||||||
}
|
|
||||||
if (overall.length > 0) {
|
if (overall.length > 0) {
|
||||||
overall.sort((a, b) => {
|
overall.sort((a, b) => {
|
||||||
const kd = kindOrder[a.kind] - kindOrder[b.kind];
|
const kd = kindOrder[a.kind] - kindOrder[b.kind];
|
||||||
if (kd !== 0) return kd;
|
if (kd !== 0) return kd;
|
||||||
const na = a.student_a_id === student.id ? a.student_b_name : a.student_a_name;
|
return a.student_b_name.localeCompare(b.student_b_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');
|
const group = document.createElement('div');
|
||||||
group.className = 'constraint-group';
|
group.className = 'constraint-group';
|
||||||
@@ -195,11 +234,10 @@ async function loadStudents() {
|
|||||||
levelLabel.textContent = 'Overall';
|
levelLabel.textContent = 'Overall';
|
||||||
group.appendChild(levelLabel);
|
group.appendChild(levelLabel);
|
||||||
for (const c of overall) {
|
for (const c of overall) {
|
||||||
const otherName = c.student_a_id === student.id ? c.student_b_name : c.student_a_name;
|
|
||||||
const tag = document.createElement('wa-tag');
|
const tag = document.createElement('wa-tag');
|
||||||
tag.size = 'small';
|
tag.size = 'small';
|
||||||
tag.variant = kindVariant[c.kind];
|
tag.variant = kindVariant[c.kind];
|
||||||
tag.textContent = kindLabels[c.kind] + ': ' + otherName;
|
tag.textContent = kindLabels[c.kind] + ': ' + c.student_b_name;
|
||||||
tag.title = 'From ' + capitalize(c.level);
|
tag.title = 'From ' + capitalize(c.level);
|
||||||
group.appendChild(tag);
|
group.appendChild(tag);
|
||||||
}
|
}
|
||||||
@@ -212,9 +250,7 @@ async function loadStudents() {
|
|||||||
lc.sort((a, b) => {
|
lc.sort((a, b) => {
|
||||||
const kd = kindOrder[a.kind] - kindOrder[b.kind];
|
const kd = kindOrder[a.kind] - kindOrder[b.kind];
|
||||||
if (kd !== 0) return kd;
|
if (kd !== 0) return kd;
|
||||||
const na = a.student_a_id === student.id ? a.student_b_name : a.student_a_name;
|
return a.student_b_name.localeCompare(b.student_b_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');
|
const group = document.createElement('div');
|
||||||
group.className = 'constraint-group';
|
group.className = 'constraint-group';
|
||||||
@@ -223,7 +259,7 @@ async function loadStudents() {
|
|||||||
levelLabel.textContent = level.charAt(0).toUpperCase() + level.slice(1);
|
levelLabel.textContent = level.charAt(0).toUpperCase() + level.slice(1);
|
||||||
group.appendChild(levelLabel);
|
group.appendChild(levelLabel);
|
||||||
for (const c of lc) {
|
for (const c of lc) {
|
||||||
const otherName = c.student_a_id === student.id ? c.student_b_name : c.student_a_name;
|
const otherName = c.student_b_name;
|
||||||
const tag = document.createElement('wa-tag');
|
const tag = document.createElement('wa-tag');
|
||||||
tag.size = 'small';
|
tag.size = 'small';
|
||||||
tag.variant = kindVariant[c.kind];
|
tag.variant = kindVariant[c.kind];
|
||||||
|
|||||||
Reference in New Issue
Block a user