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
@@ -21,6 +21,69 @@ async function api(url, method = 'GET', body) {
21
21
  return text ? JSON.parse(text) : {};
22
22
  }
23
23
 
24
+ /**
25
+ * The billing fields, as a map of contact key to the input that edits it.
26
+ *
27
+ * One list rather than a load/save pair spelled out twice - the way a field
28
+ * gets added to the form and quietly never saved.
29
+ */
30
+ const BILLING_FIELDS = {
31
+ company: '#contact-company',
32
+ jobTitle: '#contact-job-title',
33
+ website: '#contact-website',
34
+ billingEmail: '#contact-billing-email',
35
+ vatNumber: '#contact-vat-number',
36
+ companyNumber: '#contact-company-number',
37
+ reference: '#contact-reference',
38
+ paymentTerms: '#contact-payment-terms',
39
+ currency: '#contact-currency',
40
+ accountName: '#contact-account-name',
41
+ bankName: '#contact-bank-name',
42
+ sortCode: '#contact-sort-code',
43
+ accountNumber: '#contact-account-number',
44
+ iban: '#contact-iban',
45
+ bic: '#contact-bic'
46
+ };
47
+
48
+ /**
49
+ * The repeatable lists, and the columns each row is made of.
50
+ *
51
+ * `wide` gets a full-width input; `check` is the single radio-ish flag that
52
+ * says which address an invoice should use.
53
+ */
54
+ const REPEATERS = {
55
+ addresses: {
56
+ listId: 'addresses-list',
57
+ emptyId: 'addresses-empty',
58
+ columns: [
59
+ {key: 'label', placeholder: 'Label (Office, Home…)'},
60
+ {key: 'line1', placeholder: 'Address line 1', wide: true},
61
+ {key: 'line2', placeholder: 'Address line 2', wide: true},
62
+ {key: 'city', placeholder: 'City'},
63
+ {key: 'region', placeholder: 'County / Region'},
64
+ {key: 'postcode', placeholder: 'Postcode'},
65
+ {key: 'country', placeholder: 'Country'},
66
+ {key: 'isBilling', label: 'Use for invoices', check: true}
67
+ ]
68
+ },
69
+ emails: {
70
+ listId: 'emails-list',
71
+ emptyId: 'emails-empty',
72
+ columns: [
73
+ {key: 'label', placeholder: 'Label (Accounts, Personal…)'},
74
+ {key: 'address', placeholder: 'name@example.com', wide: true}
75
+ ]
76
+ },
77
+ phones: {
78
+ listId: 'phones-list',
79
+ emptyId: 'phones-empty',
80
+ columns: [
81
+ {key: 'label', placeholder: 'Label (Mobile, Office…)'},
82
+ {key: 'number', placeholder: '+44 …', wide: true}
83
+ ]
84
+ }
85
+ };
86
+
24
87
  export const contactsView = {
25
88
  templateUrl: '/plugins/contacts/admin/templates/contacts.html',
26
89
 
@@ -239,6 +302,141 @@ export const contactsView = {
239
302
  // Editor
240
303
  // ---------------------------------------------------------------
241
304
 
305
+ /**
306
+ * Draw one repeatable list.
307
+ *
308
+ * Built with DOM APIs rather than innerHTML because these rows carry
309
+ * inputs, and Domma's .html() sanitises interactive elements away.
310
+ *
311
+ * @param {string} field addresses | emails | phones
312
+ * @param {object[]} rows
313
+ */
314
+ function renderRepeater(field, rows) {
315
+ const spec = REPEATERS[field];
316
+ const listEl = $container.find('#' + spec.listId).get(0);
317
+ const emptyEl = $container.find('#' + spec.emptyId).get(0);
318
+ if (!listEl) return;
319
+
320
+ while (listEl.firstChild) listEl.removeChild(listEl.firstChild);
321
+ if (emptyEl) emptyEl.style.display = rows.length ? 'none' : '';
322
+
323
+ rows.forEach((row, index) => {
324
+ const rowEl = document.createElement('div');
325
+ rowEl.className = 'contacts-row';
326
+ rowEl.dataset.index = String(index);
327
+
328
+ for (const col of spec.columns) {
329
+ if (col.check) {
330
+ const wrap = document.createElement('label');
331
+ wrap.className = 'form-check contacts-row-check';
332
+ const box = document.createElement('input');
333
+ box.type = 'checkbox';
334
+ box.className = 'form-check-input';
335
+ box.dataset.key = col.key;
336
+ box.checked = row[col.key] === true;
337
+ // Only one address can be the billing one.
338
+ box.addEventListener('change', () => {
339
+ if (!box.checked) return;
340
+ listEl.querySelectorAll(`input[data-key="${col.key}"]`).forEach((other) => {
341
+ if (other !== box) other.checked = false;
342
+ });
343
+ });
344
+ const text = document.createElement('span');
345
+ text.className = 'form-check-label';
346
+ text.textContent = col.label;
347
+ wrap.appendChild(box);
348
+ wrap.appendChild(text);
349
+ rowEl.appendChild(wrap);
350
+ continue;
351
+ }
352
+
353
+ const input = document.createElement('input');
354
+ input.type = 'text';
355
+ input.className = 'form-input' + (col.wide ? ' contacts-row-wide' : '');
356
+ input.dataset.key = col.key;
357
+ input.placeholder = col.placeholder ?? '';
358
+ input.value = row[col.key] ?? '';
359
+ rowEl.appendChild(input);
360
+ }
361
+
362
+ const remove = document.createElement('button');
363
+ remove.type = 'button';
364
+ remove.className = 'btn btn-sm btn-ghost contacts-row-remove';
365
+ remove.title = 'Remove';
366
+ remove.textContent = '\u00d7';
367
+ remove.addEventListener('click', () => {
368
+ const next = readRepeater(field);
369
+ next.splice(index, 1);
370
+ renderRepeater(field, next);
371
+ updateBillingSummary();
372
+ });
373
+ rowEl.appendChild(remove);
374
+
375
+ listEl.appendChild(rowEl);
376
+ });
377
+ }
378
+
379
+ /**
380
+ * Read a repeatable list back out of the DOM.
381
+ *
382
+ * @param {string} field
383
+ * @returns {object[]}
384
+ */
385
+ function readRepeater(field) {
386
+ const spec = REPEATERS[field];
387
+ const listEl = $container.find('#' + spec.listId).get(0);
388
+ if (!listEl) return [];
389
+
390
+ return [...listEl.querySelectorAll('.contacts-row')].map((rowEl) => {
391
+ const row = {};
392
+ for (const col of spec.columns) {
393
+ const el = rowEl.querySelector(`[data-key="${col.key}"]`);
394
+ row[col.key] = col.check ? el?.checked === true : (el?.value ?? '').trim();
395
+ }
396
+ return row;
397
+ }).filter((row) => Object.entries(row).some(([k, v]) => k !== 'label' && v !== '' && v !== false));
398
+ }
399
+
400
+ /** A one-line "what is in here" for the folded billing section. */
401
+ function updateBillingSummary() {
402
+ const el = $container.find('#billing-summary').get(0);
403
+ if (!el) return;
404
+ const bits = [];
405
+ const company = $container.find('#contact-company').get(0)?.value?.trim();
406
+ if (company) bits.push(company);
407
+ const addresses = readRepeater('addresses').length;
408
+ if (addresses) bits.push(`${addresses} address${addresses === 1 ? '' : 'es'}`);
409
+ const extras = readRepeater('emails').length + readRepeater('phones').length;
410
+ if (extras) bits.push(`${extras} more contact${extras === 1 ? '' : 's'}`);
411
+ if ($container.find('#contact-sort-code').get(0)?.value?.trim()
412
+ || $container.find('#contact-iban').get(0)?.value?.trim()) bits.push('bank details');
413
+ el.textContent = bits.length ? bits.join(' \u00b7 ') : '';
414
+ }
415
+
416
+ /** Fill the billing inputs from a contact, or clear them for a new one. */
417
+ function loadBilling(contact) {
418
+ for (const [key, sel] of Object.entries(BILLING_FIELDS)) {
419
+ const el = $container.find(sel).get(0);
420
+ if (!el) continue;
421
+ const value = contact?.[key];
422
+ el.value = value === null || value === undefined ? '' : String(value);
423
+ }
424
+ for (const field of Object.keys(REPEATERS)) {
425
+ renderRepeater(field, Array.isArray(contact?.[field]) ? contact[field] : []);
426
+ }
427
+ updateBillingSummary();
428
+ }
429
+
430
+ /** Read the billing inputs back into the body a save posts. */
431
+ function readBilling() {
432
+ const body = {};
433
+ for (const [key, sel] of Object.entries(BILLING_FIELDS)) {
434
+ body[key] = ($container.find(sel).get(0)?.value ?? '').trim();
435
+ }
436
+ for (const field of Object.keys(REPEATERS)) body[field] = readRepeater(field);
437
+ return body;
438
+ }
439
+
242
440
  function openEditor(id) {
243
441
  currentEditId = id ?? null;
244
442
  selectedGroups = [];
@@ -269,6 +467,7 @@ export const contactsView = {
269
467
  if (notesEl) notesEl.value = c.notes ?? '';
270
468
  if (favEl) favEl.checked = c.favourite === true;
271
469
  selectedGroups = Array.isArray(c.groups) ? [...c.groups] : [];
470
+ loadBilling(c);
272
471
  } else {
273
472
  if (titleEl) titleEl.textContent = 'Add Contact';
274
473
  if (saveLbl) saveLbl.textContent = 'Save Contact';
@@ -284,6 +483,7 @@ export const contactsView = {
284
483
  if (notesEl) notesEl.value = '';
285
484
  if (favEl) favEl.checked = false;
286
485
  selectedGroups = [];
486
+ loadBilling(null);
287
487
  }
288
488
 
289
489
  updateGroupsDisplay();
@@ -337,7 +537,8 @@ export const contactsView = {
337
537
  phone: ($container.find('#contact-phone').get(0)?.value ?? '').trim(),
338
538
  notes: ($container.find('#contact-notes').get(0)?.value ?? '').trim(),
339
539
  favourite: $container.find('#contact-favourite').get(0)?.checked === true,
340
- groups: selectedGroups
540
+ groups: selectedGroups,
541
+ ...readBilling()
341
542
  };
342
543
 
343
544
  try {
@@ -562,6 +763,36 @@ export const contactsView = {
562
763
  const addBtn = $container.find('#add-contact-btn').get(0);
563
764
  if (addBtn) addBtn.addEventListener('click', () => openEditor(null));
564
765
 
766
+ // Billing section: fold, unfold, and the three Add buttons.
767
+ const billingToggle = $container.find('#billing-toggle').get(0);
768
+ const billingBody = $container.find('#billing-body').get(0);
769
+ if (billingToggle && billingBody) {
770
+ billingToggle.addEventListener('click', () => {
771
+ const open = billingBody.classList.toggle('is-open');
772
+ // The chevron is ROTATED rather than swapped for a different
773
+ // icon: I.scan replaces the span with a fresh <svg>, so the
774
+ // element you held a reference to is gone and setting
775
+ // data-icon on it changes nothing. A CSS transform needs no
776
+ // element identity at all, and animates for free.
777
+ billingToggle.classList.toggle('is-open', open);
778
+ });
779
+ }
780
+
781
+ for (const [field, btnId] of [['addresses', '#add-address-btn'], ['emails', '#add-email-btn'], ['phones', '#add-phone-btn']]) {
782
+ const btn = $container.find(btnId).get(0);
783
+ if (!btn) continue;
784
+ btn.addEventListener('click', () => {
785
+ renderRepeater(field, [...readRepeater(field), {}]);
786
+ updateBillingSummary();
787
+ });
788
+ }
789
+
790
+ // The folded summary should follow what is typed above it.
791
+ for (const sel of ['#contact-company', '#contact-sort-code', '#contact-iban']) {
792
+ const el = $container.find(sel).get(0);
793
+ if (el) el.addEventListener('input', updateBillingSummary);
794
+ }
795
+
565
796
  // Editor save
566
797
  const editorSaveBtn = $container.find('#editor-save-btn').get(0);
567
798
  if (editorSaveBtn) editorSaveBtn.addEventListener('click', saveContact);
@@ -673,13 +904,15 @@ export const contactsView = {
673
904
  // Groups panel collapse toggle
674
905
  const panelToggle = $container.find('#groups-panel-toggle').get(0);
675
906
  const panelBody = $container.find('#groups-panel-body').get(0);
676
- const chevron = $container.find('#groups-chevron').get(0);
677
907
  if (panelToggle && panelBody) {
678
908
  panelToggle.addEventListener('click', () => {
679
909
  const open = panelBody.style.display !== 'none';
680
910
  panelBody.style.display = open ? 'none' : '';
681
- if (chevron) chevron.setAttribute('data-icon', open ? 'chevron-down' : 'chevron-up');
682
- Domma.icons.scan();
911
+ // Rotated, not swapped: I.scan replaces the icon span with a
912
+ // fresh <svg> and does not carry the id across, so the
913
+ // reference this held was stale after the first scan and the
914
+ // chevron never turned.
915
+ panelToggle.classList.toggle('is-open', !open);
683
916
  });
684
917
  }
685
918
 
@@ -40,6 +40,114 @@
40
40
  "type": "text",
41
41
  "required": false
42
42
  },
43
+ {
44
+ "name": "company",
45
+ "label": "Company",
46
+ "type": "text",
47
+ "required": false
48
+ },
49
+ {
50
+ "name": "jobTitle",
51
+ "label": "Job Title",
52
+ "type": "text",
53
+ "required": false
54
+ },
55
+ {
56
+ "name": "website",
57
+ "label": "Website",
58
+ "type": "text",
59
+ "required": false
60
+ },
61
+ {
62
+ "name": "vatNumber",
63
+ "label": "VAT Number",
64
+ "type": "text",
65
+ "required": false
66
+ },
67
+ {
68
+ "name": "companyNumber",
69
+ "label": "Company Number",
70
+ "type": "text",
71
+ "required": false
72
+ },
73
+ {
74
+ "name": "reference",
75
+ "label": "Their Reference",
76
+ "type": "text",
77
+ "required": false
78
+ },
79
+ {
80
+ "name": "billingEmail",
81
+ "label": "Billing Email",
82
+ "type": "text",
83
+ "required": false
84
+ },
85
+ {
86
+ "name": "paymentTerms",
87
+ "label": "Payment Terms (days)",
88
+ "type": "number",
89
+ "required": false
90
+ },
91
+ {
92
+ "name": "currency",
93
+ "label": "Preferred Currency",
94
+ "type": "text",
95
+ "required": false
96
+ },
97
+ {
98
+ "name": "addresses",
99
+ "label": "Addresses",
100
+ "type": "json",
101
+ "required": false
102
+ },
103
+ {
104
+ "name": "emails",
105
+ "label": "Additional Emails",
106
+ "type": "json",
107
+ "required": false
108
+ },
109
+ {
110
+ "name": "phones",
111
+ "label": "Additional Phones",
112
+ "type": "json",
113
+ "required": false
114
+ },
115
+ {
116
+ "name": "accountName",
117
+ "label": "Account Name",
118
+ "type": "text",
119
+ "required": false
120
+ },
121
+ {
122
+ "name": "bankName",
123
+ "label": "Bank",
124
+ "type": "text",
125
+ "required": false
126
+ },
127
+ {
128
+ "name": "sortCode",
129
+ "label": "Sort Code",
130
+ "type": "text",
131
+ "required": false
132
+ },
133
+ {
134
+ "name": "accountNumber",
135
+ "label": "Account Number",
136
+ "type": "text",
137
+ "required": false
138
+ },
139
+ {
140
+ "name": "iban",
141
+ "label": "IBAN",
142
+ "type": "text",
143
+ "required": false
144
+ },
145
+ {
146
+ "name": "bic",
147
+ "label": "BIC / SWIFT",
148
+ "type": "text",
149
+ "required": false
150
+ },
43
151
  {
44
152
  "name": "userId",
45
153
  "label": "User ID",