/** * useGroupPrintLayout * -------------------------------------------------- * Vue 3 composable for group sizing and * print-safe layout generation. * * Uses only Vue reactivity + vanilla JS. */ import { ref, computed } from 'vue'; /** * @param {Object} options * @param {number} options.rowsPerPage * @param {number} options.headerRows * @param {number} options.spacerRows */ export function useGroupPrintLayout(options) { // ----------------------------- // Reactive state // ----------------------------- const players = ref([]); const groupIds = ref([]); const droppedPlayerIds = ref([]); // ----------------------------- // Internal helpers (pure JS) // ----------------------------- function calculateGroupSizes(totalPlayers, ids) { const baseSize = Math.floor(totalPlayers / ids.length); const remainder = totalPlayers % ids.length; return ids.map((id, index) => ({ id, players: [], size: baseSize + (index < remainder ? 1 : 0) })); } function assignPlayersToGroups(playerList, groups) { let index = 0; groups.forEach(group => { for (let i = 0; i < group.size; i++) { if (index >= playerList.length) break; const player = { ...playerList[index] }; player.groupId = group.id; group.players.push(player); index++; } }); return groups; } function applyDrops(groups, dropIds) { if (!dropIds.length) return groups; const dropSet = new Set(dropIds); groups.forEach(group => { group.players = group.players.filter(player => !dropSet.has(player.id)); }); return groups; } function calculateGroupPrintRows(group) { return options.headerRows + group.players.length + options.spacerRows; } function generatePrintLayout(groups) { const pages = []; let pageNumber = 1; let currentPage = { pageNumber, groups: [], usedRows: 0 }; groups.forEach(group => { const groupRows = calculateGroupPrintRows(group); if (currentPage.usedRows + groupRows > options.rowsPerPage) { pages.push(currentPage); pageNumber++; currentPage = { pageNumber, groups: [], usedRows: 0 }; } currentPage.groups.push(group); currentPage.usedRows += groupRows; }); pages.push(currentPage); return pages; } // ----------------------------- // Computed pipeline // ----------------------------- const groupedData = computed(() => { if (!players.value.length || !groupIds.value.length) return []; let groups = calculateGroupSizes(players.value.length, groupIds.value); groups = assignPlayersToGroups(players.value, groups); groups = applyDrops(groups, droppedPlayerIds.value); return groups; }); const printPages = computed(() => { if (!groupedData.value.length) return []; return generatePrintLayout(groupedData.value); }); // ----------------------------- // Public API // ----------------------------- function setPlayers(list) { players.value = Array.isArray(list) ? list : []; } function setGroups(ids) { groupIds.value = Array.isArray(ids) ? ids : []; } function dropPlayer(id) { if (!droppedPlayerIds.value.includes(id)) { droppedPlayerIds.value.push(id); } } function restorePlayer(id) { droppedPlayerIds.value = droppedPlayerIds.value.filter(pid => pid !== id); } function resetDrops() { droppedPlayerIds.value = []; } return { // state players, groupIds, droppedPlayerIds, // derived groupedData, printPages, // actions setPlayers, setGroups, dropPlayer, restorePlayer, resetDrops }; }