Erweitere VoTiAn NG um Fachfunktionen und verbessere die Listenoberfläche

Ergänze Auftragsbearbeitung, Disposition, Stammdaten, Kunden- und
Transporteurportale, Lagerverwaltung, Abrechnung, Exporte und Statistik.
Füge Kontoeinstellungen, Mitteilungen, Automatikdienste und Betriebsstatus
sowie die zugehörigen Tests und PHP-Abgleichsdokumentation hinzu.

Vergrößere die persönlichen Auftragslisten auf einheitliche Höhen und
richte Statusanzeigen sowie die Ranking-Vorschau-Steuerelemente bündig aus.

Validierung: 44 Frontendtests und Produktionsbuild erfolgreich.
Backend funktional unverändert gegenüber dem vollständigen Lauf mit 1.624
erfolgreichen und drei übersprungenen Tests. Layout im Browser und lokale
Bereitstellung geprüft.
This commit is contained in:
Sven committed 2026-10-20 11:19:43 +02:00
1 parent 363bc27c26
commit 781a2b82dc
1165 files changed
+74986 -1028

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
import test from 'node:test';import assert from 'node:assert/strict';import vm from 'node:vm';import {readFileSync} from 'node:fs';import ts from 'typescript';
const code=ts.transpileModule(readFileSync(new URL('../src/api.ts',import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText;
const home={userId:11,username:'synthetic',headquartersId:7,originalHeadquartersId:7,roles:[]};
const json=(data,status=200)=>new Response(JSON.stringify(data),{status,headers:{'Content-Type':'application/json'}});
function deferred(){let resolve;const promise=new Promise(r=>resolve=r);return {promise,resolve};}
function client(stored){const entries=new Map(stored?[['votian.workspace',JSON.stringify(stored)]]:[]),storage={getItem:k=>entries.get(k)||null,setItem:(k,v)=>entries.set(k,v),removeItem:k=>entries.delete(k)};const calls=[];let intercept;
const context={exports:{},Headers,URLSearchParams,Response,URL,setTimeout,sessionStorage:storage,window:{sessionStorage:storage,localStorage:{removeItem(){}}},fetch:async(path,options)=>{calls.push({path,options});if(intercept){const v=intercept(path,options);if(v)return await v;}if(path==='/api/me'||path==='/api/login')return json(home);if(path==='/api/csrf')return json({token:'isolated',headerName:'X-CSRF-TOKEN'});if(path.startsWith('/api/account/headquarters/'))return json({...home,headquartersId:Number(path.split('/').pop())});if(path==='/api/logout')return new Response(null,{status:204});return json({ok:true});}};
vm.runInNewContext(code,context,{filename:'api.js'});return {api:context.exports,calls,entries,intercept:fn=>intercept=fn};}
const header=call=>call.options.headers.get('X-Workspace-Headquarters');
test('restores only a preference bound to the same user and home; recovery endpoints stay unscoped',async()=>{const c=client({userId:11,homeId:7,target:8});assert.equal((await c.api.getSession()).headquartersId,8);assert(c.calls.every(x=>header(x)===null));await c.api.request('/customers');assert.equal(header(c.calls.at(-1)),'8');await c.api.request('/account/headquarters');assert.equal(header(c.calls.at(-1)),null);await c.api.logout();assert.equal(header(c.calls.find(x=>x.path==='/api/logout')),null);assert(!c.entries.has('votian.workspace'));const other=client({userId:99,homeId:7,target:8});assert.equal((await other.api.getSession()).headquartersId,7);assert(!other.calls.some(x=>x.path.endsWith('/headquarters/8')));});
test('failed selection retains the previous scope; denied saved selection recovers to home',async()=>{const c=client();await c.api.getSession();await c.api.changeWorkspace(8);c.intercept(p=>p.endsWith('/headquarters/9')?json({message:'denied'},403):null);await assert.rejects(c.api.changeWorkspace(9),/denied/);await c.api.request('/customers');assert.equal(header(c.calls.at(-1)),'8');const restored=client({userId:11,homeId:7,target:8});restored.intercept(p=>p.endsWith('/headquarters/8')?json({message:'revoked'},403):null);const u=await restored.api.getSession();assert.equal(u.headquartersId,7);assert.match(u.workspaceNotice,/revoked/);assert(!restored.entries.has('votian.workspace'));});
test('a response from an old headquarters cannot enter the next workspace',async()=>{const c=client();await c.api.getSession();await c.api.changeWorkspace(8);const read=deferred();c.intercept(p=>p==='/api/orders'?read.promise:null);const pending=c.api.request('/orders');const rejection=assert.rejects(pending,/beendeten Sitzung/);await c.api.changeWorkspace(7);read.resolve(json({old:true}));await rejection;assert.equal(header(c.calls.find(x=>x.path==='/api/orders')),'8');await c.api.request('/customers');assert.equal(header(c.calls.at(-1)),null);});
test('switching waits for the full write response and blocks new writes during preflight',async()=>{const c=client();await c.api.getSession();const sent=deferred(),write=deferred();c.intercept(p=>{if(p==='/api/change'){sent.resolve();return write.promise;}});const saving=c.api.put('/change',{value:'synthetic'});await sent.promise;await assert.rejects(c.api.changeWorkspace(8),/Änderung läuft/);write.resolve(json({completed:true}));await saving;const selecting=deferred();c.intercept(p=>p.endsWith('/headquarters/8')?selecting.promise:null);const switching=c.api.changeWorkspace(8);await assert.rejects(c.api.put('/change',{value:'second'}),/Niederlassung wird gewechselt/);selecting.resolve(json({...home,headquartersId:8}));await switching;});
test('two tabs keep separate contexts and caller headers cannot override the selection',async()=>{const a=client(),b=client();await a.api.getSession();await b.api.getSession();await a.api.changeWorkspace(8);await a.api.request('/customers',{headers:{'X-Workspace-Headquarters':'99','X-Order-Headquarters':'18'}});assert.equal(header(a.calls.at(-1)),'8');assert.equal(a.calls.at(-1).options.headers.get('X-Order-Headquarters'),'18');await b.api.request('/customers');assert.equal(header(b.calls.at(-1)),null);});