diff --git a/schema.sql b/schema.sql
index 98d12f9..3efef96 100644
--- a/schema.sql
+++ b/schema.sql
@@ -1,6 +1,3 @@
-DROP TABLE IF EXISTS roommate_constraints;
-DROP TYPE IF EXISTS constraint_kind;
-
DO $$ BEGIN
CREATE TYPE constraint_kind AS ENUM ('must', 'prefer', 'prefer_not', 'must_not');
EXCEPTION WHEN duplicate_object THEN NULL;
diff --git a/static/admin.html b/static/admin.html
index 2c1fca9..6cedb15 100644
--- a/static/admin.html
+++ b/static/admin.html
@@ -33,6 +33,7 @@
.trip-name { font-weight: bold; font-size: 1rem; color: var(--wa-color-brand-60); }
.tags { display: flex; flex-wrap: wrap; gap: 0.25rem; }
wa-tag { transition: opacity var(--wa-transition-normal); }
+ wa-tag::part(base) { padding: 0.15rem 0.4rem; }
wa-input.email { max-width: 20rem; }
.add-form { display: flex; flex-direction: column; gap: 0.3rem; max-width: 20rem; }
.add-form wa-button { align-self: flex-start; }
diff --git a/static/trip.html b/static/trip.html
index 97d9045..f63e504 100644
--- a/static/trip.html
+++ b/static/trip.html
@@ -32,6 +32,7 @@
.student-name { font-weight: bold; display: block; margin-bottom: 0.3rem; }
.tags { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 0.3rem; }
wa-tag { transition: opacity var(--wa-transition-normal); }
+ wa-tag::part(base) { padding: 0.15rem 0.4rem; }
wa-input.email { max-width: 20rem; }
.add-form { display: flex; flex-direction: column; gap: 0.3rem; max-width: 20rem; }
.add-form wa-button { align-self: flex-start; }
@@ -60,6 +61,9 @@
.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; }
+ #conflicts { margin-bottom: 0.5rem; }
+ .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; }
@@ -76,6 +80,7 @@
diff --git a/static/trip.js b/static/trip.js
index dff8a8b..0c11d46 100644
--- a/static/trip.js
+++ b/static/trip.js
@@ -27,6 +27,70 @@ async function loadStudents() {
api('GET', '/api/trips/' + tripID + '/students'),
api('GET', '/api/trips/' + tripID + '/constraints')
]);
+ const kindLabels = { must: 'Must', prefer: 'Prefer', prefer_not: 'Prefer Not', must_not: 'Must Not' };
+ const kindVariant = { must: 'success', prefer: 'brand', prefer_not: 'warning', must_not: 'danger' };
+ const kindColor = { must: 'var(--wa-color-success-50)', prefer: 'var(--wa-color-brand-50)', prefer_not: 'var(--wa-color-warning-50)', must_not: 'var(--wa-color-danger-50)' };
+ const kindOrder = { must: 0, prefer: 1, prefer_not: 2, must_not: 3 };
+ const isPositive = kind => kind === 'must' || kind === 'prefer';
+ const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1);
+
+ const pairs = {};
+ for (const c of constraints) {
+ const k = c.student_a_id + '-' + c.student_b_id;
+ if (!pairs[k]) pairs[k] = [];
+ pairs[k].push(c);
+ }
+ const conflictList = [];
+ const conflictMap = {};
+ for (const group of Object.values(pairs)) {
+ const pos = group.filter(c => isPositive(c.kind));
+ const neg = group.filter(c => !isPositive(c.kind));
+ if (pos.length === 0 || neg.length === 0) continue;
+ conflictList.push({
+ names: group[0].student_a_name + ' \u2192 ' + group[0].student_b_name,
+ positives: pos.map(c => ({ level: c.level, kind: c.kind })),
+ negatives: neg.map(c => ({ level: c.level, kind: c.kind }))
+ });
+ for (const c of group) {
+ const opposing = isPositive(c.kind) ? neg : pos;
+ conflictMap[c.id] = opposing.map(o => capitalize(o.level) + ' says ' + kindLabels[o.kind]).join(', ');
+ }
+ }
+
+ const conflictsEl = document.getElementById('conflicts');
+ const conflictsWasOpen = conflictsEl.querySelector('wa-details')?.open;
+ conflictsEl.innerHTML = '';
+ if (conflictList.length > 0) {
+ const det = document.createElement('wa-details');
+ det.summary = '\u26a0 Overrides (' + conflictList.length + ')';
+ 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) {
+ const div = document.createElement('div');
+ div.className = 'conflict-row';
+ div.appendChild(document.createTextNode(conflict.names + ': '));
+ conflict.positives.forEach((p, i) => {
+ if (i > 0) div.appendChild(document.createTextNode(', '));
+ div.appendChild(document.createTextNode(capitalize(p.level) + ' '));
+ div.appendChild(kindSpan(p.kind));
+ });
+ div.appendChild(document.createTextNode(' vs '));
+ conflict.negatives.forEach((n, i) => {
+ if (i > 0) div.appendChild(document.createTextNode(', '));
+ div.appendChild(document.createTextNode(capitalize(n.level) + ' '));
+ div.appendChild(kindSpan(n.kind));
+ });
+ det.appendChild(div);
+ }
+ conflictsEl.appendChild(det);
+ }
+
const container = document.getElementById('students');
const openStates = {};
for (const card of container.children) {
@@ -103,9 +167,6 @@ async function loadStudents() {
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']) {
@@ -130,11 +191,20 @@ async function loadStudents() {
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();
});
+ if (conflictMap[c.id]) {
+ const icon = document.createElement('span');
+ icon.className = 'conflict-icon';
+ icon.textContent = '\u26a0 ';
+ tag.appendChild(icon);
+ tag.appendChild(document.createTextNode(kindLabels[c.kind] + ': ' + otherName));
+ tag.title = 'Overrides: ' + conflictMap[c.id];
+ } else {
+ tag.textContent = kindLabels[c.kind] + ': ' + otherName;
+ }
group.appendChild(tag);
}
cDetails.appendChild(group);