163 lines
5.9 KiB
TypeScript
163 lines
5.9 KiB
TypeScript
/**
|
|
* Task CAD-9 - Stueckliste (BOM) fuer Traversen.
|
|
*
|
|
* buildBOM: gruppiert Traversen + Boxcorner nach Typ + Mass
|
|
* (Laenge x Breite), zaehlt Stueckzahlen, summiert Gesamt-meter.
|
|
* exportBOMCsv: saubere CSV mit Kopfzeile fuer Excel-Import.
|
|
* Dialog zeigt Tabelle + CSV-Download.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { buildBOM, exportBOMCsv, type BOMRow } from '../src/services/bomService';
|
|
import type { CADElement } from '../src/types/cad.types';
|
|
|
|
function truss(id: string, x: number, y: number, length: number, rotation: number, width = 29): CADElement {
|
|
return {
|
|
id, type: 'truss', layerId: 'l', x, y, width: length, height: width,
|
|
properties: { length, rotation, trussWidth: width, truss: true },
|
|
} as unknown as CADElement;
|
|
}
|
|
|
|
function corner(id: string, x: number, y: number, armLength: number, rotation: number, width = 29): CADElement {
|
|
return {
|
|
id, type: 'boxcorner', layerId: 'l', x, y, width: 80, height: 80,
|
|
properties: { armLength, rotation, trussWidth: width, boxcorner: true },
|
|
} as unknown as CADElement;
|
|
}
|
|
|
|
function chair(id: string): CADElement {
|
|
return {
|
|
id, type: 'chair', layerId: 'l', x: 0, y: 0, width: 45, height: 45,
|
|
properties: {},
|
|
} as unknown as CADElement;
|
|
}
|
|
|
|
// ─── buildBOM: Gruppierung ──────────────────────────────────
|
|
|
|
describe('CAD-9: buildBOM (Gruppierung + Zaehlung)', () => {
|
|
it('zwei identische Traversen (200x29) ergeben EINE Position mit Anzahl 2', () => {
|
|
const els = [
|
|
truss('t1', 0, 0, 200, 0),
|
|
truss('t2', 100, 100, 200, 90),
|
|
];
|
|
const bom = buildBOM(els);
|
|
const rows = bom.rows.filter((r) => r.type === 'Traverse');
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0].count).toBe(2);
|
|
});
|
|
|
|
it('unterschiedliche Laengen ergetrennte Positionen', () => {
|
|
const els = [
|
|
truss('t1', 0, 0, 200, 0),
|
|
truss('t2', 0, 0, 100, 0),
|
|
truss('t3', 0, 0, 300, 0),
|
|
];
|
|
const bom = buildBOM(els);
|
|
const trussRows = bom.rows.filter((r) => r.type === 'Traverse');
|
|
expect(trussRows).toHaveLength(3);
|
|
const counts = trussRows.map((r) => r.count);
|
|
expect(counts).toEqual([1, 1, 1]);
|
|
});
|
|
|
|
it('unterschiedliche Breiten = getrennte Positionen (100x29 vs 100x40)', () => {
|
|
const els = [
|
|
truss('t1', 0, 0, 100, 0, 29),
|
|
truss('t2', 0, 0, 100, 0, 40),
|
|
];
|
|
const bom = buildBOM(els);
|
|
expect(bom.rows.filter((r) => r.type === 'Traverse')).toHaveLength(2);
|
|
});
|
|
|
|
it('Boxcorner als eigener Typ mit armLength als Laenge', () => {
|
|
const els = [
|
|
corner('c1', 0, 0, 50, 0),
|
|
corner('c2', 100, 100, 50, 90),
|
|
corner('c3', 200, 200, 75, 0),
|
|
];
|
|
const bom = buildBOM(els);
|
|
const cornerRows = bom.rows.filter((r) => r.type === 'Eck-Traverse');
|
|
expect(cornerRows).toHaveLength(2); // 50cm x2 + 75cm x1
|
|
const r50 = cornerRows.find((r) => r.length === 50)!;
|
|
expect(r50.count).toBe(2);
|
|
});
|
|
|
|
it('Nicht-Traversen (chair etc.) werden ignoriert', () => {
|
|
const els = [chair('s1'), truss('t1', 0, 0, 200, 0)];
|
|
const bom = buildBOM(els);
|
|
expect(bom.rows.every((r) => r.type !== 'chair')).toBe(true);
|
|
});
|
|
|
|
it('leere Zeichnung: leere Stueckliste ohne Crash', () => {
|
|
const bom = buildBOM([]);
|
|
expect(bom.rows).toHaveLength(0);
|
|
expect(bom.totalLengthM).toBe(0);
|
|
});
|
|
|
|
it('Gesamtlaenge: 200+100+300cm Traversen + 2x50cm Ecken = 7.00m', () => {
|
|
const els = [
|
|
truss('t1', 0, 0, 200, 0),
|
|
truss('t2', 0, 0, 100, 0),
|
|
truss('t3', 0, 0, 300, 0),
|
|
corner('c1', 0, 0, 50, 0),
|
|
corner('c2', 0, 0, 50, 90),
|
|
];
|
|
const bom = buildBOM(els);
|
|
// Traversen 6.00m + 2 Ecken je 2 Arme = 8.00m
|
|
expect(bom.totalLengthM).toBeCloseTo(8.0, 2);
|
|
});
|
|
|
|
it('Boxcorner zaehlt BEIDE Arme in die Gesamtlaenge (50er-Ecke = 1.00m)', () => {
|
|
const els = [corner('c1', 0, 0, 50, 0)];
|
|
const bom = buildBOM(els);
|
|
expect(bom.totalLengthM).toBeCloseTo(1.0, 2);
|
|
});
|
|
|
|
it('Positionen sind sortiert: Traversen vor Ecken, dann nach Laenge absteigend', () => {
|
|
const els = [
|
|
corner('c1', 0, 0, 50, 0),
|
|
truss('t1', 0, 0, 100, 0),
|
|
truss('t2', 0, 0, 300, 0),
|
|
];
|
|
const bom = buildBOM(els);
|
|
const types = bom.rows.map((r) => r.type);
|
|
// Traversen zuerst (nach Laenge absteigend), dann Ecken
|
|
expect(types.indexOf('Traverse')).toBeLessThan(types.indexOf('Eck-Traverse'));
|
|
const trussLens = bom.rows.filter((r) => r.type === 'Traverse').map((r) => r.length);
|
|
expect(trussLens[0]).toBeGreaterThanOrEqual(trussLens[trussLens.length - 1]);
|
|
});
|
|
});
|
|
|
|
// ─── exportBOMCsv ─────────────────────────────────────────
|
|
|
|
describe('CAD-9: exportBOMCsv', () => {
|
|
it('CSV hat Kopfzeile und Zeilen pro Position mit Semikolon/ Komma', () => {
|
|
const els = [truss('t1', 0, 0, 200, 0), truss('t2', 0, 0, 200, 90)];
|
|
const bom = buildBOM(els);
|
|
const csv = exportBOMCsv(bom);
|
|
const lines = csv.split('\n');
|
|
expect(lines[0]).toContain('Pos');
|
|
expect(lines[0]).toContain('Typ');
|
|
expect(lines[0]).toContain('Anzahl');
|
|
expect(lines).toHaveLength(2 + 1); // Header + 1 Position + ggf. Summenzeile
|
|
});
|
|
|
|
it('CSV enthaelt Laenge und Anzahl der Position', () => {
|
|
const els = [truss('t1', 0, 0, 200, 0), truss('t2', 0, 0, 200, 0)];
|
|
const csv = exportBOMCsv(buildBOM(els));
|
|
expect(csv).toContain('200');
|
|
expect(csv).toContain('2');
|
|
});
|
|
|
|
it('CSV: Semikolon-Separatoren (Excel-DE-freundlich) oder Komma + konsistent', () => {
|
|
const csv = exportBOMCsv(buildBOM([truss('t1', 0, 0, 200, 0)]));
|
|
// Konsistenz: Jede Datenzeile hat dieselbe Anzahl Separatoren
|
|
const lines = csv.trim().split('\n');
|
|
const sepCounts = lines.map((l) => (l.match(/;/g) || []).length);
|
|
expect(new Set(sepCounts).size).toBe(1);
|
|
});
|
|
|
|
it('leere Stueckliste: nur Kopfzeile', () => {
|
|
const csv = exportBOMCsv(buildBOM([]));
|
|
expect(csv.trim().split('\n')).toHaveLength(1);
|
|
});
|
|
});
|