Add Vue 3 composable for group sizing and print-safe layout generation

This commit is contained in:
2026-01-29 02:00:27 +00:00
parent 308001594b
commit 7dc792abc9

View File

@@ -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
};
}