Three admin templates: - index: invoice list with status badge and link to detail. - new: client + terms fields, dynamic line-item rows backed by admin-invoice-form.js (add/remove + re-index of items[N][...] names). - show: invoice detail, public URL to share, toggle-paid button. One public template, invoice_public.erb, shown at /i/:uuid: client name, number, total, status, and a single big download button to /i/:uuid/pdf. All admin pages are noindex; same for the public landing (it's a client-specific URL, not search-discoverable content).
28 lines
1,017 B
JavaScript
28 lines
1,017 B
JavaScript
(function () {
|
|
const tbody = document.querySelector('#items-table tbody');
|
|
const addBtn = document.getElementById('add-row');
|
|
|
|
function reindex() {
|
|
Array.from(tbody.querySelectorAll('tr.item-row')).forEach((row, i) => {
|
|
row.querySelectorAll('input').forEach((input) => {
|
|
input.name = input.name.replace(/items\[\d+\]/, 'items[' + i + ']');
|
|
});
|
|
});
|
|
}
|
|
|
|
addBtn.addEventListener('click', function () {
|
|
const last = tbody.querySelector('tr.item-row:last-child');
|
|
const clone = last.cloneNode(true);
|
|
clone.querySelectorAll('input').forEach((input) => { input.value = ''; });
|
|
tbody.appendChild(clone);
|
|
reindex();
|
|
});
|
|
|
|
tbody.addEventListener('click', function (e) {
|
|
if (!e.target.matches('.remove-row')) return;
|
|
const rows = tbody.querySelectorAll('tr.item-row');
|
|
if (rows.length <= 1) return;
|
|
e.target.closest('tr.item-row').remove();
|
|
reindex();
|
|
});
|
|
})();
|