Обновить invoice/invoice.js
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
function ready(fn) {
|
||||
if (document.readyState !== 'loading'){
|
||||
fn();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', fn);
|
||||
}
|
||||
}
|
||||
|
||||
function addDemo(row) {
|
||||
if (!row.Issued && !row.Due) {
|
||||
for (const key of ['Number', 'Issued', 'Due']) {
|
||||
if (!row[key]) { row[key] = key; }
|
||||
}
|
||||
for (const key of ['Subtotal', 'Deduction', 'Taxes', 'Total']) {
|
||||
if (!(key in row)) { row[key] = key; }
|
||||
}
|
||||
if (!('Note' in row)) { row.Note = '(Anything in a Note column goes here)'; }
|
||||
}
|
||||
if (!row.Invoicer) {
|
||||
row.Invoicer = {
|
||||
Name: 'Invoicer.Name',
|
||||
Street1: 'Invoicer.Street1',
|
||||
Street2: 'Invoicer.Street2',
|
||||
City: 'Invoicer.City',
|
||||
State: '.State',
|
||||
Zip: '.Zip',
|
||||
Email: 'Invoicer.Email',
|
||||
Phone: 'Invoicer.Phone',
|
||||
Website: 'Invoicer.Website'
|
||||
}
|
||||
}
|
||||
if (!row.Client) {
|
||||
row.Client = {
|
||||
Name: 'Client.Name',
|
||||
Street1: 'Client.Street1',
|
||||
Street2: 'Client.Street2',
|
||||
City: 'Client.City',
|
||||
State: '.State',
|
||||
Zip: '.Zip'
|
||||
}
|
||||
}
|
||||
if (!row.Items) {
|
||||
row.Items = [
|
||||
{
|
||||
Description: 'Items[0].Description',
|
||||
Quantity: '.Quantity',
|
||||
Total: '.Total',
|
||||
Price: '.Price',
|
||||
},
|
||||
{
|
||||
Description: 'Items[1].Description',
|
||||
Quantity: '.Quantity',
|
||||
Total: '.Total',
|
||||
Price: '.Price',
|
||||
},
|
||||
];
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
const data = {
|
||||
count: 0,
|
||||
invoice: '',
|
||||
status: 'waiting',
|
||||
tableConnected: false,
|
||||
rowConnected: false,
|
||||
haveRows: false,
|
||||
};
|
||||
let app = undefined;
|
||||
|
||||
Vue.filter('currency', formatNumberAsUSD)
|
||||
function formatNumberAsUSD(value) {
|
||||
if (typeof value !== "number") {
|
||||
return value || '—'; // falsy value would be shown as a dash.
|
||||
}
|
||||
value = Math.round(value * 100) / 100; // Round to nearest cent.
|
||||
value = (value === -0 ? 0 : value); // Avoid negative zero.
|
||||
|
||||
const result = value.toLocaleString('en', {
|
||||
style: 'currency', currency: 'USD'
|
||||
})
|
||||
if (result.includes('NaN')) {
|
||||
return value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Vue.filter('fallback', function(value, str) {
|
||||
if (!value) {
|
||||
throw new Error("Please provide column " + str);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
Vue.filter('asDate', function(value) {
|
||||
if (typeof(value) === 'number') {
|
||||
value = new Date(value * 1000);
|
||||
}
|
||||
const date = moment.utc(value)
|
||||
return date.isValid() ? date.format('MMMM DD, YYYY') : value;
|
||||
});
|
||||
|
||||
function tweakUrl(url) {
|
||||
if (!url) { return url; }
|
||||
if (url.toLowerCase().startsWith('http')) {
|
||||
return url;
|
||||
}
|
||||
return 'https://' + url;
|
||||
};
|
||||
|
||||
function handleError(err) {
|
||||
console.error(err);
|
||||
const target = app || data;
|
||||
target.invoice = '';
|
||||
target.status = String(err).replace(/^Error: /, '');
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
function prepareList(lst, order) {
|
||||
if (order) {
|
||||
let orderedLst = [];
|
||||
const remaining = new Set(lst);
|
||||
for (const key of order) {
|
||||
if (remaining.has(key)) {
|
||||
remaining.delete(key);
|
||||
orderedLst.push(key);
|
||||
}
|
||||
}
|
||||
lst = [...orderedLst].concat([...remaining].sort());
|
||||
} else {
|
||||
lst = [...lst].sort();
|
||||
}
|
||||
return lst;
|
||||
}
|
||||
|
||||
function updateInvoice(row) {
|
||||
try {
|
||||
data.status = '';
|
||||
if (row === null) {
|
||||
throw new Error("(No data - not on row - please add or select a row)");
|
||||
}
|
||||
console.log("GOT...", JSON.stringify(row));
|
||||
if (row.References) {
|
||||
try {
|
||||
Object.assign(row, row.References);
|
||||
} catch (err) {
|
||||
throw new Error('Could not understand References column. ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
// Add some guidance about columns.
|
||||
const want = new Set(Object.keys(addDemo({})));
|
||||
const accepted = new Set(['References']);
|
||||
const importance = ['Number', 'Client', 'Items', 'Total', 'Invoicer', 'Due', 'Issued', 'Subtotal', 'Deduction', 'Taxes', 'Note'];
|
||||
if (!(row.Due || row.Issued)) {
|
||||
const seen = new Set(Object.keys(row).filter(k => k !== 'id' && k !== '_error_'));
|
||||
const help = row.Help = {};
|
||||
help.seen = prepareList(seen);
|
||||
const missing = [...want].filter(k => !seen.has(k));
|
||||
const ignoring = [...seen].filter(k => !want.has(k) && !accepted.has(k));
|
||||
const recognized = [...seen].filter(k => want.has(k) || accepted.has(k));
|
||||
if (missing.length > 0) {
|
||||
help.expected = prepareList(missing, importance);
|
||||
}
|
||||
if (ignoring.length > 0) {
|
||||
help.ignored = prepareList(ignoring);
|
||||
}
|
||||
if (recognized.length > 0) {
|
||||
help.recognized = prepareList(recognized);
|
||||
}
|
||||
if (!seen.has('References') && !(row.Issued || row.Due)) {
|
||||
row.SuggestReferencesColumn = true;
|
||||
}
|
||||
}
|
||||
addDemo(row);
|
||||
if (!row.Subtotal && !row.Total && row.Items && Array.isArray(row.Items)) {
|
||||
try {
|
||||
row.Subtotal = row.Items.reduce((a, b) => a + b.Price * b.Quantity, 0);
|
||||
row.Total = row.Subtotal + (row.Taxes || 0) - (row.Deduction || 0);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
if (row.Invoicer && row.Invoicer.Website && !row.Invoicer.Url) {
|
||||
row.Invoicer.Url = tweakUrl(row.Invoicer.Website);
|
||||
}
|
||||
|
||||
// Fiddle around with updating Vue (I'm not an expert).
|
||||
for (const key of want) {
|
||||
Vue.delete(data.invoice, key);
|
||||
}
|
||||
for (const key of ['Help', 'SuggestReferencesColumn', 'References']) {
|
||||
Vue.delete(data.invoice, key);
|
||||
}
|
||||
data.invoice = Object.assign({}, data.invoice, row);
|
||||
|
||||
// Make invoice information available for debugging.
|
||||
window.invoice = row;
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
ready(function() {
|
||||
// Update the invoice anytime the document data changes.
|
||||
grist.ready();
|
||||
grist.onRecord(updateInvoice);
|
||||
|
||||
// Monitor status so we can give user advice.
|
||||
grist.on('message', msg => {
|
||||
// If we are told about a table but not which row to access, check the
|
||||
// number of rows. Currently if the table is empty, and "select by" is
|
||||
// not set, onRecord() will never be called.
|
||||
if (msg.tableId && !app.rowConnected) {
|
||||
grist.docApi.fetchSelectedTable().then(table => {
|
||||
if (table.id && table.id.length >= 1) {
|
||||
app.haveRows = true;
|
||||
}
|
||||
}).catch(e => console.log(e));
|
||||
}
|
||||
if (msg.tableId) { app.tableConnected = true; }
|
||||
if (msg.tableId && !msg.dataChange) { app.RowConnected = true; }
|
||||
});
|
||||
|
||||
Vue.config.errorHandler = function (err, vm, info) {
|
||||
handleError(err);
|
||||
};
|
||||
|
||||
app = new Vue({
|
||||
el: '#app',
|
||||
data: data
|
||||
});
|
||||
|
||||
if (document.location.search.includes('demo')) {
|
||||
updateInvoice(exampleData);
|
||||
}
|
||||
if (document.location.search.includes('labels')) {
|
||||
updateInvoice({});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user