feat: Sitzungsverwaltung mit Stapel-Zuweisung und Ruecknahme
This commit is contained in:
+104
@@ -0,0 +1,104 @@
|
|||||||
|
// Reines Modul: kein window, kein document, kein localStorage.
|
||||||
|
|
||||||
|
import { specsCompatible } from './spec.js';
|
||||||
|
|
||||||
|
const STACK_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
|
||||||
|
/** @returns {{stacks: Array, entries: Array, nextEntryId: number}} */
|
||||||
|
export function createSession() {
|
||||||
|
return { stacks: [], entries: [], nextEntryId: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextStackId(session) {
|
||||||
|
const used = new Set(session.stacks.map((stack) => stack.id));
|
||||||
|
for (const letter of STACK_LETTERS) {
|
||||||
|
if (!used.has(letter)) return letter;
|
||||||
|
}
|
||||||
|
// Nach Z weiter mit A2, B2, ... - in der Praxis nie erreicht.
|
||||||
|
return `A${session.stacks.length}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schlaegt vor, wohin ein Modul gehoert.
|
||||||
|
* Genau ein vertraeglicher Stapel -> Zuweisung.
|
||||||
|
* Keiner -> neuer Stapel.
|
||||||
|
* Mehrere -> mehrdeutig, der Nutzer entscheidet.
|
||||||
|
*/
|
||||||
|
export function proposeAssignment(session, spec) {
|
||||||
|
const candidates = session.stacks
|
||||||
|
.filter((stack) => specsCompatible(stack.spec, spec))
|
||||||
|
.map((stack) => stack.id);
|
||||||
|
|
||||||
|
if (candidates.length === 1) {
|
||||||
|
return { kind: 'match', stackId: candidates[0], candidates };
|
||||||
|
}
|
||||||
|
if (candidates.length === 0) {
|
||||||
|
return { kind: 'new', stackId: nextStackId(session), candidates: [] };
|
||||||
|
}
|
||||||
|
return { kind: 'ambiguous', stackId: null, candidates };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bucht das Modul auf den angegebenen Stapel und legt ihn bei Bedarf an. */
|
||||||
|
export function commitAssignment(session, spec, source, stackId) {
|
||||||
|
let stack = session.stacks.find((candidate) => candidate.id === stackId);
|
||||||
|
if (!stack) {
|
||||||
|
stack = { id: stackId, spec, count: 0 };
|
||||||
|
session.stacks.push(stack);
|
||||||
|
session.stacks.sort((a, b) => a.id.localeCompare(b.id));
|
||||||
|
} else {
|
||||||
|
// Ein spaeterer, vollstaendigerer Scan ergaenzt fehlende Felder des Stapels.
|
||||||
|
for (const field of ['capacityGb', 'formFactor', 'rank', 'speed', 'partNumber']) {
|
||||||
|
if (stack.spec[field] === null && spec[field] !== null) {
|
||||||
|
stack.spec[field] = spec[field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.count += 1;
|
||||||
|
const entry = { entryId: session.nextEntryId++, spec, stackId, source };
|
||||||
|
session.entries.push(entry);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dropEmptyStacks(session) {
|
||||||
|
session.stacks = session.stacks.filter((stack) => stack.count > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nimmt den zuletzt erfassten Eintrag zurueck. */
|
||||||
|
export function undoLast(session) {
|
||||||
|
const entry = session.entries.pop();
|
||||||
|
if (!entry) return null;
|
||||||
|
const stack = session.stacks.find((candidate) => candidate.id === entry.stackId);
|
||||||
|
if (stack) stack.count -= 1;
|
||||||
|
dropEmptyStacks(session);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ordnet einen bereits erfassten Eintrag einem anderen Stapel zu. */
|
||||||
|
export function moveEntry(session, entryId, stackId) {
|
||||||
|
const entry = session.entries.find((candidate) => candidate.entryId === entryId);
|
||||||
|
if (!entry || entry.stackId === stackId) return;
|
||||||
|
|
||||||
|
const from = session.stacks.find((candidate) => candidate.id === entry.stackId);
|
||||||
|
if (from) from.count -= 1;
|
||||||
|
|
||||||
|
let to = session.stacks.find((candidate) => candidate.id === stackId);
|
||||||
|
if (!to) {
|
||||||
|
to = { id: stackId, spec: entry.spec, count: 0 };
|
||||||
|
session.stacks.push(to);
|
||||||
|
session.stacks.sort((a, b) => a.id.localeCompare(b.id));
|
||||||
|
}
|
||||||
|
to.count += 1;
|
||||||
|
entry.stackId = stackId;
|
||||||
|
dropEmptyStacks(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Entfernt einen einzelnen Eintrag aus der Sitzung. */
|
||||||
|
export function removeEntry(session, entryId) {
|
||||||
|
const index = session.entries.findIndex((candidate) => candidate.entryId === entryId);
|
||||||
|
if (index === -1) return;
|
||||||
|
const [entry] = session.entries.splice(index, 1);
|
||||||
|
const stack = session.stacks.find((candidate) => candidate.id === entry.stackId);
|
||||||
|
if (stack) stack.count -= 1;
|
||||||
|
dropEmptyStacks(session);
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { emptySpec } from '../src/spec.js';
|
||||||
|
import {
|
||||||
|
createSession, proposeAssignment, commitAssignment,
|
||||||
|
undoLast, moveEntry, removeEntry,
|
||||||
|
} from '../src/session.js';
|
||||||
|
|
||||||
|
const s64 = () => ({ ...emptySpec(), capacityGb: 64, formFactor: 'LRDIMM', rank: '4DRx4', speed: 'PC4-2400', partNumber: 'M386A8K40BM1-CRC4Y' });
|
||||||
|
const s32 = () => ({ ...emptySpec(), capacityGb: 32, formFactor: 'RDIMM', rank: '2Rx4', speed: 'PC4-2666', partNumber: 'M393A4K40BB1-CTD' });
|
||||||
|
|
||||||
|
function scan(session, spec, source = 'barcode') {
|
||||||
|
const plan = proposeAssignment(session, spec);
|
||||||
|
return commitAssignment(session, spec, source, plan.stackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('erster Scan legt Stapel A an', () => {
|
||||||
|
const session = createSession();
|
||||||
|
const plan = proposeAssignment(session, s64());
|
||||||
|
assert.equal(plan.kind, 'new');
|
||||||
|
assert.equal(plan.stackId, 'A');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gleiches Modul landet auf demselben Stapel', () => {
|
||||||
|
const session = createSession();
|
||||||
|
scan(session, s64());
|
||||||
|
const plan = proposeAssignment(session, s64());
|
||||||
|
assert.equal(plan.kind, 'match');
|
||||||
|
assert.equal(plan.stackId, 'A');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('anderes Modul legt Stapel B an', () => {
|
||||||
|
const session = createSession();
|
||||||
|
scan(session, s64());
|
||||||
|
const plan = proposeAssignment(session, s32());
|
||||||
|
assert.equal(plan.kind, 'new');
|
||||||
|
assert.equal(plan.stackId, 'B');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('eine Verwechslung erzeugt keinen Fast-Duplikat-Stapel', () => {
|
||||||
|
const session = createSession();
|
||||||
|
scan(session, s64());
|
||||||
|
const verrauscht = { ...s64(), partNumber: 'M386A8K4OBM1-CRC4Y' };
|
||||||
|
const plan = proposeAssignment(session, verrauscht);
|
||||||
|
assert.equal(plan.kind, 'match');
|
||||||
|
assert.equal(plan.stackId, 'A');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Scan ohne Teilenummer bei zwei passenden Stapeln ist mehrdeutig', () => {
|
||||||
|
const session = createSession();
|
||||||
|
scan(session, { ...s64(), partNumber: 'M386A8K40BM1-CRC4Y' });
|
||||||
|
scan(session, { ...s64(), partNumber: 'HMAA8GL7AMR4N-UH' });
|
||||||
|
const ohnePn = { ...s64(), partNumber: null };
|
||||||
|
const plan = proposeAssignment(session, ohnePn);
|
||||||
|
assert.equal(plan.kind, 'ambiguous');
|
||||||
|
assert.deepEqual(plan.candidates, ['A', 'B']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('commitAssignment zaehlt den Stapel hoch und fuehrt die Liste', () => {
|
||||||
|
const session = createSession();
|
||||||
|
scan(session, s64());
|
||||||
|
scan(session, s64());
|
||||||
|
assert.equal(session.stacks.length, 1);
|
||||||
|
assert.equal(session.stacks[0].count, 2);
|
||||||
|
assert.equal(session.entries.length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('undoLast nimmt den letzten Eintrag zurueck', () => {
|
||||||
|
const session = createSession();
|
||||||
|
scan(session, s64());
|
||||||
|
scan(session, s64());
|
||||||
|
const entfernt = undoLast(session);
|
||||||
|
assert.equal(entfernt.stackId, 'A');
|
||||||
|
assert.equal(session.stacks[0].count, 1);
|
||||||
|
assert.equal(session.entries.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('undoLast entfernt einen leer gewordenen Stapel', () => {
|
||||||
|
const session = createSession();
|
||||||
|
scan(session, s64());
|
||||||
|
undoLast(session);
|
||||||
|
assert.equal(session.stacks.length, 0);
|
||||||
|
assert.equal(undoLast(session), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('moveEntry sortiert einen Eintrag um', () => {
|
||||||
|
const session = createSession();
|
||||||
|
const a = scan(session, s64());
|
||||||
|
scan(session, s32());
|
||||||
|
moveEntry(session, a.entryId, 'B');
|
||||||
|
const stapelA = session.stacks.find((s) => s.id === 'A');
|
||||||
|
const stapelB = session.stacks.find((s) => s.id === 'B');
|
||||||
|
assert.equal(stapelA, undefined, 'leerer Stapel wird entfernt');
|
||||||
|
assert.equal(stapelB.count, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removeEntry entfernt einen einzelnen Eintrag', () => {
|
||||||
|
const session = createSession();
|
||||||
|
const a = scan(session, s64());
|
||||||
|
scan(session, s64());
|
||||||
|
removeEntry(session, a.entryId);
|
||||||
|
assert.equal(session.entries.length, 1);
|
||||||
|
assert.equal(session.stacks[0].count, 1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user