/***** Major Flag Football – Team Generator v1.0 ***** * How to use: * 1) Set TABLE/VIEW names below to match your base. * 2) Run script after new registrations (or put in an Automation). * 3) Review output log for any clusters that couldn’t be placed. *****************************************************/ //////////////////// CONFIG //////////////////// const CONFIG = { playersTable: "Players", teamsTable: "Teams", settingsTable: "Settings", // optional; leave blank "" if unused playerView: "", // optional: a view filtered to Status=Registered and no Team onlyStatus: "Registered", allowCreateTeams: false, // set to true if you want the script to create placeholder teams defaultTeamNamePrefix: "Team", defaultMaxRoster: 10, }; //////////////////// LOAD TABLES //////////////////// const playersTbl = base.getTable(CONFIG.playersTable); const teamsTbl = base.getTable(CONFIG.teamsTable); const settingsTbl = CONFIG.settingsTable ? base.getTable(CONFIG.settingsTable) : null; const playersQuery = await playersTbl.selectRecordsAsync({ fields: ["Player Name","Division","Requested Coach","Friend Group ID","Sibling Group ID","Team","Status"] }); const teamsQuery = await teamsTbl.selectRecordsAsync({ fields: ["Team Name","Coach","Division","Max Roster"] }); let settingsByDivision = {}; if (settingsTbl) { const settingsQuery = await settingsTbl.selectRecordsAsync({ fields: ["Division","Min Roster","Max Roster","Allow Cross-Grade (Y/N)"] }); for (let rec of settingsQuery.records) { const div = rec.getCellValueAsString("Division"); if (!div) continue; settingsByDivision[div] = { min: Number(rec.getCellValue("Min Roster") || 0), max: Number(rec.getCellValue("Max Roster") || CONFIG.defaultMaxRoster), allowCross: (rec.getCellValueAsString("Allow Cross-Grade (Y/N)") || "N").toUpperCase() === "Y", }; } } //////////////////// HELPERS //////////////////// function getTeamCounts() { // Count current roster sizes from Players.Team links const counts = {}; for (let p of playersQuery.records) { const teamLink = p.getCellValue("Team"); if (teamLink && teamLink.length) { const teamId = teamLink[0].id; counts[teamId] = (counts[teamId] || 0) + 1; } } return counts; } function teamsByDivision(division) { return teamsQuery.records.filter(t => (t.getCellValueAsString("Division") || "").trim() === (division || "").trim()); } function getTeamCapacity(teamRec, counts) { const max = Number(teamRec.getCellValue("Max Roster") || CONFIG.defaultMaxRoster); const current = counts[teamRec.id] || 0; return max - current; } function normalized(str) { return (str || "").trim().toLowerCase(); } //////////////////// BUILD CLUSTERS //////////////////// // Build clusters from unassigned + Registered players, merging Friend Group and Sibling Group const unassigned = playersQuery.records.filter(p => (p.getCellValueAsString("Status") === CONFIG.onlyStatus) && (!p.getCellValue("Team") || p.getCellValue("Team").length === 0) ); const clusters = new Map(); // key -> {players:[], division, requestedCoach} for (let p of unassigned) { const keyParts = [ p.getCellValueAsString("Friend Group ID"), p.getCellValueAsString("Sibling Group ID"), // if neither is set, fallback to player's own id to create a single-person cluster p.id ].filter(Boolean); const key = keyParts.length > 0 ? keyParts[0] : p.id; if (!clusters.has(key)) { clusters.set(key, { players: [], division: p.getCellValueAsString("Division"), requestedCoach: p.getCellValueAsString("Requested Coach") }); } clusters.get(key).players.push(p); } // Sort clusters largest-first to place big groups first const clusterList = Array.from(clusters.values()).sort((a,b) => b.players.length - a.players.length); //////////////////// ASSIGNMENT //////////////////// const teamCounts = getTeamCounts(); const updates = []; // {playerId, teamId} const unplaced = []; for (let cluster of clusterList) { const division = cluster.division; const divTeams = teamsByDivision(division); // Get settings cap if present const divMax = settingsByDivision[division]?.max ?? CONFIG.defaultMaxRoster; // Prefer requested coach if exists and has capacity let preferred = null; if (cluster.requestedCoach) { preferred = divTeams.find(t => normalized(t.getCellValueAsString("Coach")) === normalized(cluster.requestedCoach)); if (preferred) { const cap = getTeamCapacity(preferred, teamCounts); if (cap >= cluster.players.length) { // Assign all to preferred for (let p of cluster.players) updates.push({playerId: p.id, teamId: preferred.id}); teamCounts[preferred.id] = (teamCounts[preferred.id] || 0) + cluster.players.length; continue; // next cluster } } } // Else, place in team with most remaining capacity in this division const sortedByCapacity = divTeams .map(t => ({team: t, cap: getTeamCapacity(t, teamCounts)})) .sort((a,b) => b.cap - a.cap); let placed = false; for (let entry of sortedByCapacity) { if (entry.cap >= cluster.players.length && entry.cap > 0) { for (let p of cluster.players) updates.push({playerId: p.id, teamId: entry.team.id}); teamCounts[entry.team.id] = (teamCounts[entry.team.id] || 0) + cluster.players.length; placed = true; break; } } // Optionally create a new team if none had space if (!placed && CONFIG.allowCreateTeams) { // Create a simple placeholder team in this division const newTeamName = `${CONFIG.defaultTeamNamePrefix} ${division} ${teamsByDivision(division).length + 1}`; const createRec = await teamsTbl.createRecordAsync({ "Team Name": newTeamName, "Division": { name: division }, "Max Roster": divMax }); teamCounts[createRec] = 0; // Assign to the new team for (let p of cluster.players) updates.push({playerId: p.id, teamId: createRec}); teamCounts[createRec] += cluster.players.length; placed = true; } if (!placed) { unplaced.push({ clusterSize: cluster.players.length, division, requestedCoach: cluster.requestedCoach || "(none)" }); } } //////////////////// WRITE BACK //////////////////// let batch = []; for (let up of updates) { batch.push({ id: up.playerId, fields: { "Team": [ { id: up.teamId } ] } }); if (batch.length === 50) { await playersTbl.updateRecordsAsync(batch); batch = []; } } if (batch.length) await playersTbl.updateRecordsAsync(batch); //////////////////// REPORT //////////////////// output.markdown(`### Team Generator Complete`); output.markdown(`Assigned **${updates.length}** players across **${clusterList.length}** clusters.`); if (unplaced.length) { output.markdown(`\n**Unplaced clusters (need capacity or new teams):**`); unplaced.forEach(u => { output.text(`- Division ${u.division} | size ${u.clusterSize} | requested coach: ${u.requestedCoach}`); }); } else { output.text(`All clusters placed successfully.`); }