mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 07:01:16 +00:00
220 lines
8.0 KiB
JavaScript
220 lines
8.0 KiB
JavaScript
/*
|
|
* Reconcile (agent/reconcile.php): tick the lines the statement shows. Each
|
|
* tick is saved as it is made (agent/ajax.php's reconcile_tick); the
|
|
* cleared balance and the difference follow, and Finish opens once the
|
|
* difference is nothing. The pills and the search box only change what is
|
|
* shown - Tick all shown and Untick all shown act on what they leave.
|
|
*
|
|
* itflowReconcile({ id: 12, csrf: '...', starting: 1000.00, ending: 1250.00,
|
|
* card: false, currency: 'USD', locale: 'en-US' });
|
|
*
|
|
* Money is counted in cents, so a long statement adds up exactly.
|
|
*/
|
|
function itflowReconcile(opts) {
|
|
var root = document.getElementById('reconcile');
|
|
if (!root || root.dataset.wired) {
|
|
return;
|
|
}
|
|
root.dataset.wired = '1';
|
|
|
|
var rows = Array.prototype.slice.call(root.querySelectorAll('tr[data-reconcile-line]'));
|
|
var format = new Intl.NumberFormat(opts.locale || undefined, { style: 'currency', currency: opts.currency || 'USD' });
|
|
var money = function (cents) {
|
|
return format.format(cents / 100);
|
|
};
|
|
// A card's balance below zero is what is owed on it
|
|
var balance = function (cents) {
|
|
return opts.card && cents < 0 ? money(-cents) + ' owed' : money(cents);
|
|
};
|
|
var toCents = function (value) {
|
|
return Math.round(parseFloat(value || 0) * 100);
|
|
};
|
|
var starting = toCents(opts.starting);
|
|
var ending = toCents(opts.ending);
|
|
var show = '';
|
|
var query = '';
|
|
var sent = 0;
|
|
var pending = 0;
|
|
|
|
var finishes = document.querySelectorAll('[data-reconcile-finish]');
|
|
var finishForm = document.getElementById('reconcileFinish');
|
|
var cleared = document.querySelector('[data-reconcile-cleared]');
|
|
var ticked = document.querySelector('[data-reconcile-ticked]');
|
|
var differences = document.querySelectorAll('[data-reconcile-difference]');
|
|
var differenceCard = document.querySelector('[data-reconcile-difference-card]');
|
|
var differenceNote = document.querySelector('[data-reconcile-difference-note]');
|
|
|
|
function box(row) {
|
|
return row.querySelector('[data-reconcile-tick]');
|
|
}
|
|
|
|
function matches(row, key) {
|
|
var amount = toCents(row.dataset.amount);
|
|
var on = box(row).checked;
|
|
return key === '' || (key === 'in' && amount > 0) || (key === 'out' && amount < 0)
|
|
|| (key === 'ticked' && on) || (key === 'unticked' && !on);
|
|
}
|
|
|
|
function searched(row) {
|
|
return query === '' || (row.dataset.search || '').indexOf(query) !== -1;
|
|
}
|
|
|
|
// What shows, the pill counts, and where the statement stands
|
|
function redraw() {
|
|
var counts = { '': 0, 'in': 0, 'out': 0, 'ticked': 0, 'unticked': 0 };
|
|
var inCount = 0;
|
|
var inCents = 0;
|
|
var outCount = 0;
|
|
var outCents = 0;
|
|
rows.forEach(function (row) {
|
|
var on = box(row).checked;
|
|
var amount = toCents(row.dataset.amount);
|
|
row.classList.toggle('itflow-reconcile-ticked', on);
|
|
if (on && amount > 0) {
|
|
inCount++;
|
|
inCents += amount;
|
|
} else if (on && amount < 0) {
|
|
outCount++;
|
|
outCents -= amount;
|
|
}
|
|
if (searched(row)) {
|
|
Object.keys(counts).forEach(function (key) {
|
|
if (matches(row, key)) {
|
|
counts[key]++;
|
|
}
|
|
});
|
|
}
|
|
row.hidden = !(searched(row) && matches(row, show));
|
|
});
|
|
Object.keys(counts).forEach(function (key) {
|
|
var badge = root.querySelector('[data-reconcile-count="' + key + '"]');
|
|
if (badge) {
|
|
badge.textContent = counts[key];
|
|
}
|
|
});
|
|
|
|
var clearedCents = starting + inCents - outCents;
|
|
var differenceCents = ending - clearedCents;
|
|
if (cleared) {
|
|
cleared.textContent = balance(clearedCents);
|
|
}
|
|
if (ticked) {
|
|
ticked.textContent = (inCount + outCount) + ' ticked - ' + money(inCents) + ' in, ' + money(outCents) + ' out';
|
|
}
|
|
differences.forEach(function (difference) {
|
|
difference.textContent = money(differenceCents);
|
|
difference.classList.toggle('text-success-emphasis', differenceCents === 0);
|
|
difference.classList.toggle('text-danger-emphasis', differenceCents !== 0);
|
|
});
|
|
if (differenceCard) {
|
|
differenceCard.classList.toggle('itflow-reconcile-balanced', differenceCents === 0);
|
|
}
|
|
if (differenceNote) {
|
|
differenceNote.textContent = differenceCents === 0
|
|
? 'It balances - Finish it'
|
|
: (differenceCents > 0 ? 'What is ticked comes to less than the statement' : 'What is ticked comes to more than the statement');
|
|
}
|
|
finishes.forEach(function (finish) {
|
|
finish.disabled = differenceCents !== 0 || pending > 0;
|
|
});
|
|
}
|
|
|
|
// Save ticks; on a refusal put them back as they were
|
|
function save(changed, tick) {
|
|
if (!changed.length) {
|
|
return;
|
|
}
|
|
var ticket = ++sent;
|
|
pending++;
|
|
redraw();
|
|
var undo = function (message) {
|
|
changed.forEach(function (row) {
|
|
box(row).checked = !tick;
|
|
});
|
|
pending--;
|
|
redraw();
|
|
itflowToast(message || 'That tick was not saved - try again.', 'error');
|
|
};
|
|
itflowPost('ajax.php', {
|
|
reconcile_tick: 1,
|
|
csrf_token: opts.csrf,
|
|
reconciliation_id: opts.id,
|
|
tick: tick ? 1 : 0,
|
|
lines: changed.map(function (row) {
|
|
return row.dataset.reconcileLine;
|
|
})
|
|
}, function (text) {
|
|
var result;
|
|
try {
|
|
result = JSON.parse(text);
|
|
} catch (e) {
|
|
result = null;
|
|
}
|
|
if (!result || result.error || !result.totals) {
|
|
undo(result && result.error);
|
|
return;
|
|
}
|
|
pending--;
|
|
// The latest answer is the one to trust for where it stands
|
|
if (ticket === sent && pending === 0) {
|
|
starting = toCents(result.totals.starting);
|
|
ending = toCents(result.totals.ending);
|
|
}
|
|
redraw();
|
|
}, function () {
|
|
undo();
|
|
});
|
|
}
|
|
|
|
root.addEventListener('change', function (e) {
|
|
var input = e.target.closest('[data-reconcile-tick]');
|
|
if (!input) {
|
|
return;
|
|
}
|
|
save([input.closest('tr')], input.checked);
|
|
});
|
|
|
|
root.querySelectorAll('[data-reconcile-all]').forEach(function (button) {
|
|
button.addEventListener('click', function () {
|
|
var tick = button.dataset.reconcileAll === '1';
|
|
var changed = rows.filter(function (row) {
|
|
return !row.hidden && box(row).checked !== tick && !box(row).disabled;
|
|
});
|
|
changed.forEach(function (row) {
|
|
box(row).checked = tick;
|
|
});
|
|
save(changed, tick);
|
|
});
|
|
});
|
|
|
|
root.querySelectorAll('[data-reconcile-show]').forEach(function (pill) {
|
|
pill.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
show = pill.dataset.reconcileShow;
|
|
root.querySelectorAll('[data-reconcile-show]').forEach(function (other) {
|
|
other.classList.toggle('active', other === pill);
|
|
});
|
|
redraw();
|
|
});
|
|
});
|
|
|
|
var search = root.querySelector('[data-reconcile-search]');
|
|
if (search) {
|
|
search.addEventListener('input', function () {
|
|
query = search.value.trim().toLowerCase().replace(/\$/g, '');
|
|
redraw();
|
|
});
|
|
}
|
|
|
|
// Finishing with a tick still being saved would finish on the old ones
|
|
if (finishForm) {
|
|
finishForm.addEventListener('submit', function (e) {
|
|
if (pending > 0) {
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
}
|
|
|
|
redraw();
|
|
}
|