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({}); } });