bmc_hub/static/js/case-commands.js
Christian 4871dd2d9e Add comprehensive test coverage for various modules
- Introduced tests for mail templates including ranking, rendering, and attachment handling.
- Added tests for time billing selection and approval processes, ensuring proper validation and state management.
- Implemented tests for case types in JavaScript, focusing on loading and editing functionality.
- Created tests for contact mail AI integration, ensuring proper parsing and error handling.
- Developed tests for dark theme CSS contrast and loading order.
- Added tests for invoice fee service, covering various scenarios including exemptions and invalid inputs.
- Implemented vendor resource tests, ensuring proper file handling and query validation.
2026-10-08 08:58:02 +02:00

109 lines
7.6 KiB
JavaScript

/* Deterministic comment commands. Parsing never performs an action. */
(function (root, factory) {
const api = factory();
if (typeof module === 'object' && module.exports) module.exports = api;
if (root) root.CaseCommands = api;
})(typeof window !== 'undefined' ? window : globalThis, function () {
'use strict';
const names = ['tid','tildel','forventet','undersag','mig','todo','afventer','løsning','deadline','udsat','fil','hjælp','timer','relation','prioritet','gruppe','mail','note'];
function tokens(value) {
const result = []; let i = 0;
while (i < value.length) {
if (/\s/.test(value[i])) { i++; continue; }
if (value[i] === '"') {
let text = ''; let closed = false; i++;
while (i < value.length) {
if (value[i] === '\\' && ['"','\\'].includes(value[i + 1])) { text += value[i + 1]; i += 2; }
else if (value[i] === '"') { i++; closed = true; break; }
else text += value[i++];
}
if (!closed) throw new Error('Afslut teksten med et dobbelt anførselstegn.');
if (i < value.length && !/\s/.test(value[i])) throw new Error('Der skal være mellemrum efter den citerede tekst.');
result.push({value:text,quoted:true});
} else {
const start = i; while (i < value.length && !/\s/.test(value[i])) i++;
const text = value.slice(start,i);
if (text.includes('"')) throw new Error('Sæt hele teksten i dobbelte anførselstegn.');
result.push({value:text,quoted:false});
}
}
return result;
}
function duration(text) {
const m = /^(\d+(?:[.,]\d+)?)(m|min|h|t)?$/i.exec(text || '');
if (!m) throw new Error('Angiv tid som 30m, 1h eller et antal minutter.');
const value = Number(m[1].replace(',','.')) * (/^[ht]$/i.test(m[2] || '') ? 60 : 1);
if (!Number.isInteger(value) || value <= 0 || value > 525600) throw new Error('Tiden skal være et positivt, helt antal minutter.');
return value;
}
function iso(date) { return [date.getFullYear(),String(date.getMonth()+1).padStart(2,'0'),String(date.getDate()).padStart(2,'0')].join('-'); }
function dateValue(text, now) {
const value = text.toLowerCase().trim();
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
const parts = value.split('-').map(Number); const d = new Date(parts[0],parts[1]-1,parts[2]);
if (iso(d) !== value) throw new Error('Datoen findes ikke.');
return value;
}
const base = now instanceof Date ? new Date(now.getTime()) : now ? new Date(now) : new Date();
if (Number.isNaN(base.getTime())) throw new Error('Startdatoen er ugyldig.');
base.setHours(12,0,0,0);
if (value === 'i dag' || value === 'idag') return iso(base);
if (value === 'i morgen' || value === 'imorgen') { base.setDate(base.getDate()+1); return iso(base); }
const weekdays = ['søndag','mandag','tirsdag','onsdag','torsdag','fredag','lørdag'];
const day = weekdays.indexOf(value);
if (day !== -1) { base.setDate(base.getDate()+(day-base.getDay()+7)%7); return iso(base); }
throw new Error('Angiv en dato som 2026-10-09, i morgen eller fredag.');
}
function parse(text, options) {
const commands = [], errors = [], comment = []; let scope = null; let subcaseCount = 0;
options = options || {};
String(text || '').split(/\r?\n/).forEach(function (raw,index) {
const line = raw.trim();
if (!line.startsWith('/')) { comment.push(raw); return; }
const match = /^(\/\/|\/)([^\s/]+)(?:\s+(.*))?$/.exec(line);
const name = match ? match[2].toLowerCase() : '';
try {
if (!match || !names.includes(name)) throw new Error(name === 'tildek' ? 'Mente du /tildel?' : 'Ukendt kommando. Skriv /hjælp for at se mulighederne.');
if (match[1] === '//' && scope === null) throw new Error('// kræver en /undersag ovenover.');
const args = tokens(match[3] || ''); const values = args.map(t => t.value);
const cmd = {type:'',scope:match[1] === '//' ? scope : 'main',line:index+1};
function count(n) { if (args.length !== n) throw new Error('Kommandoen har manglende eller ekstra oplysninger.'); }
function quoted(i) { if (!args[i] || !args[i].quoted || !args[i].value.trim()) throw new Error('Skriv teksten i dobbelte anførselstegn.'); return args[i].value.trim(); }
function identifier(i) {
const value = args[i] ? values[i].trim() : '';
const pattern = args[i] && args[i].quoted ? /^@?[\p{L}\p{N}_.-]+(?:\s+[\p{L}\p{N}_.-]+)*$/u : /^@?[\p{L}\p{N}_.-]+$/u;
if (!value || !pattern.test(value)) throw new Error('Angiv initialer eller et fuldt navn i dobbelte anførselstegn.');
return value.replace(/^@/,'').replace(/\s+/g,' ');
}
switch (name) {
case 'undersag': count(1); if(match[1] === '//') throw new Error('Opret en undersag med /undersag, ikke //undersag.'); cmd.type='subcase'; cmd.title=quoted(0); cmd.scope=subcaseCount++; scope=cmd.scope; break;
case 'tid': count(3); cmd.type='time'; cmd.minutes=duration(values[0]); cmd.category=identifier(1); cmd.description=quoted(2); break;
case 'tildel': count(2); cmd.type='assign'; cmd.assignee=identifier(0); cmd.reason=quoted(1); break;
case 'forventet': count(1); cmd.type='estimate'; cmd.minutes=duration(values[0]); break;
case 'mig': count(0); cmd.type='self'; break;
case 'todo': {
cmd.type='todo'; cmd.title=quoted(0); let rest=values.slice(1);
if (rest[0] && rest[0].startsWith('@')) { cmd.assignee=identifier(1); rest=rest.slice(1); }
if(rest.length) {
if (args.slice(values.length-rest.length).some(t=>t.quoted)) throw new Error('Todo-fristen skal skrives uden anførselstegn.');
cmd.date=dateValue(rest.join(' '),options.now);
}
break;
}
case 'afventer': count(2); if(values[0].toLowerCase() !== 'kunde') throw new Error('Brug /afventer kunde "Begrundelse".'); cmd.type='waiting'; cmd.reason=quoted(1); break;
case 'løsning': case 'note': case 'mail': count(1); cmd.type=name === 'løsning' ? 'solution' : name; cmd.text=quoted(0); break;
case 'deadline': case 'udsat': if(!args.length || args.some(t=>t.quoted)) throw new Error('Angiv en dato efter kommandoen.'); cmd.type=name === 'udsat' ? 'defer' : 'deadline'; cmd.date=dateValue(values.join(' '),options.now); break;
case 'fil': case 'hjælp': count(0); cmd.type=name === 'fil' ? 'file' : 'help'; break;
case 'timer': cmd.type='timer'; cmd.action=(values[0] || '').toLowerCase(); if(cmd.action === 'start') { count(2); cmd.description=quoted(1); } else if(cmd.action === 'pause') count(1); else throw new Error('Brug /timer start "Beskrivelse" eller /timer pause.'); break;
case 'relation': count(1); if(!/^#?[1-9]\d*$/.test(values[0]) || !Number.isSafeInteger(Number(values[0].replace('#','')))) throw new Error('Angiv et sagsnummer som #1248.'); cmd.type='relation'; cmd.caseId=Number(values[0].replace('#','')); break;
case 'prioritet': count(2); cmd.type='priority'; cmd.priority=identifier(0).toLowerCase(); if(!['lav','normal','høj','kritisk'].includes(cmd.priority)) throw new Error('Prioritet skal være lav, normal, høj eller kritisk.'); cmd.reason=quoted(1); break;
case 'gruppe': count(2); cmd.type='group'; cmd.group=identifier(0); cmd.reason=quoted(1); break;
}
commands.push(cmd);
} catch(error) { errors.push({line:index+1,command:name,message:error.message}); }
});
return {comment:comment.join('\n').trim(),commands:commands,errors:errors};
}
return {parse:parse,duration:duration,resolveDate:dateValue,names:names};
});