- 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.
109 lines
7.6 KiB
JavaScript
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};
|
|
});
|