const {test} = require('node:test'); const assert = require('node:assert/strict'); const vm = require('node:vm'); const fs = require('node:fs'); const source = fs.readFileSync('static/js/work-desk.js','utf8'); function extract(start,end) {const a=source.indexOf(start),b=source.indexOf(end,a);assert.ok(a>0&&b>a);return source.slice(a,b);} test('allocated and fixed/actual time have independent calendar columns with overlap lanes',()=>{ const context={Date,workHours:[{weekday:2,start_time:'08:30:00',end_time:'16:30:00'}]};vm.createContext(context); vm.runInContext(extract(' function calendarColumns(', ' async function events('),context); const event=(id,start,end,column)=>({id,start:'2026-10-21T'+start+':00',end:'2026-10-21T'+end+':00',extendedProps:{deskColumn:column}}); const events=context.calendarColumns([event(1,'09:00','10:00',0),event(2,'09:30','10:30',0),event(3,'09:00','10:00',1),event(4,'11:00','12:00',0)]); assert.equal(events[0].extendedProps.deskLeft,0);assert.equal(events[0].extendedProps.deskRight,75); assert.equal(events[1].extendedProps.deskLeft,25); assert.equal(events[2].extendedProps.deskLeft,50);assert.equal(events[2].extendedProps.deskRight,0); assert.equal(events[3].extendedProps.deskLeft,0);assert.equal(events[3].extendedProps.deskRight,50); assert.equal(context.workingWindow('2026-10-21').start,'08:30'); assert.equal(context.workingWindow('2026-10-21').end,'16:30'); assert.equal(context.workingWindow('2026-10-22').start,'07:00'); }); test('dropping Planlæg saves at the chosen time without opening a dialog and supports undo',async()=>{ const calls=[];let undo,refreshes=0; const context={busy:false,desk:{can_edit:true},metadata:{settings:{default_allocation_minutes:60}},uid:4,planner:'/api/v1/planner', mutation:async(path,method,body)=>{calls.push({path,method,body:JSON.parse(JSON.stringify(body))});return{event:{id:8,version:2}};}, api:async(path,options)=>calls.push({path,...options}),clock:()=>'',status:(text,fn)=>{undo=fn;},refresh:async()=>{refreshes++;},error:e=>{throw e;},Date}; vm.createContext(context);vm.runInContext(extract(' function reservationDuration(', ' function plan('),context); await context.dropReservation({sag_id:12,titel:'Test',remaining_work_minutes:100,missing_minutes:40},new Date('2026-10-21T08:15:00Z')); assert.equal(calls[0].body.starts_at,'2026-10-21T08:15:00.000Z'); assert.equal(calls[0].body.ends_at,'2026-10-21T09:00:00.000Z'); assert.deepEqual(calls[0].body.user_ids,[4]);assert.equal(refreshes,1); await undo();assert.equal(calls[1].path,'/api/v1/planner/events/8?version=2');assert.equal(calls[1].method,'DELETE'); assert.equal(context.reservationDuration({remaining_work_minutes:null}),60); assert.equal(context.reservationDuration({remaining_work_minutes:180}),180); }); test('quick meeting records the chosen duration for multiple employees without a case or customer',()=>{ const context={Date};vm.createContext(context); vm.runInContext(extract(' function meetingPayload(', ' async function meeting('),context); const form={elements:{description:{value:' Internt møde '}},querySelectorAll:()=>[{value:'4'},{value:'7'}]}; for(const minutes of [15,30,45,60]) { const before=Date.now(),payload=context.meetingPayload(form,'request-1',minutes,null,null); assert.equal(payload.mode,'registered');assert.equal(payload.minutes,minutes); assert.deepEqual(Array.from(payload.user_ids),[4,7]); assert.equal(payload.customer_id,null);assert.equal(payload.contact_id,null); assert.equal(payload.description,'Internt møde');assert.equal(payload.sag_id,undefined); assert.ok(Math.abs(new Date(payload.starts_at).getTime()-(before-minutes*60000))<100); } const customer=context.meetingPayload(form,'request-2',30,9,12); assert.equal(customer.customer_id,9);assert.equal(customer.contact_id,12); assert.throws(()=>context.meetingPayload(form,'x',20,null,null)); assert.throws(()=>context.meetingPayload({...form,querySelectorAll:()=>[]},'x',30,null,null)); }); test('only a scheduling conflict can be confirmed and retried',async()=>{ const calls=[]; let responses=[{status:409,detail:{confirm_conflicts:true},message:'Overlap'},{}]; const context={api:async(path,options)=>{calls.push(JSON.parse(options.body));const next=responses.shift();if(next.status)throw next;return next;},confirm:()=>true,JSON}; vm.createContext(context);vm.runInContext(extract(' async function mutation(', ' function error('),context); await context.mutation('/events/5','PATCH',{version:3}); assert.deepEqual(calls,[{version:3},{version:3,confirm_conflicts:true}]); responses=[{status:409,detail:{current:{version:4}},message:'Stale'}];calls.length=0; await assert.rejects(context.mutation('/events/5','PATCH',{version:3})); assert.equal(calls.length,1); }); test('reordering retains future-day tasks and undo is versioned',async()=>{ const calls=[];let undo; const context={desk:{version:9,can_edit:true,manual_order:false,items:[{sag_id:1},{sag_id:2},{sag_id:3,in_day:false}]}, planner:'/api/v1/planner',mutation:async(path,method,body)=>{calls.push(JSON.parse(JSON.stringify(body)));return{version:10};}, load:async()=>{},status:(message,action)=>{undo=action;}}; vm.createContext(context);vm.runInContext(extract(' async function saveOrder(', ' function eventBody('),context); await context.reorder(2,1); assert.deepEqual(calls[0],{ids:[2,1,3],mode:'manual',version:9}); await undo(); assert.deepEqual(calls[1],{ids:[1,2,3],mode:'recommended',version:10}); }); test('moving shared meetings preserves all participants and undo uses new event version',async()=>{ const calls=[];let undo,reverted=false; const booking={id:7,event_id:5,event_version:3,title:'Meeting',kind:'meeting',participants:[1,2],starts_at:'2026-10-21T08:00:00Z',ends_at:'2026-10-21T09:00:00Z'}; const context={uid:1,metadata:{editable_user_ids:[1,2]},planner:'/api/v1/planner', mutation:async(path,method,body)=>{calls.push({path,method,body:JSON.parse(JSON.stringify(body))});return{event:{version:4}};}, status:(message,action)=>{undo=action;},refresh:async()=>{},error:()=>{},calendar:{refetchEvents(){}}}; vm.createContext(context);vm.runInContext(extract(' function eventBody(', ' function plan('),context); await context.changeBooking({event:{extendedProps:{booking},start:new Date('2026-10-21T10:00:00Z'),end:new Date('2026-10-21T11:00:00Z')},revert:()=>{reverted=true;}}); assert.equal(reverted,false); assert.deepEqual(calls[0].body.user_ids,[1,2]); assert.equal(calls[0].body.version,3); await undo(); assert.equal(calls[1].body.version,4); assert.equal(calls[1].body.starts_at,booking.starts_at); }); test('locked or unauthorized booking is reverted without a write',async()=>{ let writes=0,reverted=0; const context={uid:1,metadata:{editable_user_ids:[1]},planner:'/api/v1/planner',mutation:async()=>{writes++;}}; vm.createContext(context);vm.runInContext(extract(' function eventBody(', ' function plan('),context); for(const booking of [{participants:[1,2]},{participants:[1],locked:true}]) await context.changeBooking({event:{extendedProps:{booking}},revert:()=>{reverted++;}}); assert.equal(writes,0);assert.equal(reverted,2); });