diff --git a/code/websites/pokedex.online/src/composables/useGroupPrintLayout.js b/code/websites/pokedex.online/src/composables/useGroupPrintLayout.js index e69de29..a781a9f 100644 --- a/code/websites/pokedex.online/src/composables/useGroupPrintLayout.js +++ b/code/websites/pokedex.online/src/composables/useGroupPrintLayout.js @@ -0,0 +1,165 @@ +/** + * 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 + }; +}