domma-cms 0.54.2 → 0.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/CLAUDE.md +151 -77
  2. package/README.md +4 -4
  3. package/admin/css/admin.css +1 -1
  4. package/admin/dist/domma/domma-tools.css +3 -3
  5. package/admin/dist/domma/domma-tools.min.js +3 -3
  6. package/admin/js/api.js +1 -1
  7. package/admin/js/app.js +3 -3
  8. package/admin/js/lib/page-picker.js +1 -0
  9. package/admin/js/lib/plugin-accent.js +1 -0
  10. package/admin/js/lib/plugin-chrome.js +1 -0
  11. package/admin/js/lib/shortcode-context-menu.js +2 -2
  12. package/admin/js/lib/sidebar-grouping.js +1 -1
  13. package/admin/js/lib/sidebar-grouping.test.js +1 -1
  14. package/admin/js/lib/sidebar-renderer.js +4 -4
  15. package/admin/js/lib/slideover-resizable.js +1 -0
  16. package/admin/js/templates/context-menu-editor.html +212 -0
  17. package/admin/js/templates/context-menus.html +16 -0
  18. package/admin/js/templates/menu-editor.html +19 -25
  19. package/admin/js/templates/plugin-code.html +1 -1
  20. package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
  21. package/admin/js/templates/settings.html +27 -97
  22. package/admin/js/templates/theme.html +173 -0
  23. package/admin/js/views/context-menu-editor.js +55 -0
  24. package/admin/js/views/context-menus.js +5 -0
  25. package/admin/js/views/form-editor.js +7 -7
  26. package/admin/js/views/index.js +1 -1
  27. package/admin/js/views/menu-editor.js +13 -13
  28. package/admin/js/views/plugin-marketplace.js +1 -1
  29. package/admin/js/views/plugins.js +25 -23
  30. package/admin/js/views/search.js +1 -0
  31. package/admin/js/views/settings.js +3 -3
  32. package/admin/js/views/theme.js +1 -0
  33. package/bin/cli.js +11 -2
  34. package/bin/lib/smtp-defaults.js +53 -0
  35. package/bin/update.js +13 -2
  36. package/config/menus/admin-sidebar.json +129 -23
  37. package/config/plugins.json +5 -5
  38. package/config/search.json +13 -0
  39. package/config/theme.json +18 -0
  40. package/package.json +12 -4
  41. package/plugins/_lib/admin/mail/compose-window.js +914 -0
  42. package/plugins/_lib/admin/mail/contacts.js +301 -0
  43. package/plugins/_lib/admin/mail/diagnostics-section.js +133 -0
  44. package/plugins/_lib/admin/mail/folder-tree.js +254 -0
  45. package/plugins/_lib/admin/mail/identity.js +480 -0
  46. package/plugins/_lib/admin/mail/image-senders-section.js +131 -0
  47. package/plugins/_lib/admin/mail/keyboard.js +136 -0
  48. package/plugins/_lib/admin/mail/mail.css +1 -0
  49. package/plugins/_lib/admin/mail/mail.html +71 -0
  50. package/plugins/_lib/admin/mail/panes.js +253 -0
  51. package/plugins/_lib/admin/mail/reader-view.js +4453 -0
  52. package/plugins/_lib/admin/mail/resizable.js +26 -0
  53. package/plugins/_lib/admin/mail/rules.js +343 -0
  54. package/plugins/_lib/admin/mail/scheduling.js +203 -0
  55. package/plugins/_lib/admin/mail/section-kit.js +277 -0
  56. package/plugins/_lib/admin/mail/templates.js +238 -0
  57. package/plugins/_lib/admin/mail/threads.js +200 -0
  58. package/plugins/_lib/admin/mail/vacation.js +269 -0
  59. package/plugins/_lib/admin/ui/help.css +1 -0
  60. package/plugins/_lib/admin/ui/help.js +174 -0
  61. package/plugins/_lib/admin/ui/resizable.js +151 -0
  62. package/plugins/_lib/dataStore.js +117 -0
  63. package/plugins/_lib/mail/accounts.js +919 -0
  64. package/plugins/_lib/mail/bodyTokens.js +101 -0
  65. package/plugins/_lib/mail/compose.js +256 -0
  66. package/plugins/_lib/mail/defaults.js +57 -0
  67. package/plugins/_lib/mail/diagnostics.js +154 -0
  68. package/plugins/_lib/mail/envelope.js +161 -0
  69. package/plugins/_lib/mail/folders.js +192 -0
  70. package/plugins/_lib/mail/handoff.js +274 -0
  71. package/plugins/_lib/mail/imapPool.js +293 -0
  72. package/plugins/_lib/mail/mbox.js +74 -0
  73. package/plugins/_lib/mail/pollSchedule.js +79 -0
  74. package/plugins/_lib/mail/poller.js +135 -0
  75. package/plugins/_lib/mail/priority.js +138 -0
  76. package/plugins/_lib/mail/readRoutes.js +680 -0
  77. package/plugins/_lib/mail/render.js +291 -0
  78. package/plugins/_lib/mail/ruleRunner.js +152 -0
  79. package/plugins/_lib/mail/scheduler.js +254 -0
  80. package/plugins/_lib/mail/secretbox.js +229 -0
  81. package/plugins/_lib/mail/send.js +396 -0
  82. package/plugins/_lib/mail/store.js +1002 -0
  83. package/plugins/_lib/mail/sync.js +292 -0
  84. package/plugins/_lib/mail/syncPlan.js +126 -0
  85. package/plugins/_lib/mail/syncSelection.js +82 -0
  86. package/plugins/_lib/mail/unsubscribe.js +183 -0
  87. package/plugins/_lib/mail/vacationRunner.js +114 -0
  88. package/plugins/_lib/mail/write.js +473 -0
  89. package/plugins/_lib/schemaSync.js +83 -0
  90. package/plugins/_template/admin/css/index.css +0 -0
  91. package/plugins/_template/admin/templates/index.html +4 -4
  92. package/plugins/_template/admin/views/index.js +7 -0
  93. package/plugins/analytics/admin/css/index.css +1 -0
  94. package/plugins/analytics/admin/templates/analytics.html +22 -13
  95. package/plugins/analytics/plugin.json +3 -0
  96. package/plugins/blog/admin/css/index.css +1 -0
  97. package/plugins/blog/admin/templates/blog.html +30 -18
  98. package/plugins/blog/admin/templates/categories.html +2 -2
  99. package/plugins/blog/admin/templates/comments.html +2 -2
  100. package/plugins/blog/admin/templates/post-editor.html +34 -34
  101. package/plugins/blog/admin/templates/settings.html +6 -3
  102. package/plugins/blog/admin/views/blog.js +8 -5
  103. package/plugins/blog/admin/views/categories.js +5 -10
  104. package/plugins/blog/admin/views/comments.js +5 -5
  105. package/plugins/blog/admin/views/post-editor.js +39 -20
  106. package/plugins/blog/admin/views/settings.js +52 -50
  107. package/plugins/blog/collections/categories/schema.json +7 -6
  108. package/plugins/blog/collections/comments/schema.json +11 -10
  109. package/plugins/blog/collections/posts/schema.json +14 -13
  110. package/plugins/blog/plugin.js +36 -13
  111. package/plugins/blog/plugin.json +13 -5
  112. package/plugins/blog/plugin.public.js +312 -0
  113. package/plugins/contacts/admin/css/index.css +1 -0
  114. package/plugins/contacts/admin/templates/contacts.html +128 -0
  115. package/plugins/contacts/admin/views/contacts.js +237 -4
  116. package/plugins/contacts/collections/user-contacts/schema.json +108 -0
  117. package/plugins/contacts/plugin.js +214 -27
  118. package/plugins/contacts/plugin.json +4 -1
  119. package/plugins/invoice/admin/css/index.css +1 -0
  120. package/plugins/invoice/admin/templates/editor.html +140 -49
  121. package/plugins/invoice/admin/templates/index.html +153 -23
  122. package/plugins/invoice/admin/templates/issuers.html +2 -5
  123. package/plugins/invoice/admin/templates/receivers.html +2 -5
  124. package/plugins/invoice/admin/views/contacts-source.js +266 -0
  125. package/plugins/invoice/admin/views/editor.js +366 -199
  126. package/plugins/invoice/admin/views/export.js +199 -0
  127. package/plugins/invoice/admin/views/help-content.js +61 -0
  128. package/plugins/invoice/admin/views/index.js +582 -94
  129. package/plugins/invoice/admin/views/issuers.js +24 -17
  130. package/plugins/invoice/admin/views/media.js +172 -0
  131. package/plugins/invoice/admin/views/party-view.js +305 -67
  132. package/plugins/invoice/admin/views/payments.js +127 -0
  133. package/plugins/invoice/admin/views/print.js +130 -0
  134. package/plugins/invoice/admin/views/receivers.js +49 -16
  135. package/plugins/invoice/admin/views/send.js +212 -0
  136. package/plugins/invoice/admin/views/settings.js +594 -0
  137. package/plugins/invoice/admin/views/view-lifecycle.js +33 -0
  138. package/plugins/invoice/collections/invoice-issuers/schema.json +77 -11
  139. package/plugins/invoice/collections/invoice-receivers/schema.json +10 -9
  140. package/plugins/invoice/collections/invoices/schema.json +19 -13
  141. package/plugins/invoice/config.js +27 -6
  142. package/plugins/invoice/pdf.js +164 -0
  143. package/plugins/invoice/plugin.js +1217 -44
  144. package/plugins/invoice/plugin.json +10 -9
  145. package/plugins/invoice/templates/_base.css +1 -0
  146. package/plugins/invoice/templates/classic-nologo.html +100 -0
  147. package/plugins/invoice/templates/classic.html +91 -0
  148. package/plugins/invoice/templates/invoice-print.html +24 -0
  149. package/plugins/invoice/templates/minimal.html +99 -0
  150. package/plugins/invoice/templates/modern-nologo.html +114 -0
  151. package/plugins/invoice/templates/modern.html +113 -0
  152. package/plugins/invoice/templates/templates.json +11 -0
  153. package/plugins/mail-reader/admin/views/mail.js +19 -0
  154. package/plugins/mail-reader/config.js +7 -0
  155. package/plugins/mail-reader/plugin.js +48 -0
  156. package/plugins/mail-reader/plugin.json +33 -0
  157. package/plugins/notes/admin/views/notes.js +1 -1
  158. package/plugins/notes/plugin.json +2 -2
  159. package/plugins/surveys/lib/audience.js +37 -0
  160. package/plugins/surveys/lib/campaigns.js +43 -0
  161. package/plugins/surveys/lib/ledger.js +110 -0
  162. package/plugins/surveys/lib/sending.js +106 -0
  163. package/plugins/surveys/lib/stats.js +62 -0
  164. package/plugins/surveys/lib/submit.js +95 -0
  165. package/plugins/surveys/lib/tokens.js +28 -0
  166. package/plugins/surveys/plugin.public.js +149 -0
  167. package/plugins/theme-switcher/admin/templates/theme-switcher.html +1 -1
  168. package/public/css/forms.css +1 -1
  169. package/public/css/menu-highlight.css +1 -1
  170. package/public/css/search.css +1 -0
  171. package/public/css/site.css +1 -1
  172. package/public/js/collection-context.js +2 -2
  173. package/public/js/context-menus.js +1 -0
  174. package/public/js/form-logic-engine.js +1 -1
  175. package/public/js/forms.js +2 -2
  176. package/public/js/menu-decor.mjs +1 -1
  177. package/public/js/search.js +1 -0
  178. package/public/js/site.js +1 -1
  179. package/scripts/build.js +37 -3
  180. package/scripts/copy-domma.js +48 -0
  181. package/scripts/seed.js +1996 -0
  182. package/scripts/setup.js +8 -0
  183. package/server/routes/api/collections.js +34 -0
  184. package/server/routes/api/context-menus.js +104 -0
  185. package/server/routes/api/forms.js +42 -3
  186. package/server/routes/api/notifications.js +69 -19
  187. package/server/routes/api/plugins.js +50 -6
  188. package/server/routes/api/search.js +43 -0
  189. package/server/routes/api/theme.js +69 -0
  190. package/server/routes/public.js +42 -7
  191. package/server/server.js +74 -0
  192. package/server/services/adapters/FileAdapter.js +6 -1
  193. package/server/services/content.js +26 -0
  194. package/server/services/contextMenus.js +477 -0
  195. package/server/services/email.js +29 -3
  196. package/server/services/health.js +23 -2
  197. package/server/services/markdown.js +70 -9
  198. package/server/services/menuRender.js +28 -4
  199. package/server/services/menus.js +25 -1
  200. package/server/services/permissionRegistry.js +24 -0
  201. package/server/services/pluginFiles.js +52 -11
  202. package/server/services/plugins.js +229 -6
  203. package/server/services/renderer.js +148 -22
  204. package/server/services/roles.js +1 -1
  205. package/server/services/search-migration.js +82 -0
  206. package/server/services/search.js +413 -0
  207. package/server/services/sidebar-migration.js +1 -0
  208. package/server/services/themeSettings.js +541 -0
  209. package/server/services/users.js +8 -0
  210. package/server/templates/page.html +4 -2
  211. package/plugins/contacts/data/contacts.json +0 -20
  212. package/plugins/notes/data/notes.json +0 -1
  213. package/plugins/site-search/admin/views/site-search.js +0 -116
  214. package/plugins/site-search/config.js +0 -15
  215. package/plugins/site-search/plugin.js +0 -188
  216. package/plugins/site-search/plugin.json +0 -40
  217. package/plugins/site-search/public/inject-body.html +0 -17
  218. package/plugins/site-search/public/inject-head.html +0 -1
  219. package/plugins/site-search/public/search.css +0 -1
  220. package/plugins/site-search/public/search.js +0 -1
  221. package/plugins/todo/data/todos.json +0 -1
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Invoice plugin - saving invoices as a file.
3
+ *
4
+ * Domma's table can export what it is showing, but it exports the RENDERED
5
+ * columns: a receiver id instead of a name, an HTML badge instead of a status,
6
+ * and no line items at all, because they are not in the table. An accountant
7
+ * asking for "the invoices as a spreadsheet" wants neither of those, so the
8
+ * rows are built here from the data and the table's own exporter is left off.
9
+ *
10
+ * Excel is an HTML table served as `application/vnd.ms-excel`, which is what
11
+ * Domma's own exporter does and what Excel and LibreOffice both open without
12
+ * a converter. It is not a real .xlsx, and it does not need to be - nothing
13
+ * here has formulas, merged cells or more than one sheet.
14
+ */
15
+ function escapeHtml(s) {
16
+ return String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
17
+ }
18
+
19
+ /**
20
+ * @param {Array<Array<string|number>>} rows
21
+ * @returns {string}
22
+ */
23
+ function toCsv(rows) {
24
+ return rows.map((row) => row.map((cell) => {
25
+ const text = cell === null || cell === undefined ? '' : String(cell);
26
+ return `"${text.replace(/"/g, '""')}"`;
27
+ }).join(',')).join('\r\n');
28
+ }
29
+
30
+ /**
31
+ * @param {string} title
32
+ * @param {Array<Array<string|number>>} rows
33
+ * @param {number} headerRows - how many leading rows to render as <th>
34
+ * @returns {string}
35
+ */
36
+ function toExcel(title, rows, headerRows = 1) {
37
+ const body = rows.map((row, index) => {
38
+ const tag = index < headerRows ? 'th' : 'td';
39
+ return '<tr>' + row.map((cell) => `<${tag}>${escapeHtml(cell)}</${tag}>`).join('') + '</tr>';
40
+ }).join('');
41
+
42
+ return `<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel">
43
+ <head><meta charset="UTF-8">
44
+ <!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet>
45
+ <x:Name>${escapeHtml(title).slice(0, 31)}</x:Name>
46
+ <x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions>
47
+ </x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]-->
48
+ <style>table{border-collapse:collapse}th,td{border:1px solid #999;padding:6px}th{background:#eee;font-weight:bold;text-align:left}</style>
49
+ </head><body><table>${body}</table></body></html>`;
50
+ }
51
+
52
+ /**
53
+ * @param {string} content
54
+ * @param {string} mime
55
+ * @param {string} filename
56
+ * @returns {void}
57
+ */
58
+ function save(content, mime, filename) {
59
+ // A BOM, because Excel reads a CSV without one as the system codepage and
60
+ // turns every pound sign into £.
61
+ const blob = new Blob([mime.startsWith('text/csv') ? '' + content : content], { type: mime });
62
+ const url = URL.createObjectURL(blob);
63
+ const a = document.createElement('a');
64
+ a.href = url;
65
+ a.download = filename;
66
+ a.rel = 'noopener';
67
+ document.body.appendChild(a);
68
+ a.click();
69
+ a.remove();
70
+ // Revoked on the next tick: revoking synchronously races the download in
71
+ // Firefox, which has not necessarily read the blob by the time click()
72
+ // returns.
73
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
74
+ }
75
+
76
+ /** A filename component that no filesystem will argue about. */
77
+ function safeName(text, fallback) {
78
+ const cleaned = String(text ?? '').replace(/[^\w.-]+/g, '_').replace(/^_+|_+$/g, '');
79
+ return cleaned || fallback;
80
+ }
81
+
82
+ function money(amount) {
83
+ return (Number(amount) || 0).toFixed(2);
84
+ }
85
+
86
+ function dateText(value) {
87
+ return value ? D(value).format('DD MMM YYYY') : '';
88
+ }
89
+
90
+ /**
91
+ * One invoice as a small worksheet: the facts, then the lines, then the totals.
92
+ *
93
+ * @param {object} invoice
94
+ * @param {{issuerName: string, receiverName: string}} names
95
+ * @returns {Array<Array<string|number>>}
96
+ */
97
+ export function invoiceRows(invoice, { issuerName = '', receiverName = '' } = {}) {
98
+ const currency = invoice.currency || 'GBP';
99
+ const totals = invoice.totals ?? {};
100
+ const rows = [
101
+ ['Invoice', invoice.number || ''],
102
+ ['Status', invoice.overdue ? 'overdue' : (invoice.status || 'draft')],
103
+ ['From', issuerName],
104
+ ['To', receiverName],
105
+ ['Issued', dateText(invoice.issueDate)],
106
+ ['Due', dateText(invoice.dueDate)],
107
+ ['Currency', currency],
108
+ [],
109
+ ['Description', 'Quantity', 'Unit price', 'Line total']
110
+ ];
111
+
112
+ for (const line of invoice.lineItems ?? []) {
113
+ const quantity = Number(line.quantity) || 0;
114
+ const unit = Number(line.unitPrice) || 0;
115
+ rows.push([line.description || '', quantity, money(unit), money(quantity * unit)]);
116
+ }
117
+
118
+ rows.push([]);
119
+ rows.push(['', '', 'Subtotal', money(totals.subtotal)]);
120
+ if (invoice.vatEnabled) rows.push(['', '', `VAT (${totals.vatRate ?? invoice.vatRate ?? 0}%)`, money(totals.vat)]);
121
+ rows.push(['', '', 'Total', money(totals.total)]);
122
+ if (Number(totals.paid) > 0) {
123
+ rows.push(['', '', 'Paid', money(totals.paid)]);
124
+ rows.push(['', '', 'Balance', money(Math.max(0, Number(totals.balance) || 0))]);
125
+ }
126
+
127
+ if (invoice.notes) {
128
+ rows.push([]);
129
+ rows.push(['Notes', invoice.notes]);
130
+ }
131
+ return rows;
132
+ }
133
+
134
+ /**
135
+ * A list of invoices, one per row.
136
+ *
137
+ * @param {object[]} invoices
138
+ * @param {(invoice: object) => {issuerName: string, receiverName: string}} names
139
+ * @returns {Array<Array<string|number>>}
140
+ */
141
+ export function listRows(invoices, names) {
142
+ const rows = [[
143
+ 'Number', 'Status', 'From', 'To', 'Issued', 'Due',
144
+ 'Currency', 'Subtotal', 'VAT', 'Total', 'Paid', 'Balance'
145
+ ]];
146
+
147
+ for (const invoice of invoices) {
148
+ const totals = invoice.totals ?? {};
149
+ const { issuerName = '', receiverName = '' } = names(invoice) ?? {};
150
+ rows.push([
151
+ invoice.number || '',
152
+ invoice.overdue ? 'overdue' : (invoice.status || 'draft'),
153
+ issuerName,
154
+ receiverName,
155
+ dateText(invoice.issueDate),
156
+ dateText(invoice.dueDate),
157
+ invoice.currency || 'GBP',
158
+ money(totals.subtotal),
159
+ money(totals.vat),
160
+ money(totals.total),
161
+ money(totals.paid),
162
+ money(totals.balance)
163
+ ]);
164
+ }
165
+ return rows;
166
+ }
167
+
168
+ /**
169
+ * @param {object} invoice
170
+ * @param {'csv'|'excel'} format
171
+ * @param {object} names
172
+ * @returns {void}
173
+ */
174
+ export function exportInvoice(invoice, format, names) {
175
+ const rows = invoiceRows(invoice, names);
176
+ const stem = safeName(invoice.number, 'invoice');
177
+ if (format === 'excel') {
178
+ save(toExcel(stem, rows, 0), 'application/vnd.ms-excel', `${stem}.xls`);
179
+ } else {
180
+ save(toCsv(rows), 'text/csv;charset=utf-8', `${stem}.csv`);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * @param {object[]} invoices
186
+ * @param {'csv'|'excel'} format
187
+ * @param {Function} names
188
+ * @param {string} [stem]
189
+ * @returns {void}
190
+ */
191
+ export function exportList(invoices, format, names, stem = 'invoices') {
192
+ const rows = listRows(invoices, names);
193
+ const filename = `${safeName(stem, 'invoices')}-${D().format('YYYY-MM-DD')}`;
194
+ if (format === 'excel') {
195
+ save(toExcel('Invoices', rows, 1), 'application/vnd.ms-excel', `${filename}.xls`);
196
+ } else {
197
+ save(toCsv(rows), 'text/csv;charset=utf-8', `${filename}.csv`);
198
+ }
199
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * What the Invoices plugin's Help panel says.
3
+ *
4
+ * Kept as data rather than markup so the same words can be shown in the panel,
5
+ * quoted in a tooltip, or lifted into the docs without being re-typed - and so
6
+ * that changing how help LOOKS never means editing what it SAYS.
7
+ *
8
+ * Written for someone who has not used the screen before, in the order they
9
+ * will meet it: make an issuer, make a receiver, make an invoice, send it, get
10
+ * paid, file it away.
11
+ */
12
+ export const INVOICE_HELP = {
13
+ title: 'Invoices - how it works',
14
+ intro: 'An invoice is raised BY an issuer (you) FOR a receiver (your customer). Both live in Settings, behind the cog.',
15
+ sections: [
16
+ {
17
+ title: 'Before your first invoice',
18
+ body: [
19
+ 'Add an issuer: your trading name, address, VAT number and the account you are paid into. Add one per business you invoice as; "Invoice as" picks which one a new invoice opens with.',
20
+ 'Add a receiver: who you are billing. If you use the Contacts plugin, Import from Contacts brings the company name, billing address, VAT number and reference across for you.',
21
+ 'Until there is at least one of each, New Invoice stays switched off - an invoice with nobody on either end cannot be saved.'
22
+ ]
23
+ },
24
+ {
25
+ title: 'Numbers, dates and VAT',
26
+ body: [
27
+ 'Numbers are allocated when an invoice is created, per prefix and per year, and are never reused. Typing your own number still wins.',
28
+ 'The due date is filled in from your payment terms, and an invoice past its due date reports itself overdue without anything having to run - the stored status stays whatever you set.',
29
+ 'VAT is one rate per invoice, applied to the total. Line item prices are net.'
30
+ ]
31
+ },
32
+ {
33
+ title: 'Sending and getting paid',
34
+ body: [
35
+ 'Send emails the invoice, attaching a PDF where the server can render one. Recipients can be typed, or picked from your contacts - including everybody in a group.',
36
+ 'Record a payment against an invoice and the balance follows; settle it in full and the status moves to paid on its own.'
37
+ ]
38
+ },
39
+ {
40
+ title: 'Filing',
41
+ body: [
42
+ 'Archive is a shelf, not a status: an archived invoice keeps whatever it was and simply stops appearing in the list. Show archived swaps the list over to the shelf.',
43
+ 'Delete is permanent. For anything you might need again - and an invoice you have sent is exactly that - archive it instead.'
44
+ ]
45
+ },
46
+ {
47
+ title: 'How it looks',
48
+ body: [
49
+ 'Settings > Templates chooses the layout, with a worked example in your own name. An issuer with no logo gets a layout built for having none rather than the same one with a gap in it.',
50
+ 'Settings > Branding holds the house logo, used for any issuer that has not got one of its own.'
51
+ ]
52
+ },
53
+ {
54
+ title: 'Getting it out',
55
+ body: [
56
+ 'Right-click any row (or use the ⋯ button) to print, send, download a PDF, export that one invoice as CSV or Excel, duplicate it, change its status, archive or delete it.',
57
+ 'The CSV and Excel buttons above the table export what the filters are currently showing, not everything.'
58
+ ]
59
+ }
60
+ ]
61
+ };