domma-cms 0.66.0 → 0.69.4
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.
- package/CLAUDE.md +64 -1
- package/README.md +14 -10
- package/admin/css/admin.css +1 -1
- package/admin/index.html +1 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/analytics-shape.js +1 -0
- package/admin/js/lib/contacts-arrange.js +1 -0
- package/admin/js/lib/help-popover.js +1 -0
- package/admin/js/lib/notes-arrange.js +1 -0
- package/admin/js/lib/plugin-accent.js +1 -1
- package/admin/js/lib/plugin-chrome.js +1 -1
- package/admin/js/lib/sidebar-grouping.js +1 -1
- package/admin/js/lib/sidebar-grouping.test.js +1 -1
- package/admin/js/lib/sidebar-renderer.js +3 -3
- package/admin/js/lib/todo-arrange.js +1 -0
- package/admin/js/lib/tool-kit.js +1 -0
- package/admin/js/templates/analytics.html +138 -0
- package/admin/js/templates/contacts.html +156 -0
- package/admin/js/templates/notes.html +137 -0
- package/admin/js/templates/plugin-marketplace.html +17 -4
- package/admin/js/templates/plugins.html +13 -7
- package/admin/js/templates/theme.html +38 -0
- package/admin/js/templates/todo.html +113 -0
- package/admin/js/views/analytics.js +62 -0
- package/admin/js/views/contacts.js +174 -0
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/notes.js +104 -0
- package/admin/js/views/plugin-marketplace.js +1 -1
- package/admin/js/views/plugins.js +24 -22
- package/admin/js/views/theme.js +1 -1
- package/admin/js/views/todo.js +96 -0
- package/bin/cli.js +4 -10
- package/bin/lib/plugin-version.js +28 -0
- package/bin/update.js +14 -0
- package/config/menus/admin-sidebar.json +1 -1
- package/config/server.json +3 -1
- package/package.json +3 -7
- package/plugins/_lib/admin/mail/reader-view.js +20 -0
- package/plugins/_lib/mail/store.js +21 -1
- package/plugins/blog/CLAUDE.md +237 -0
- package/plugins/blog/admin/css/index.css +178 -1
- package/plugins/blog/admin/templates/blog.html +35 -53
- package/plugins/blog/admin/templates/categories.html +19 -6
- package/plugins/blog/admin/templates/comments.html +26 -9
- package/plugins/blog/admin/templates/post-editor.html +36 -13
- package/plugins/blog/admin/templates/settings.html +2 -11
- package/plugins/blog/admin/views/blog.js +288 -162
- package/plugins/blog/admin/views/categories.js +212 -201
- package/plugins/blog/admin/views/comments.js +207 -158
- package/plugins/blog/admin/views/kit.js +372 -0
- package/plugins/blog/admin/views/post-editor.js +269 -21
- package/plugins/blog/admin/views/settings-cog.js +56 -0
- package/plugins/blog/admin/views/settings.js +16 -86
- package/plugins/blog/blocks/blog-card-compact.css +17 -0
- package/plugins/blog/blocks/blog-card-compact.html +8 -0
- package/plugins/blog/blocks/blog-card-row.css +22 -0
- package/plugins/blog/blocks/blog-card-row.html +11 -0
- package/plugins/blog/blocks/blog-card.css +35 -0
- package/plugins/blog/blocks/blog-card.html +16 -0
- package/plugins/blog/blocks/blog-post-classic.css +15 -0
- package/plugins/blog/blocks/blog-post-classic.html +28 -0
- package/plugins/blog/blocks/blog-post-feature.css +20 -0
- package/plugins/blog/blocks/blog-post-feature.html +30 -0
- package/plugins/blog/blocks/blog-post-minimal.css +10 -0
- package/plugins/blog/blocks/blog-post-minimal.html +23 -0
- package/plugins/blog/blocks/blog-post-sidebar.css +22 -0
- package/plugins/blog/blocks/blog-post-sidebar.html +36 -0
- package/plugins/blog/collections/posts/schema.json +3 -1
- package/plugins/blog/config.js +6 -0
- package/plugins/blog/lib/layouts.js +304 -0
- package/plugins/blog/lib/page.js +158 -0
- package/plugins/blog/lib/render.js +119 -0
- package/plugins/blog/lib/samples.js +234 -0
- package/plugins/blog/plugin.js +225 -15
- package/plugins/blog/plugin.json +21 -81
- package/plugins/blog/plugin.public.js +115 -210
- package/plugins/blog/public/blog.css +177 -0
- package/plugins/blog/public/blog.js +586 -0
- package/plugins/blog/public/samples/aurora.svg +7 -0
- package/plugins/blog/public/samples/dusk.svg +7 -0
- package/plugins/blog/public/samples/ember.svg +7 -0
- package/plugins/blog/public/samples/harbour.svg +7 -0
- package/plugins/blog/public/samples/meadow.svg +7 -0
- package/plugins/blog/public/samples/rosewood.svg +7 -0
- package/plugins/blog/templates/index.html +9 -8
- package/plugins/blog/templates/post.html +4 -17
- package/plugins/blog/tests/layouts.test.js +97 -0
- package/plugins/blog/tests/public.test.js +71 -0
- package/plugins/free-tier.lock.json +74 -0
- package/plugins/mail-reader/CLAUDE.md +78 -0
- package/plugins/mail-reader/plugin.json +2 -1
- package/public/css/theme-switcher.css +1 -0
- package/public/js/analytics.js +1 -0
- package/public/js/theme-switcher.js +1 -0
- package/scripts/build.js +41 -2
- package/scripts/run-tests.mjs +94 -0
- package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
- package/{plugins/contacts/plugin.js → server/routes/api/contacts.js} +71 -43
- package/server/routes/api/dashboard.js +8 -2
- package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
- package/server/routes/api/plugin-marketplace.js +78 -23
- package/server/routes/api/plugins.js +148 -3
- package/server/routes/api/todo.js +178 -0
- package/server/server.js +61 -1
- package/server/services/collections.js +13 -3
- package/server/services/managerClient.js +72 -136
- package/server/services/markdown.js +54 -1
- package/server/services/permissionRegistry.js +50 -0
- package/server/services/pluginEntitlement.js +171 -0
- package/server/services/pluginEnvelope.js +242 -0
- package/server/services/pluginInstaller.js +246 -26
- package/server/services/pluginScaffold.js +5 -0
- package/server/services/plugins.js +117 -7
- package/server/services/presetCollections.js +344 -0
- package/server/services/promoted-plugins-migration.js +254 -0
- package/server/services/publicCors.js +46 -0
- package/server/services/renderer.js +83 -3
- package/server/services/sidebarBadges.js +37 -0
- package/server/services/siteGitignore.js +310 -0
- package/server/services/themeSettings.js +39 -0
- package/config/theme.json +0 -18
- package/plugins/analytics/admin/css/index.css +0 -1
- package/plugins/analytics/admin/templates/analytics.html +0 -70
- package/plugins/analytics/admin/views/analytics.js +0 -176
- package/plugins/analytics/config.js +0 -14
- package/plugins/analytics/plugin.json +0 -60
- package/plugins/analytics/public/inject-body.html +0 -60
- package/plugins/analytics/public/inject-head.html +0 -1
- package/plugins/blog/templates/author.html +0 -9
- package/plugins/blog/templates/category.html +0 -9
- package/plugins/blog/templates/tag.html +0 -9
- package/plugins/contacts/admin/templates/contacts.html +0 -254
- package/plugins/contacts/admin/views/contacts.js +0 -943
- package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
- package/plugins/contacts/collections/user-contacts/schema.json +0 -179
- package/plugins/contacts/config.js +0 -6
- package/plugins/contacts/plugin.json +0 -36
- package/plugins/demo-viewer/config.js +0 -4
- package/plugins/demo-viewer/plugin.js +0 -36
- package/plugins/demo-viewer/plugin.json +0 -9
- package/plugins/invoice/admin/css/index.css +0 -1
- package/plugins/invoice/admin/templates/editor.html +0 -220
- package/plugins/invoice/admin/templates/index.html +0 -173
- package/plugins/invoice/admin/templates/issuers.html +0 -2
- package/plugins/invoice/admin/templates/receivers.html +0 -2
- package/plugins/invoice/admin/views/contacts-source.js +0 -266
- package/plugins/invoice/admin/views/editor.js +0 -434
- package/plugins/invoice/admin/views/export.js +0 -199
- package/plugins/invoice/admin/views/help-content.js +0 -61
- package/plugins/invoice/admin/views/index.js +0 -643
- package/plugins/invoice/admin/views/issuers.js +0 -30
- package/plugins/invoice/admin/views/media.js +0 -172
- package/plugins/invoice/admin/views/party-view.js +0 -386
- package/plugins/invoice/admin/views/payments.js +0 -127
- package/plugins/invoice/admin/views/print.js +0 -130
- package/plugins/invoice/admin/views/receivers.js +0 -55
- package/plugins/invoice/admin/views/send.js +0 -212
- package/plugins/invoice/admin/views/settings.js +0 -594
- package/plugins/invoice/admin/views/view-lifecycle.js +0 -33
- package/plugins/invoice/collections/invoice-issuers/schema.json +0 -82
- package/plugins/invoice/collections/invoice-receivers/schema.json +0 -16
- package/plugins/invoice/collections/invoices/schema.json +0 -33
- package/plugins/invoice/config.js +0 -37
- package/plugins/invoice/pdf.js +0 -164
- package/plugins/invoice/plugin.js +0 -1456
- package/plugins/invoice/plugin.json +0 -86
- package/plugins/invoice/templates/_base.css +0 -1
- package/plugins/invoice/templates/classic-nologo.html +0 -100
- package/plugins/invoice/templates/classic.html +0 -91
- package/plugins/invoice/templates/invoice-print.html +0 -237
- package/plugins/invoice/templates/minimal.html +0 -99
- package/plugins/invoice/templates/modern-nologo.html +0 -114
- package/plugins/invoice/templates/modern.html +0 -113
- package/plugins/invoice/templates/templates.json +0 -11
- package/plugins/notes/admin/templates/notes.html +0 -83
- package/plugins/notes/admin/views/notes.js +0 -281
- package/plugins/notes/collections/user-notes/schema.json +0 -54
- package/plugins/notes/config.js +0 -6
- package/plugins/notes/plugin.json +0 -33
- package/plugins/surveys/admin/templates/audience.html +0 -47
- package/plugins/surveys/admin/templates/results.html +0 -58
- package/plugins/surveys/admin/templates/survey-editor.html +0 -141
- package/plugins/surveys/admin/templates/surveys.html +0 -25
- package/plugins/surveys/admin/views/audience.js +0 -301
- package/plugins/surveys/admin/views/results.js +0 -172
- package/plugins/surveys/admin/views/survey-editor.js +0 -211
- package/plugins/surveys/admin/views/surveys.js +0 -161
- package/plugins/surveys/collections/survey-contacts/schema.json +0 -13
- package/plugins/surveys/collections/survey-groups/schema.json +0 -11
- package/plugins/surveys/collections/survey-invites/schema.json +0 -16
- package/plugins/surveys/collections/surveys/schema.json +0 -23
- package/plugins/surveys/config.js +0 -8
- package/plugins/surveys/lib/audience.js +0 -37
- package/plugins/surveys/lib/campaigns.js +0 -43
- package/plugins/surveys/lib/ledger.js +0 -110
- package/plugins/surveys/lib/sending.js +0 -106
- package/plugins/surveys/lib/stats.js +0 -62
- package/plugins/surveys/lib/submit.js +0 -95
- package/plugins/surveys/lib/tokens.js +0 -28
- package/plugins/surveys/plugin.js +0 -174
- package/plugins/surveys/plugin.json +0 -65
- package/plugins/surveys/plugin.public.js +0 -149
- package/plugins/surveys/public/dist-shared.mjs +0 -1
- package/plugins/surveys/public/survey.css +0 -1
- package/plugins/surveys/public/survey.mjs +0 -1
- package/plugins/surveys/templates/survey-page.html +0 -63
- package/plugins/theme-switcher/admin/templates/theme-switcher.html +0 -86
- package/plugins/theme-switcher/admin/views/theme-switcher.js +0 -65
- package/plugins/theme-switcher/config.js +0 -10
- package/plugins/theme-switcher/plugin.js +0 -26
- package/plugins/theme-switcher/plugin.json +0 -74
- package/plugins/theme-switcher/public/inject-body.html +0 -153
- package/plugins/theme-switcher/public/inject-head.html +0 -260
- package/plugins/todo/admin/templates/todo.html +0 -158
- package/plugins/todo/admin/views/todo.js +0 -343
- package/plugins/todo/collections/todos/schema.json +0 -60
- package/plugins/todo/config.js +0 -7
- package/plugins/todo/plugin.js +0 -102
- package/plugins/todo/plugin.json +0 -33
- /package/{plugins/contacts/admin/css/index.css → admin/css/contacts.css} +0 -0
|
@@ -1,643 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Invoice plugin - list view.
|
|
3
|
-
*
|
|
4
|
-
* The page is bound: every filter control is an observable, the filtered list
|
|
5
|
-
* and the KPIs are computeds over them, and one M.effect pushes the result into
|
|
6
|
-
* Domma's table. There is no applyFilters()/refreshKpis() pass and no
|
|
7
|
-
* hand-rolled debounce - the search box is `.extend({rateLimit})`, which holds
|
|
8
|
-
* the NOTIFICATION back without ever delaying the write.
|
|
9
|
-
*
|
|
10
|
-
* The table itself stays imperative: T owns that markup and re-renders it on
|
|
11
|
-
* setData, so bindings placed inside it would not survive. The effect is the
|
|
12
|
-
* seam between the two.
|
|
13
|
-
*
|
|
14
|
-
* Per-row actions live in a CONTEXT MENU rather than in a column of buttons.
|
|
15
|
-
* There are eleven of them now - print, send, PDF, two spreadsheet formats,
|
|
16
|
-
* duplicate, two status moves, archive, delete - and a row of eleven icons is
|
|
17
|
-
* not a table any more. The same menu opens from the row's ⋯ button, so the
|
|
18
|
-
* gesture is discoverable without a right-click.
|
|
19
|
-
*/
|
|
20
|
-
import {canDownloadPdf, downloadPdf, downloadPdfBatch, openPrintWindow} from './print.js?v=9';
|
|
21
|
-
import {disposeOnLeave} from './view-lifecycle.js?v=8';
|
|
22
|
-
import {openSendDialog} from './send.js?v=8';
|
|
23
|
-
import {openInvoiceSettings} from './settings.js?v=8';
|
|
24
|
-
import {exportInvoice, exportList} from './export.js?v=8';
|
|
25
|
-
import {INVOICE_HELP} from './help-content.js?v=8';
|
|
26
|
-
import {openHelpPanel, scanHelp} from '/plugins/_lib/admin/ui/help.js?v=8';
|
|
27
|
-
|
|
28
|
-
function escapeHtml(s) {
|
|
29
|
-
return String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const BASE = '/api/plugins/invoice';
|
|
33
|
-
|
|
34
|
-
const STATUS_BADGE = {
|
|
35
|
-
draft: 'badge-secondary',
|
|
36
|
-
sent: 'badge-info',
|
|
37
|
-
paid: 'badge-success',
|
|
38
|
-
overdue: 'badge-danger',
|
|
39
|
-
cancelled: 'badge-secondary'
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* What the row should SAY its status is.
|
|
44
|
-
*
|
|
45
|
-
* `overdue` is derived server-side on every read rather than stored, so a
|
|
46
|
-
* sent invoice past its due date reports overdue without anything having had
|
|
47
|
-
* to run a job to flip it. The stored status is still `sent`, which is why the
|
|
48
|
-
* editor's dropdown does not suddenly disagree with the list.
|
|
49
|
-
*
|
|
50
|
-
* @param {object} row
|
|
51
|
-
* @returns {string}
|
|
52
|
-
*/
|
|
53
|
-
function displayStatus(row) {
|
|
54
|
-
return row?.overdue ? 'overdue' : (row?.status || 'draft');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function money(amount, currency) {
|
|
58
|
-
try {
|
|
59
|
-
return new Intl.NumberFormat('en-GB', { style: 'currency', currency: currency || 'GBP' }).format(Number(amount) || 0);
|
|
60
|
-
} catch {
|
|
61
|
-
return `${currency || 'GBP'} ${(Number(amount) || 0).toFixed(2)}`;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export const indexView = {
|
|
66
|
-
templateUrl: '/plugins/invoice/admin/templates/index.html',
|
|
67
|
-
|
|
68
|
-
async onMount($container) {
|
|
69
|
-
const state = {
|
|
70
|
-
invoices: M.observable([]),
|
|
71
|
-
receivers: M.observable([]),
|
|
72
|
-
issuers: M.observable([]),
|
|
73
|
-
canPdf: M.observable(false),
|
|
74
|
-
|
|
75
|
-
status: M.observable(''),
|
|
76
|
-
issuerId: M.observable(''),
|
|
77
|
-
from: M.observable(''),
|
|
78
|
-
to: M.observable(''),
|
|
79
|
-
showArchived: M.observable(false),
|
|
80
|
-
sortBy: M.observable('issueDate:desc'),
|
|
81
|
-
// The write lands immediately; only the announcement waits for the
|
|
82
|
-
// typing to stop, so the filter runs once per pause, not per key.
|
|
83
|
-
query: M.observable('').extend({ rateLimit: 200 })
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const receiverOf = (id) => state.receivers.value.find((x) => x.id === id) ?? null;
|
|
87
|
-
const issuerOf = (id) => state.issuers.value.find((x) => x.id === id) ?? null;
|
|
88
|
-
|
|
89
|
-
const receiverName = (id) => {
|
|
90
|
-
const r = receiverOf(id);
|
|
91
|
-
return r ? (r.company || r.name || id) : '-';
|
|
92
|
-
};
|
|
93
|
-
const issuerName = (id) => {
|
|
94
|
-
const i = issuerOf(id);
|
|
95
|
-
return i ? (i.name || id) : '-';
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
state.issuerOptions = M.computed(() => state.issuers.value
|
|
99
|
-
.map((i) => ({ id: i.id, label: i.name || '(unnamed)' })));
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* The rows the table works on.
|
|
103
|
-
*
|
|
104
|
-
* The scalar `_` fields exist so T can SORT them: its comparator reads
|
|
105
|
-
* `row[column.key]`, and `totals` is an object and `receiverId` is an
|
|
106
|
-
* id, so sorting either column sorted by something nobody can see.
|
|
107
|
-
*/
|
|
108
|
-
const rows = M.computed(() => state.invoices.value.map((inv) => ({
|
|
109
|
-
...inv,
|
|
110
|
-
_number: inv.number ?? '',
|
|
111
|
-
_issuer: issuerName(inv.issuerId),
|
|
112
|
-
_receiver: receiverName(inv.receiverId),
|
|
113
|
-
_status: displayStatus(inv),
|
|
114
|
-
_total: Number(inv.totals?.total) || 0,
|
|
115
|
-
_balance: Number(inv.totals?.balance) || 0
|
|
116
|
-
})));
|
|
117
|
-
|
|
118
|
-
state.filtered = M.computed(() => {
|
|
119
|
-
const status = state.status.value;
|
|
120
|
-
const issuer = state.issuerId.value;
|
|
121
|
-
const from = state.from.value;
|
|
122
|
-
const to = state.to.value;
|
|
123
|
-
const archived = state.showArchived.value;
|
|
124
|
-
const q = String(state.query.value || '').toLowerCase().trim();
|
|
125
|
-
|
|
126
|
-
return rows.value.filter((inv) => {
|
|
127
|
-
// Archived is a shelf: it is hidden unless asked for, and when
|
|
128
|
-
// it IS asked for the shelf is all you get - mixing the two
|
|
129
|
-
// makes "show archived" look like it did nothing.
|
|
130
|
-
if (Boolean(inv.archived) !== archived) return false;
|
|
131
|
-
if (status && inv._status !== status) return false;
|
|
132
|
-
if (issuer && inv.issuerId !== issuer) return false;
|
|
133
|
-
if (from && String(inv.issueDate ?? '') < from) return false;
|
|
134
|
-
if (to && String(inv.issueDate ?? '') > to) return false;
|
|
135
|
-
if (!q) return true;
|
|
136
|
-
|
|
137
|
-
const receiver = receiverOf(inv.receiverId);
|
|
138
|
-
return [inv._number, inv._receiver, inv._issuer, receiver?.reference, receiver?.email, inv.notes]
|
|
139
|
-
.some((v) => String(v ?? '').toLowerCase().includes(q));
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// ---- KPIs -----------------------------------------------------------
|
|
144
|
-
//
|
|
145
|
-
// Counted over the LIVE invoices rather than the filtered list: a KPI
|
|
146
|
-
// that moved with the search box would answer a different question
|
|
147
|
-
// every keystroke, and the one being asked here is "how is the ledger".
|
|
148
|
-
|
|
149
|
-
const live = M.computed(() => state.invoices.value.filter((i) => !i.archived));
|
|
150
|
-
const countBy = (test) => M.computed(() => live.value.filter(test).length);
|
|
151
|
-
|
|
152
|
-
state.kpiTotal = M.computed(() => live.value.length);
|
|
153
|
-
state.kpiPaid = countBy((i) => i.status === 'paid');
|
|
154
|
-
state.kpiDrafts = countBy((i) => i.status === 'draft');
|
|
155
|
-
state.kpiOverdue = countBy((i) => displayStatus(i) === 'overdue');
|
|
156
|
-
state.kpiOutstanding = M.computed(() => {
|
|
157
|
-
const owed = live.value
|
|
158
|
-
.filter((i) => ['sent', 'overdue'].includes(displayStatus(i)))
|
|
159
|
-
.reduce((sum, i) => sum + (Number(i.totals?.balance) || 0), 0);
|
|
160
|
-
return owed;
|
|
161
|
-
});
|
|
162
|
-
state.kpiOutstandingText = M.computed(() => {
|
|
163
|
-
// Whatever the first outstanding invoice is billed in. Mixing
|
|
164
|
-
// currencies into one total would be a lie, so the count carries
|
|
165
|
-
// the meaning when they differ.
|
|
166
|
-
const outstanding = live.value.filter((i) => ['sent', 'overdue'].includes(displayStatus(i)));
|
|
167
|
-
const currencies = new Set(outstanding.map((i) => i.currency || 'GBP'));
|
|
168
|
-
if (currencies.size > 1) return `${outstanding.length} invoices`;
|
|
169
|
-
return money(state.kpiOutstanding.value, [...currencies][0] || 'GBP');
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
state.hasFilters = M.computed(() => Boolean(
|
|
173
|
-
state.status.value || state.issuerId.value || state.from.value
|
|
174
|
-
|| state.to.value || state.query.value || state.showArchived.value
|
|
175
|
-
));
|
|
176
|
-
|
|
177
|
-
state.matchesText = M.computed(() => {
|
|
178
|
-
const shown = state.filtered.value.length;
|
|
179
|
-
const all = rows.value.filter((i) => Boolean(i.archived) === state.showArchived.value).length;
|
|
180
|
-
if (!all) return '';
|
|
181
|
-
return shown === all ? `${all} shown` : `${shown} of ${all} shown`;
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
state.hasRows = M.computed(() => state.filtered.value.length > 0);
|
|
185
|
-
state.isEmpty = M.computed(() => state.filtered.value.length === 0);
|
|
186
|
-
|
|
187
|
-
// Three different nothings, and saying which one it is saves a hunt:
|
|
188
|
-
// an empty ledger, an empty shelf, or a filter that matches nothing.
|
|
189
|
-
state.emptyText = M.computed(() => {
|
|
190
|
-
if (!state.invoices.value.length) return 'No invoices yet. Click “New Invoice” to create your first.';
|
|
191
|
-
if (state.showArchived.value
|
|
192
|
-
&& !state.invoices.value.some((i) => i.archived)) return 'Nothing has been archived.';
|
|
193
|
-
return 'No invoice matches those filters.';
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// ---- can we invoice at all? ----------------------------------------
|
|
197
|
-
//
|
|
198
|
-
// An invoice needs somebody at both ends, and the server refuses one
|
|
199
|
-
// without them. A button that leads to a screen whose Save can only
|
|
200
|
-
// fail is worse than a button that is plainly off, so this is computed
|
|
201
|
-
// from the same two lists the editor's pickers are built from.
|
|
202
|
-
|
|
203
|
-
// Each of these reads the OBSERVABLES, not each other. A computed built
|
|
204
|
-
// on another computed updated when something read it through a third
|
|
205
|
-
// computed (`newInvoiceTitle` was correct all along) but the `data-if`
|
|
206
|
-
// and `data-bind-disabled` hung off it never re-ran - the element stayed
|
|
207
|
-
// and the button stayed disabled with the right title on it. One level
|
|
208
|
-
// of derivation, and all three agree.
|
|
209
|
-
const noIssuers = () => state.issuers.value.length === 0;
|
|
210
|
-
const noReceivers = () => state.receivers.value.length === 0;
|
|
211
|
-
|
|
212
|
-
state.needsIssuer = M.computed(noIssuers);
|
|
213
|
-
state.needsReceiver = M.computed(noReceivers);
|
|
214
|
-
state.needsSetup = M.computed(() => noIssuers() || noReceivers());
|
|
215
|
-
state.cannotCreate = M.computed(() => noIssuers() || noReceivers());
|
|
216
|
-
|
|
217
|
-
state.newInvoiceTitle = M.computed(() => {
|
|
218
|
-
if (!noIssuers() && !noReceivers()) return 'Create a new invoice';
|
|
219
|
-
const missing = [noIssuers() ? 'an issuer' : '', noReceivers() ? 'a receiver' : '']
|
|
220
|
-
.filter(Boolean).join(' and ');
|
|
221
|
-
return `Add ${missing} first - an invoice needs somebody at both ends.`;
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
state.setupTitle = M.computed(() => (noIssuers() && noReceivers()
|
|
225
|
-
? 'Two things first'
|
|
226
|
-
: 'One thing first'));
|
|
227
|
-
|
|
228
|
-
state.setupText = M.computed(() => {
|
|
229
|
-
if (noIssuers() && noReceivers()) {
|
|
230
|
-
return 'An invoice is raised by an issuer (you) for a receiver (your customer). Add one of each and New Invoice switches on.';
|
|
231
|
-
}
|
|
232
|
-
if (noIssuers()) {
|
|
233
|
-
return 'An issuer is you - the trading name, address and bank details an invoice is paid into.';
|
|
234
|
-
}
|
|
235
|
-
return 'A receiver is who you are billing. If you use Contacts, you can import one.';
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
state.addIssuer = () => openSettings('issuers');
|
|
239
|
-
state.addReceiver = () => openSettings('receivers');
|
|
240
|
-
state.openHelp = () => openHelpPanel(INVOICE_HELP);
|
|
241
|
-
|
|
242
|
-
// ---- the KPI tiles are filters -------------------------------------
|
|
243
|
-
//
|
|
244
|
-
// A number you cannot act on is a poster. Clicking one puts the list
|
|
245
|
-
// into the state it counts, which is what somebody wanted the moment
|
|
246
|
-
// they read it.
|
|
247
|
-
|
|
248
|
-
// Assigned once the pickers exist; the KPI tiles and Clear both reset
|
|
249
|
-
// the date range and are wired before them.
|
|
250
|
-
let clearPickers = () => {};
|
|
251
|
-
|
|
252
|
-
const filterTo = (status) => {
|
|
253
|
-
state.showArchived.value = false;
|
|
254
|
-
state.query.value = '';
|
|
255
|
-
state.issuerId.value = '';
|
|
256
|
-
state.from.value = '';
|
|
257
|
-
state.to.value = '';
|
|
258
|
-
state.status.value = status;
|
|
259
|
-
clearPickers();
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
state.showAll = () => filterTo('');
|
|
263
|
-
state.showOutstanding = () => filterTo('sent');
|
|
264
|
-
state.showOverdue = () => filterTo('overdue');
|
|
265
|
-
state.showPaid = () => filterTo('paid');
|
|
266
|
-
state.showDrafts = () => filterTo('draft');
|
|
267
|
-
|
|
268
|
-
state.newInvoice = () => {
|
|
269
|
-
if (noIssuers() || noReceivers()) {
|
|
270
|
-
E.toast(state.newInvoiceTitle.value, { type: 'error' });
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
R.navigate('#/plugins/invoice/new');
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
state.clearFilters = () => {
|
|
277
|
-
state.status.value = '';
|
|
278
|
-
state.issuerId.value = '';
|
|
279
|
-
state.from.value = '';
|
|
280
|
-
state.to.value = '';
|
|
281
|
-
state.query.value = '';
|
|
282
|
-
state.showArchived.value = false;
|
|
283
|
-
// The pickers own their inputs' text, so clearing the observable is
|
|
284
|
-
// not enough - the field would still read as a date it no longer
|
|
285
|
-
// filters by.
|
|
286
|
-
clearPickers();
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
const exportNames = (inv) => ({
|
|
290
|
-
issuerName: issuerName(inv.issuerId),
|
|
291
|
-
receiverName: receiverName(inv.receiverId)
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
state.exportCsv = () => exportList(state.filtered.value, 'csv', exportNames);
|
|
295
|
-
state.exportExcel = () => exportList(state.filtered.value, 'excel', exportNames);
|
|
296
|
-
// The spreadsheet exports describe the invoices; this one IS them - the
|
|
297
|
-
// same documents a client receives, one per page. It goes through the
|
|
298
|
-
// server because that is where the renderer is, so unlike CSV and Excel
|
|
299
|
-
// it is offered only where a PDF can actually be made.
|
|
300
|
-
state.exportPdf = () => downloadPdfBatch(state.filtered.value.map((inv) => inv.id));
|
|
301
|
-
|
|
302
|
-
// ---- mutations ------------------------------------------------------
|
|
303
|
-
|
|
304
|
-
/** Replace one invoice in the list, in place. */
|
|
305
|
-
function replace(updated) {
|
|
306
|
-
if (!updated?.id) return;
|
|
307
|
-
state.invoices.value = state.invoices.value.map((x) => (x.id === updated.id ? updated : x));
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
async function setStatus(inv, status) {
|
|
311
|
-
try {
|
|
312
|
-
const res = await H.put(`${BASE}/invoices/${inv.id}`, { status });
|
|
313
|
-
const out = res?.data ?? res;
|
|
314
|
-
if (out?.error) throw new Error(out.error);
|
|
315
|
-
replace(out);
|
|
316
|
-
E.toast(`Marked ${status}.`, { type: 'success' });
|
|
317
|
-
} catch (err) {
|
|
318
|
-
E.toast(err?.message ?? 'Could not update that invoice.', { type: 'error' });
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
async function setArchived(inv, archived) {
|
|
323
|
-
try {
|
|
324
|
-
const res = await H.put(`${BASE}/invoices/${inv.id}`, { archived });
|
|
325
|
-
const out = res?.data ?? res;
|
|
326
|
-
if (out?.error) throw new Error(out.error);
|
|
327
|
-
replace(out);
|
|
328
|
-
E.toast(archived ? 'Archived.' : 'Restored.', { type: 'success' });
|
|
329
|
-
} catch (err) {
|
|
330
|
-
E.toast(err?.message ?? 'Could not archive that invoice.', { type: 'error' });
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
async function duplicate(inv) {
|
|
335
|
-
try {
|
|
336
|
-
const res = await H.post(`${BASE}/invoices/${inv.id}/duplicate`, {});
|
|
337
|
-
const out = res?.data ?? res;
|
|
338
|
-
if (out?.error) throw new Error(out.error);
|
|
339
|
-
state.invoices.value = [out, ...state.invoices.value];
|
|
340
|
-
E.toast(`Copied to ${out.number}.`, { type: 'success' });
|
|
341
|
-
R.navigate(`#/plugins/invoice/edit/${out.id}`);
|
|
342
|
-
} catch (err) {
|
|
343
|
-
E.toast(err?.message ?? 'Could not copy that invoice.', { type: 'error' });
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
async function remove(inv) {
|
|
348
|
-
const ok = await E.confirm(`Delete invoice ${inv.number ?? ''}? Archiving keeps it and hides it.`);
|
|
349
|
-
if (!ok) return;
|
|
350
|
-
try {
|
|
351
|
-
await H.delete(`${BASE}/invoices/${inv.id}`);
|
|
352
|
-
E.toast('Invoice deleted.', { type: 'success' });
|
|
353
|
-
// One write; the KPIs, the count and the table follow.
|
|
354
|
-
state.invoices.value = state.invoices.value.filter((x) => x.id !== inv.id);
|
|
355
|
-
} catch {
|
|
356
|
-
E.toast('Delete failed.', { type: 'error' });
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// ---- the table ------------------------------------------------------
|
|
361
|
-
|
|
362
|
-
const tableEl = $container.find('#invoices-table').get(0);
|
|
363
|
-
let table = null;
|
|
364
|
-
// Declared before renderTable, which closes over it: T's onSort fires
|
|
365
|
-
// from inside the table, and a `let` read before its initialiser is a
|
|
366
|
-
// ReferenceError rather than `undefined`.
|
|
367
|
-
let syncingSort = false;
|
|
368
|
-
|
|
369
|
-
function renderTable(data) {
|
|
370
|
-
if (!tableEl) return;
|
|
371
|
-
if (table) {
|
|
372
|
-
table.setData(data);
|
|
373
|
-
Domma.icons.scan(tableEl);
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
table = T.create(tableEl, {
|
|
377
|
-
data,
|
|
378
|
-
columns: [
|
|
379
|
-
{
|
|
380
|
-
key: '_number', title: 'Number', sortable: true,
|
|
381
|
-
render: (v, row) => `<a class="invoice-number-link" data-id="${escapeHtml(row.id)}" href="#/plugins/invoice/edit/${escapeHtml(row.id)}">${escapeHtml(v || '-')}</a>`
|
|
382
|
-
},
|
|
383
|
-
{ key: '_receiver', title: 'Billed To', sortable: true, render: (v) => escapeHtml(v) },
|
|
384
|
-
{ key: 'issueDate', title: 'Issued', sortable: true,
|
|
385
|
-
render: (v) => v ? escapeHtml(D(v).format('DD MMM YYYY')) : '-'
|
|
386
|
-
},
|
|
387
|
-
{ key: 'dueDate', title: 'Due', sortable: true,
|
|
388
|
-
render: (v, row) => {
|
|
389
|
-
if (!v) return '-';
|
|
390
|
-
const late = row._status === 'overdue';
|
|
391
|
-
return `<span class="${late ? 'text-danger' : ''}">${escapeHtml(D(v).format('DD MMM YYYY'))}</span>`;
|
|
392
|
-
}
|
|
393
|
-
},
|
|
394
|
-
{ key: '_total', title: 'Total', sortable: true,
|
|
395
|
-
render: (v, row) => `<span class="invoice-num">${escapeHtml(money(v, row.currency))}</span>`
|
|
396
|
-
},
|
|
397
|
-
{ key: '_status', title: 'Status', sortable: true,
|
|
398
|
-
render: (v, row) => {
|
|
399
|
-
const archived = row.archived ? ' <span class="badge badge-secondary">archived</span>' : '';
|
|
400
|
-
return `<span class="badge ${STATUS_BADGE[v] || 'badge-secondary'}">${escapeHtml(v)}</span>${archived}`;
|
|
401
|
-
}
|
|
402
|
-
},
|
|
403
|
-
// A space, not '': T falls back to the column KEY for a
|
|
404
|
-
// falsy title, which printed a column headed "id".
|
|
405
|
-
{ key: 'id', title: ' ', sortable: false,
|
|
406
|
-
render: (id) => `<button class="btn btn-sm btn-ghost invoice-row-menu" data-id="${escapeHtml(id)}" title="Actions" aria-label="Actions">⋯</button>`
|
|
407
|
-
}
|
|
408
|
-
],
|
|
409
|
-
// Domma's table ships its own search box. This view already has
|
|
410
|
-
// one, and its filter also matches the receiver NAME, the
|
|
411
|
-
// reference and the notes - which a search over the raw row
|
|
412
|
-
// cannot. Two boxes, one of them worse, is not a feature.
|
|
413
|
-
searchable: false,
|
|
414
|
-
pagination: true,
|
|
415
|
-
pageSize: 25,
|
|
416
|
-
defaultSort: { column: 'issueDate', direction: 'desc' },
|
|
417
|
-
// Clicking a header is the other half of the Sort control; this
|
|
418
|
-
// keeps the two showing the same answer.
|
|
419
|
-
onSort: ({ sorts }) => {
|
|
420
|
-
const first = sorts?.[0];
|
|
421
|
-
if (!first) return;
|
|
422
|
-
const value = `${first.column}:${first.direction}`;
|
|
423
|
-
if (state.sortBy.value !== value) {
|
|
424
|
-
syncingSort = true;
|
|
425
|
-
state.sortBy.value = value;
|
|
426
|
-
syncingSort = false;
|
|
427
|
-
}
|
|
428
|
-
},
|
|
429
|
-
// NB: Domma's table has no empty-state option, so the "nothing
|
|
430
|
-
// here" line is in the template, bound to `isEmpty`.
|
|
431
|
-
pageSizeOptions: [25, 50, 100]
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
tableEl.addEventListener('click', (e) => {
|
|
435
|
-
const menuBtn = e.target.closest('.invoice-row-menu');
|
|
436
|
-
if (!menuBtn) return;
|
|
437
|
-
e.preventDefault();
|
|
438
|
-
const rect = menuBtn.getBoundingClientRect();
|
|
439
|
-
rowMenu?.open(rect.left, rect.bottom, menuBtn.closest('tr'));
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
Domma.icons.scan(tableEl);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// The seam: tracked read of the filtered list, untracked write to the
|
|
446
|
-
// table, so T's own DOM reads never become dependencies of this effect.
|
|
447
|
-
// M.effect returns its disposer, not a handle.
|
|
448
|
-
const stopFeed = M.effect(() => {
|
|
449
|
-
const data = state.filtered.value;
|
|
450
|
-
M.untracked(() => renderTable(data));
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
// The Sort select drives T's own sort rather than re-ordering the data,
|
|
454
|
-
// so there is ONE sort in play and a header click cannot disagree with
|
|
455
|
-
// the select. `syncingSort` stops the round trip from looping.
|
|
456
|
-
const stopSort = M.effect(() => {
|
|
457
|
-
const [column, direction] = String(state.sortBy.value || '').split(':');
|
|
458
|
-
if (syncingSort || !column) return;
|
|
459
|
-
M.untracked(() => table?.sort(column, direction === 'asc' ? 'asc' : 'desc'));
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
// ---- the row menu ---------------------------------------------------
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* The invoice a gesture landed on.
|
|
466
|
-
*
|
|
467
|
-
* T does not put the row key on its `<tr>`, so the id is read off the
|
|
468
|
-
* only thing in the row that carries one - the number link.
|
|
469
|
-
*
|
|
470
|
-
* @param {Element} target
|
|
471
|
-
* @returns {object|null}
|
|
472
|
-
*/
|
|
473
|
-
function invoiceFor(target) {
|
|
474
|
-
const id = target?.querySelector?.('[data-id]')?.dataset?.id
|
|
475
|
-
?? target?.closest?.('[data-id]')?.dataset?.id;
|
|
476
|
-
if (!id) return null;
|
|
477
|
-
return state.filtered.value.find((x) => x.id === id) ?? null;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
const rowMenu = E.contextMenu('#invoices-table', {
|
|
481
|
-
match: 'tbody tr',
|
|
482
|
-
// A row menu that also offered whatever an ancestor had bound would
|
|
483
|
-
// be a different menu depending on where the table was mounted.
|
|
484
|
-
inherit: false,
|
|
485
|
-
items: (target) => {
|
|
486
|
-
const inv = invoiceFor(target);
|
|
487
|
-
if (!inv) return [];
|
|
488
|
-
const status = inv._status;
|
|
489
|
-
|
|
490
|
-
const items = [
|
|
491
|
-
{ header: inv.number || 'Invoice' },
|
|
492
|
-
{ label: 'Open', icon: 'edit', action: () => R.navigate(`#/plugins/invoice/edit/${inv.id}`) },
|
|
493
|
-
{ label: 'Print / preview', icon: 'printer', action: () => openPrintWindow(inv.id) },
|
|
494
|
-
{ label: 'Send by email', icon: 'mail',
|
|
495
|
-
action: () => openSendDialog(inv.id, replace) }
|
|
496
|
-
];
|
|
497
|
-
|
|
498
|
-
if (state.canPdf.value) {
|
|
499
|
-
items.push({ label: 'Download PDF', icon: 'download', action: () => downloadPdf(inv.id) });
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
items.push(
|
|
503
|
-
{ type: 'divider' },
|
|
504
|
-
{ label: 'Export', icon: 'file-text', submenu: [
|
|
505
|
-
{ label: 'CSV', action: () => exportInvoice(inv, 'csv', exportNames(inv)) },
|
|
506
|
-
{ label: 'Excel', action: () => exportInvoice(inv, 'excel', exportNames(inv)) }
|
|
507
|
-
] },
|
|
508
|
-
{ label: 'Duplicate', icon: 'copy', action: () => duplicate(inv) },
|
|
509
|
-
{ type: 'divider' }
|
|
510
|
-
);
|
|
511
|
-
|
|
512
|
-
if (status === 'draft') {
|
|
513
|
-
items.push({ label: 'Mark as sent', icon: 'send', action: () => setStatus(inv, 'sent') });
|
|
514
|
-
}
|
|
515
|
-
if (status !== 'paid' && status !== 'cancelled') {
|
|
516
|
-
items.push({ label: 'Mark as paid', icon: 'check-circle', action: () => setStatus(inv, 'paid') });
|
|
517
|
-
}
|
|
518
|
-
if (status !== 'cancelled') {
|
|
519
|
-
items.push({ label: 'Cancel invoice', icon: 'x-circle', action: () => setStatus(inv, 'cancelled') });
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
items.push(
|
|
523
|
-
{ type: 'divider' },
|
|
524
|
-
inv.archived
|
|
525
|
-
? { label: 'Restore', icon: 'archive', action: () => setArchived(inv, false) }
|
|
526
|
-
: { label: 'Archive', icon: 'archive', action: () => setArchived(inv, true) },
|
|
527
|
-
{ label: 'Delete', icon: 'trash', danger: true, action: () => remove(inv) }
|
|
528
|
-
);
|
|
529
|
-
|
|
530
|
-
return items;
|
|
531
|
-
}
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
// ---- settings -------------------------------------------------------
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* @param {string} [tab] open straight onto a section
|
|
538
|
-
* @returns {void}
|
|
539
|
-
*/
|
|
540
|
-
const openSettings = (tab) => openInvoiceSettings({
|
|
541
|
-
tab,
|
|
542
|
-
// Issuers and receivers are edited in there, and this screen gates
|
|
543
|
-
// New Invoice on both - so it has to hear about it.
|
|
544
|
-
onChange: async () => {
|
|
545
|
-
try {
|
|
546
|
-
const [recRes, issRes] = await Promise.all([
|
|
547
|
-
H.get(`${BASE}/receivers`).catch(() => []),
|
|
548
|
-
H.get(`${BASE}/issuers`).catch(() => [])
|
|
549
|
-
]);
|
|
550
|
-
state.receivers.value = recRes.data ?? recRes ?? [];
|
|
551
|
-
state.issuers.value = issRes.data ?? issRes ?? [];
|
|
552
|
-
} catch { /* the list still shows what it had */ }
|
|
553
|
-
}
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
// The cog belongs in the admin's plugin banner, which this view does
|
|
557
|
-
// not own - so the helper is imported at RUN time and its absence is
|
|
558
|
-
// survivable. An older admin simply has no banner to put it in, and the
|
|
559
|
-
// fallback puts it in the toolbar instead of losing the screen.
|
|
560
|
-
try {
|
|
561
|
-
const chrome = await import('/admin/js/lib/plugin-chrome.js');
|
|
562
|
-
// Help first, settings second: they read left to right, and the
|
|
563
|
-
// one you need when you are lost should not be the further away.
|
|
564
|
-
chrome.addBannerAction?.($container, {
|
|
565
|
-
id: 'invoice-help',
|
|
566
|
-
icon: 'help-circle',
|
|
567
|
-
title: 'How invoicing works',
|
|
568
|
-
onClick: () => openHelpPanel(INVOICE_HELP)
|
|
569
|
-
});
|
|
570
|
-
const placed = chrome.addBannerAction?.($container, {
|
|
571
|
-
id: 'invoice-settings',
|
|
572
|
-
icon: 'settings',
|
|
573
|
-
title: 'Invoice settings',
|
|
574
|
-
onClick: () => openSettings()
|
|
575
|
-
});
|
|
576
|
-
if (!placed) throw new Error('no banner');
|
|
577
|
-
} catch {
|
|
578
|
-
const toolbar = $container.find('.invoice-toolbar-row').get(0);
|
|
579
|
-
if (toolbar) {
|
|
580
|
-
const cog = document.createElement('button');
|
|
581
|
-
cog.type = 'button';
|
|
582
|
-
cog.className = 'btn btn-sm btn-ghost';
|
|
583
|
-
cog.title = 'Invoice settings';
|
|
584
|
-
const glyph = document.createElement('span');
|
|
585
|
-
glyph.setAttribute('data-icon', 'settings');
|
|
586
|
-
glyph.setAttribute('data-icon-size', '16');
|
|
587
|
-
cog.appendChild(glyph);
|
|
588
|
-
cog.addEventListener('click', () => openSettings());
|
|
589
|
-
toolbar.appendChild(cog);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
const handle = M.applyBindings(state, $container.get(0));
|
|
594
|
-
|
|
595
|
-
// ---- data -----------------------------------------------------------
|
|
596
|
-
|
|
597
|
-
try {
|
|
598
|
-
const [invRes, recRes, issRes] = await Promise.all([
|
|
599
|
-
H.get(`${BASE}/invoices`),
|
|
600
|
-
H.get(`${BASE}/receivers`).catch(() => []),
|
|
601
|
-
H.get(`${BASE}/issuers`).catch(() => [])
|
|
602
|
-
]);
|
|
603
|
-
state.receivers.value = recRes.data ?? recRes ?? [];
|
|
604
|
-
state.issuers.value = issRes.data ?? issRes ?? [];
|
|
605
|
-
state.invoices.value = invRes.data ?? invRes ?? [];
|
|
606
|
-
} catch {
|
|
607
|
-
E.toast('Could not load invoices.', { type: 'error' });
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// Only offered where the host can actually render one; a menu item that
|
|
611
|
-
// always answers "not installed" is worse than no menu item.
|
|
612
|
-
canDownloadPdf().then((ok) => { state.canPdf.value = ok; });
|
|
613
|
-
|
|
614
|
-
// The same calendar the editor uses, on the two range filters. Each
|
|
615
|
-
// bounds the other, so the range cannot be typed backwards.
|
|
616
|
-
const fromPicker = E.datePicker($container.find('#filter-from').get(0), {
|
|
617
|
-
placeholder: 'Any',
|
|
618
|
-
onChange: (iso) => toPicker?.setOptions({min: iso || null})
|
|
619
|
-
});
|
|
620
|
-
const toPicker = E.datePicker($container.find('#filter-to').get(0), {
|
|
621
|
-
placeholder: 'Any',
|
|
622
|
-
onChange: (iso) => fromPicker?.setOptions({max: iso || null})
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
clearPickers = () => {
|
|
626
|
-
fromPicker?.setValue('', {silent: true});
|
|
627
|
-
toPicker?.setValue('', {silent: true});
|
|
628
|
-
fromPicker?.setOptions({max: null});
|
|
629
|
-
toPicker?.setOptions({min: null});
|
|
630
|
-
};
|
|
631
|
-
|
|
632
|
-
disposeOnLeave(tableEl, stopFeed, stopSort, handle,
|
|
633
|
-
() => rowMenu?.destroy?.(),
|
|
634
|
-
() => fromPicker?.destroy?.(),
|
|
635
|
-
() => toPicker?.destroy?.());
|
|
636
|
-
|
|
637
|
-
Domma.icons.scan($container.get(0));
|
|
638
|
-
// After the bindings, so a control inside a data-if that has just been
|
|
639
|
-
// put into the document is wired too.
|
|
640
|
-
M.flush();
|
|
641
|
-
scanHelp($container.get(0));
|
|
642
|
-
}
|
|
643
|
-
};
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Invoice plugin - issuers screen.
|
|
3
|
-
*
|
|
4
|
-
* The same panel the settings slideover puts in its Issuers tab. This route
|
|
5
|
-
* stays because the sidebar and old bookmarks point at it, and because a
|
|
6
|
-
* fifteen-field issuer form is more comfortable on a full screen than in a
|
|
7
|
-
* slideover.
|
|
8
|
-
*/
|
|
9
|
-
import {ISSUER_FIELDS, mountPartyPanel} from './party-view.js?v=8';
|
|
10
|
-
import {disposeOnLeave} from './view-lifecycle.js?v=8';
|
|
11
|
-
|
|
12
|
-
export const issuersView = {
|
|
13
|
-
templateUrl: '/plugins/invoice/admin/templates/issuers.html',
|
|
14
|
-
|
|
15
|
-
onMount($container) {
|
|
16
|
-
const host = $container.find('#party-host').get(0);
|
|
17
|
-
if (!host) return;
|
|
18
|
-
|
|
19
|
-
const panel = mountPartyPanel(host, {
|
|
20
|
-
kind: 'issuer',
|
|
21
|
-
endpoint: '/api/plugins/invoice/issuers',
|
|
22
|
-
label: 'Issuer',
|
|
23
|
-
intro: 'An issuer is you - a trading name, its address, VAT number and the account an invoice is paid into. Add one profile per business you invoice as.',
|
|
24
|
-
fields: ISSUER_FIELDS
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
disposeOnLeave(host, panel.dispose);
|
|
28
|
-
Domma.icons.scan($container.get(0));
|
|
29
|
-
}
|
|
30
|
-
};
|