mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 07:01:16 +00:00
219 lines
8.9 KiB
JavaScript
219 lines
8.9 KiB
JavaScript
/*
|
|
* Import Statement's review (agent/expense_import.php?step=review).
|
|
*
|
|
* Each line's vendor, category and client start as they were matched. A
|
|
* vendor picked for a line fills in its category (the vendor's last one)
|
|
* and the same vendor on the other lines from that merchant that nothing
|
|
* matched yet; + New vendor shows a name to make it with. Bill on shows
|
|
* once there is a client. Lines not new (money in, lines already in) are
|
|
* hidden until asked for. The ticked lines need a vendor and category;
|
|
* the summary and button follow the ticks. On Import every line's choices
|
|
* go as one JSON field, so a long statement stays under max_input_vars.
|
|
*
|
|
* Needs js/billing_items.js for itflowBillingMoney when it is loaded, and
|
|
* falls back to Intl otherwise.
|
|
*
|
|
* itflowExpenseImport({ currency: 'USD', locale: 'en-US', defaults: { vendor_id: category_id } });
|
|
*/
|
|
function itflowExpenseImport(options) {
|
|
var opts = options || {};
|
|
var form = document.getElementById('expenseImportReview');
|
|
if (!form || form.dataset.wired) {
|
|
return;
|
|
}
|
|
form.dataset.wired = '1';
|
|
var rows = Array.prototype.slice.call(form.querySelectorAll('[data-import-row]'));
|
|
var all = form.querySelector('[data-import-all]');
|
|
var showAll = form.querySelector('[data-import-show-all]');
|
|
var summary = form.querySelector('[data-import-summary]');
|
|
var submit = form.querySelector('[data-import-submit]');
|
|
var submitLabel = form.querySelector('[data-import-submit-label]');
|
|
var picks = form.querySelector('[data-import-picks]');
|
|
var bulkCategory = form.querySelector('[data-import-bulk-category]');
|
|
var bulkClient = form.querySelector('[data-import-bulk-client]');
|
|
var defaults = opts.defaults || {};
|
|
|
|
function money(amount) {
|
|
try {
|
|
return new Intl.NumberFormat(opts.locale || undefined, { style: 'currency', currency: opts.currency || 'USD' }).format(amount);
|
|
} catch (e) {
|
|
return amount.toFixed(2);
|
|
}
|
|
}
|
|
|
|
function part(row, name) {
|
|
return row.querySelector('[data-import-' + name + ']');
|
|
}
|
|
|
|
// One line: the new vendor's name only for + New vendor, Bill on only with a client
|
|
function tidy(row) {
|
|
var vendor = part(row, 'vendor');
|
|
if (!vendor) {
|
|
return;
|
|
}
|
|
var take = part(row, 'take');
|
|
part(row, 'new-vendor').hidden = vendor.value !== 'new';
|
|
part(row, 'billable-box').hidden = part(row, 'client').value === '0' || Number(row.getAttribute('data-amount')) <= 0;
|
|
var on = take.checked;
|
|
vendor.required = on;
|
|
part(row, 'category').required = on;
|
|
part(row, 'new-vendor').required = on && vendor.value === 'new';
|
|
row.classList.toggle('itflow-unbilled-off', !on);
|
|
}
|
|
|
|
function update() {
|
|
var count = 0;
|
|
var total = 0;
|
|
rows.forEach(function (row) {
|
|
var take = part(row, 'take');
|
|
tidy(row);
|
|
if (take.checked && !take.disabled) {
|
|
count++;
|
|
total += Number(row.getAttribute('data-amount'));
|
|
}
|
|
});
|
|
summary.textContent = count ? count + ' line' + (count === 1 ? '' : 's') + ' · ' + money(Math.round(total * 100) / 100) : 'Tick the lines to import';
|
|
submitLabel.textContent = count ? 'Import ' + count : 'Import';
|
|
submit.disabled = count === 0;
|
|
if (all) {
|
|
var shown = rows.filter(function (row) { return !row.hidden && !part(row, 'take').disabled; });
|
|
var on = shown.filter(function (row) { return part(row, 'take').checked; }).length;
|
|
all.checked = on > 0 && on === shown.length;
|
|
all.indeterminate = on > 0 && on < shown.length;
|
|
}
|
|
}
|
|
|
|
// Start each pick where it was matched
|
|
rows.forEach(function (row) {
|
|
['vendor', 'category', 'client'].forEach(function (name) {
|
|
var select = part(row, name);
|
|
if (select && select.getAttribute('data-value') !== null) {
|
|
var value = select.getAttribute('data-value');
|
|
if (select.querySelector('option[value="' + CSS.escape(value) + '"]')) {
|
|
select.value = value;
|
|
}
|
|
}
|
|
});
|
|
if (row.hasAttribute('data-import-extra')) {
|
|
row.hidden = true;
|
|
}
|
|
});
|
|
|
|
// A vendor picked fills in its category, and the same merchant's other unmatched lines
|
|
form.addEventListener('change', function (e) {
|
|
var row = e.target.closest('[data-import-row]');
|
|
if (row && e.target.matches('[data-import-vendor]')) {
|
|
row.dataset.touched = '1';
|
|
var vendor = e.target.value;
|
|
var category = part(row, 'category');
|
|
if (vendor !== '' && vendor !== 'new' && defaults[vendor] && !category.dataset.touched) {
|
|
category.value = String(defaults[vendor]);
|
|
}
|
|
var key = row.getAttribute('data-key');
|
|
if (key && vendor !== '') {
|
|
rows.forEach(function (other) {
|
|
var otherVendor = part(other, 'vendor');
|
|
if (other === row || other.getAttribute('data-key') !== key || other.dataset.touched || !otherVendor || (otherVendor.value !== '' && otherVendor.value !== 'new')) {
|
|
return;
|
|
}
|
|
otherVendor.value = vendor;
|
|
part(other, 'new-vendor').value = part(row, 'new-vendor').value;
|
|
if (!part(other, 'category').dataset.touched) {
|
|
part(other, 'category').value = category.value;
|
|
}
|
|
});
|
|
}
|
|
} else if (row && e.target.matches('[data-import-category], [data-import-client]')) {
|
|
e.target.dataset.touched = '1';
|
|
}
|
|
if (e.target === bulkCategory || e.target === bulkClient) {
|
|
var name = e.target === bulkCategory ? 'category' : 'client';
|
|
if (e.target.value !== '') {
|
|
rows.forEach(function (r) {
|
|
if (part(r, 'take').checked && part(r, name)) {
|
|
part(r, name).value = e.target.value;
|
|
part(r, name).dataset.touched = '1';
|
|
}
|
|
});
|
|
}
|
|
e.target.value = '';
|
|
}
|
|
update();
|
|
});
|
|
|
|
// A new vendor's name typed on one line follows to the same merchant's lines
|
|
form.addEventListener('input', function (e) {
|
|
if (!e.target.matches('[data-import-new-vendor]')) {
|
|
return;
|
|
}
|
|
var row = e.target.closest('[data-import-row]');
|
|
var key = row.getAttribute('data-key');
|
|
rows.forEach(function (other) {
|
|
if (other !== row && key && other.getAttribute('data-key') === key && part(other, 'vendor') && part(other, 'vendor').value === 'new' && !other.dataset.touched) {
|
|
part(other, 'new-vendor').value = e.target.value;
|
|
}
|
|
});
|
|
});
|
|
|
|
if (all) {
|
|
all.addEventListener('change', function () {
|
|
rows.forEach(function (row) {
|
|
var take = part(row, 'take');
|
|
if (!row.hidden && !take.disabled) {
|
|
take.checked = all.checked;
|
|
}
|
|
});
|
|
update();
|
|
});
|
|
}
|
|
if (showAll) {
|
|
showAll.addEventListener('change', function () {
|
|
rows.forEach(function (row) {
|
|
if (row.hasAttribute('data-import-extra')) {
|
|
row.hidden = !showAll.checked;
|
|
if (row.hidden) {
|
|
part(row, 'take').checked = false;
|
|
}
|
|
}
|
|
});
|
|
update();
|
|
});
|
|
}
|
|
|
|
// Clicking a line (not its picks) ticks it
|
|
rows.forEach(function (row) {
|
|
row.addEventListener('click', function (e) {
|
|
if (e.target.closest('a, input, button, select, label, textarea')) {
|
|
return;
|
|
}
|
|
var take = part(row, 'take');
|
|
if (!take.disabled) {
|
|
take.checked = !take.checked;
|
|
update();
|
|
}
|
|
});
|
|
});
|
|
|
|
form.addEventListener('submit', function (e) {
|
|
if (!form.reportValidity()) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
var list = rows.map(function (row) {
|
|
var vendor = part(row, 'vendor');
|
|
return {
|
|
n: Number(row.getAttribute('data-n')),
|
|
take: part(row, 'take').checked && !part(row, 'take').disabled,
|
|
vendor: vendor ? vendor.value : '',
|
|
new_vendor: vendor ? part(row, 'new-vendor').value : '',
|
|
category: part(row, 'category') ? part(row, 'category').value : '',
|
|
client: part(row, 'client') ? part(row, 'client').value : '0',
|
|
billable: part(row, 'billable') ? part(row, 'billable').checked : false
|
|
};
|
|
});
|
|
picks.value = JSON.stringify(list);
|
|
});
|
|
|
|
update();
|
|
}
|