invoicing: admin and public views
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).
This commit is contained in:
parent
77724e1ee2
commit
226ccb8192
5 changed files with 224 additions and 0 deletions
28
public/admin-invoice-form.js
Normal file
28
public/admin-invoice-form.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
(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();
|
||||
});
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue