Files
itflow/js/contract_form.js

152 lines
6.6 KiB
JavaScript

// New Contract, Copy and Edit (agent/modals/contract/contract_form.php).
// Picking a template fills the type, term, renewal, notice, cover and terms
// from it. Picking the client shows only that client's contacts. The end
// date follows the start and the term - a term's length less a day - until
// it is changed by hand, and the line under the term says when notice is
// due. The server checks it all again.
//
// Wrapped in an IIFE because a modal can be opened, closed and opened again
// in one page load, which re-runs this file.
(function () {
// 'Y-m-d' a term's length after $start, less a day - contractTermEnd() in functions/contract.php
function termEnd(start, months) {
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(start || '');
if (!m || !(months > 0)) {
return '';
}
const year = Number(m[1]);
const month = Number(m[2]) - 1 + Number(months);
const day = Number(m[3]);
// The next term starts on the same day of the month - or the last day of a month too short for it
// (Jan 31 a month on is Feb 28) - and this one ends the day before
const last = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
const end = new Date(Date.UTC(year, month, Math.min(day, last)));
end.setUTCDate(end.getUTCDate() - 1);
return end.toISOString().slice(0, 10);
}
function niceDate(ymd) {
const d = new Date(ymd + 'T00:00:00Z');
return isNaN(d) ? ymd : d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', timeZone: 'UTC' });
}
document.querySelectorAll('[data-contract-form]:not([data-wired])').forEach(function (form) {
form.setAttribute('data-wired', '1');
const templateData = form.querySelector('[data-contract-templates]');
const templates = templateData ? JSON.parse(templateData.textContent) : {};
const template = form.querySelector('[data-contract-template]');
const client = form.querySelector('[data-contract-client]');
const contact = form.querySelector('[data-contract-contact]');
const start = form.querySelector('[data-contract-start]');
const term = form.querySelector('[data-contract-term]');
const end = form.querySelector('[data-contract-end]');
const noticeNote = form.querySelector('[data-contract-notice-note]');
const field = function (name) {
return form.querySelector('[data-contract-field="' + name + '"]');
};
function drawNotice() {
const days = Number(field('notice_days').value || 0);
if (!end.value) {
noticeNote.textContent = 'No end date - it runs until it is cancelled.';
return;
}
const notice = new Date(end.value + 'T00:00:00Z');
notice.setUTCDate(notice.getUTCDate() - days);
const renews = field('auto_renew').checked;
noticeNote.textContent = (renews ? 'Renews itself after ' : 'Ends on ') + niceDate(end.value)
+ (days ? ' - notice ' + (renews ? 'to stop it ' : '') + 'by ' + niceDate(notice.toISOString().slice(0, 10)) + '.' : '.');
}
function followTerm() {
if (!end.dataset.touched && start.value) {
end.value = termEnd(start.value, Number(term.value));
}
drawNotice();
}
// Only the client's own contacts
function filterContacts() {
if (!client || !contact) {
return;
}
const id = client.value;
Array.prototype.forEach.call(contact.options, function (option) {
if (option.value === '0') {
return;
}
const mine = option.dataset.client === id;
option.hidden = !mine;
if (!mine && option.selected) {
contact.value = '0';
}
});
}
function setValue(name, value) {
const input = field(name);
if (!input) {
return;
}
if (input.type === 'checkbox') {
input.checked = Boolean(Number(value));
} else if (input.tagName === 'TEXTAREA' && window.tinymce) {
const editor = tinymce.get().filter(function (ed) { return ed.getElement() === input; })[0];
if (editor) {
editor.setContent(value || '');
}
input.value = value || '';
} else {
input.value = value === null || value === undefined ? '' : value;
}
input.dispatchEvent(new Event('change', { bubbles: true }));
}
if (template) {
template.addEventListener('change', function () {
const t = templates[template.value];
if (!t) {
return;
}
// The template's name only when nothing is written yet
if (!field('name').value.trim()) {
setValue('name', t.name);
}
['type', 'auto_renew', 'notice_days', 'rate_standard', 'rate_after_hours', 'net_terms', 'support_hours', 'details',
'sla_low', 'sla_medium', 'sla_high', 'sla_urgent', 'monthly_hours', 'hours_alert'].forEach(function (name) {
if (name in t) {
setValue(name, t[name]);
}
});
// A term the list doesn't have is added to it
if (!Array.prototype.some.call(term.options, function (o) { return Number(o.value) === t.term_months; })) {
term.add(new Option(t.term_months + ' months', t.term_months));
}
term.value = String(t.term_months);
followTerm();
});
}
if (client && client.tagName === 'SELECT') {
client.addEventListener('change', filterContacts);
// Select2 changes the select without a native event
if (window.jQuery) {
window.jQuery(client).on('change', filterContacts);
}
filterContacts();
}
start.addEventListener('change', followTerm);
term.addEventListener('change', followTerm);
end.addEventListener('input', function () {
end.dataset.touched = '1';
drawNotice();
});
field('notice_days').addEventListener('input', drawNotice);
field('auto_renew').addEventListener('change', drawNotice);
drawNotice();
});
})();