Teams tile: one Colors group, no Positions switches

Colors and Status colors were two groups asking the same question, so they
are one: team header, the tints and the three status colours together.

The Positions switches go entirely — the team switches already decide what
a tile shows, and "Group by position" still arranges what is left. With
them go GET /positions and positionOptions().

Two rules the tile was breaking on the way past: a gear group may carry no
`help` (rule 5), and only hide-toggles belong in Show (rule 8) — so the
group blurbs are gone and the Columns picker sits loose at the top beside
the team switches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Waters Church
2026-09-24 15:15:34 -04:00
co-authored by Claude Opus 5
parent 6d0f8828c1
commit 6c0aee5abf
3 changed files with 38 additions and 86 deletions
+34 -56
View File
@@ -411,21 +411,35 @@
"h": 2 "h": 2
}, },
"instanceSchema": { "instanceSchema": {
"columns": {
"type": "select",
"label": "Columns",
"default": "auto",
"options": [
{
"value": "auto",
"label": "Fit the tile"
},
{
"value": "1",
"label": "One"
},
{
"value": "2",
"label": "Two"
},
{
"value": "3",
"label": "Three"
}
]
},
"teams": { "teams": {
"type": "multiselect", "type": "multiselect",
"label": "Teams", "label": "Teams",
"group": "Teams",
"optionsRoute": "/teams",
"columns": 1,
"allByDefault": true
},
"positions": {
"type": "multiselect",
"label": "Positions",
"group": "Positions",
"optionsRoute": "/positions",
"columns": 2, "columns": 2,
"allByDefault": true "allByDefault": true,
"optionsRoute": "/teams"
}, },
"headerTemplate": { "headerTemplate": {
"type": "template", "type": "template",
@@ -484,12 +498,6 @@
"default": false, "default": false,
"group": "Colors" "group": "Colors"
}, },
"headerGradient": {
"type": "boolean",
"label": "Header fade",
"default": true,
"group": "Colors"
},
"tintRows": { "tintRows": {
"type": "boolean", "type": "boolean",
"label": "Tint rows too", "label": "Tint rows too",
@@ -497,23 +505,29 @@
"group": "Colors", "group": "Colors",
"showWhen": "tintTeams" "showWhen": "tintTeams"
}, },
"headerGradient": {
"type": "boolean",
"label": "Header fade",
"default": true,
"group": "Colors"
},
"confirmedColor": { "confirmedColor": {
"type": "color", "type": "color",
"label": "Confirmed", "label": "Confirmed",
"default": "#2ee59a", "default": "#2ee59a",
"group": "Status colors" "group": "Colors"
}, },
"unconfirmedColor": { "unconfirmedColor": {
"type": "color", "type": "color",
"label": "Unconfirmed", "label": "Unconfirmed",
"default": "#f0b429", "default": "#f0b429",
"group": "Status colors" "group": "Colors"
}, },
"declinedColor": { "declinedColor": {
"type": "color", "type": "color",
"label": "Declined", "label": "Declined",
"default": "#f4433c", "default": "#f4433c",
"group": "Status colors" "group": "Colors"
}, },
"showTopBar": { "showTopBar": {
"type": "boolean", "type": "boolean",
@@ -580,52 +594,16 @@
"label": "Teams with nobody", "label": "Teams with nobody",
"default": false, "default": false,
"group": "Show" "group": "Show"
},
"columns": {
"type": "select",
"label": "Columns",
"default": "auto",
"group": "Show",
"options": [
{
"value": "auto",
"label": "Fit the tile"
},
{
"value": "1",
"label": "One"
},
{
"value": "2",
"label": "Two"
},
{
"value": "3",
"label": "Three"
}
]
} }
}, },
"instanceGroups": { "instanceGroups": {
"Top bar": { "Top bar": {
"collapsed": false "collapsed": false
}, },
"Teams": {
"collapsed": true,
"help": "Untick a team to leave it out of this tile."
},
"Positions": {
"collapsed": true,
"help": "Untick a position to leave it out. Positions follow the teams above."
},
"Colors": { "Colors": {
"collapsed": false, "collapsed": false,
"columns": 2 "columns": 2
}, },
"Status colors": {
"collapsed": true,
"columns": 3
},
"Show": { "Show": {
"collapsed": true, "collapsed": true,
"columns": 2 "columns": 2
+4 -28
View File
@@ -18,7 +18,6 @@
* GET /note-categories → { options } for a tile's "Item notes" categories: * GET /note-categories → { options } for a tile's "Item notes" categories:
* the service type's item note categories, by name * the service type's item note categories, by name
* GET /teams → { options } for a Teams tile's "Teams" switches * GET /teams → { options } for a Teams tile's "Teams" switches
* GET /positions → { options } for a Teams tile's "Positions" switches
* GET /team-avatar/<planPersonId> → that person's photo, proxied + cached * GET /team-avatar/<planPersonId> → that person's photo, proxied + cached
* GET /timers → snapshot for the Timers module (guide: "Offering * GET /timers → snapshot for the Timers module (guide: "Offering
* GET /timers/stream → SSE, `timers` events timers to the Timers module") * GET /timers/stream → SSE, `timers` events timers to the Timers module")
@@ -821,24 +820,6 @@ module.exports = {
return { options: state.teams.map((t) => ({ value: t.id, label: t.name })) }; return { options: state.teams.map((t) => ({ value: t.id, label: t.name })) };
} }
/**
* Options for a Teams tile's "Positions" switches. Positions are named,
* not numbered, in Planning Center, so the name is the value; the team
* name groups them so two "Leader" positions stay apart in the list.
*/
function positionOptions() {
const seen = new Set();
const options = [];
for (const team of state.teams) {
for (const m of team.members) {
const value = m.position || '(no position)';
if (seen.has(value)) continue;
seen.add(value);
options.push({ value, label: value, group: team.name });
}
}
return { options };
}
async function readPlan() { async function readPlan() {
if (!pco.configured || !serviceTypeId) return; if (!pco.configured || !serviceTypeId) return;
@@ -1589,7 +1570,6 @@ module.exports = {
documentOptions, documentOptions,
noteCategoryOptions, noteCategoryOptions,
teamOptions, teamOptions,
positionOptions,
avatarUrlFor: (id) => teamPhotos.get(String(id)) || "", avatarUrlFor: (id) => teamPhotos.get(String(id)) || "",
}; };
@@ -1684,19 +1664,15 @@ module.exports = {
}, },
/** /**
* Options for a Teams tile's "Teams" and "Positions" switches. Like the * Options for a Teams tile's "Teams" switches. Like the note
* note categories, always a 200 with a list: an empty one simply means * categories, always a 200 with a list: an empty one simply means there
* there is nothing scheduled to choose from yet, which the gear window * is nothing scheduled to choose from yet, which the gear window says
* says plainly. * plainly.
*/ */
'GET /teams': (req, res) => { 'GET /teams': (req, res) => {
json(res, 200, current ? current.teamOptions() : { options: [] }); json(res, 200, current ? current.teamOptions() : { options: [] });
}, },
'GET /positions': (req, res) => {
json(res, 200, current ? current.positionOptions() : { options: [] });
},
/** /**
* A scheduled person's photo, proxied from Planning Center and cached. * A scheduled person's photo, proxied from Planning Center and cached.
* The tile asks by plan-person id and never sees the CDN URL; an id the * The tile asks by plan-person id and never sees the CDN URL; an id the
-2
View File
@@ -94,12 +94,10 @@ export default function createTeams({ root, moduleApi }) {
function visibleTeams() { function visibleTeams() {
const teams = Array.isArray(state?.teams) ? state.teams : []; const teams = Array.isArray(state?.teams) ? state.teams : [];
const wantTeams = chosen('teams'); const wantTeams = chosen('teams');
const wantPositions = chosen('positions');
const out = []; const out = [];
for (const team of teams) { for (const team of teams) {
if (wantTeams && !wantTeams.includes(team.id)) continue; if (wantTeams && !wantTeams.includes(team.id)) continue;
const members = team.members.filter((m) => { const members = team.members.filter((m) => {
if (wantPositions && !wantPositions.includes(m.position || '(no position)')) return false;
if (m.status === 'declined' && !on('showDeclined')) return false; if (m.status === 'declined' && !on('showDeclined')) return false;
if (m.status === 'unconfirmed' && !on('showUnconfirmed')) return false; if (m.status === 'unconfirmed' && !on('showUnconfirmed')) return false;
return true; return true;