domma-cms 0.55.1 → 0.69.3
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 +194 -3
- package/README.md +18 -14
- package/admin/css/admin.css +1 -1
- package/admin/css/contacts.css +1 -0
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- 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/page-picker.js +1 -0
- package/admin/js/lib/plugin-accent.js +1 -0
- package/admin/js/lib/plugin-chrome.js +1 -0
- package/admin/js/lib/shortcode-context-menu.js +2 -2
- 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 +4 -4
- package/admin/js/lib/slideover-resizable.js +1 -0
- 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/context-menu-editor.html +212 -0
- package/admin/js/templates/context-menus.html +16 -0
- package/admin/js/templates/notes.html +137 -0
- package/admin/js/templates/plugin-code.html +1 -1
- package/admin/js/templates/plugin-marketplace.html +17 -4
- package/admin/js/templates/plugins.html +13 -7
- package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
- package/admin/js/templates/settings.html +16 -97
- package/admin/js/templates/theme.html +211 -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/context-menu-editor.js +55 -0
- package/admin/js/views/context-menus.js +5 -0
- package/admin/js/views/form-editor.js +7 -7
- 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 +28 -24
- package/admin/js/views/search.js +1 -0
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/theme.js +1 -0
- package/admin/js/views/todo.js +96 -0
- package/bin/cli.js +6 -10
- package/bin/lib/plugin-version.js +28 -0
- package/bin/update.js +27 -2
- package/config/menus/admin-sidebar.json +129 -23
- package/config/plugins.json +5 -5
- package/config/search.json +13 -0
- package/config/server.json +3 -1
- package/package.json +14 -10
- package/plugins/_lib/admin/mail/compose-window.js +914 -0
- package/plugins/_lib/admin/mail/contacts.js +301 -0
- package/plugins/_lib/admin/mail/diagnostics-section.js +133 -0
- package/plugins/_lib/admin/mail/folder-tree.js +254 -0
- package/plugins/_lib/admin/mail/identity.js +480 -0
- package/plugins/_lib/admin/mail/image-senders-section.js +131 -0
- package/plugins/_lib/admin/mail/keyboard.js +136 -0
- package/plugins/_lib/admin/mail/mail.css +1 -0
- package/plugins/_lib/admin/mail/mail.html +71 -0
- package/plugins/_lib/admin/mail/panes.js +253 -0
- package/plugins/_lib/admin/mail/reader-view.js +4473 -0
- package/plugins/_lib/admin/mail/resizable.js +26 -0
- package/plugins/_lib/admin/mail/rules.js +343 -0
- package/plugins/_lib/admin/mail/scheduling.js +203 -0
- package/plugins/_lib/admin/mail/section-kit.js +277 -0
- package/plugins/_lib/admin/mail/templates.js +238 -0
- package/plugins/_lib/admin/mail/threads.js +200 -0
- package/plugins/_lib/admin/mail/vacation.js +269 -0
- package/plugins/_lib/admin/ui/help.css +1 -0
- package/plugins/_lib/admin/ui/help.js +174 -0
- package/plugins/_lib/admin/ui/resizable.js +151 -0
- package/plugins/_lib/dataStore.js +117 -0
- package/plugins/_lib/mail/accounts.js +919 -0
- package/plugins/_lib/mail/bodyTokens.js +101 -0
- package/plugins/_lib/mail/compose.js +256 -0
- package/plugins/_lib/mail/defaults.js +57 -0
- package/plugins/_lib/mail/diagnostics.js +154 -0
- package/plugins/_lib/mail/envelope.js +161 -0
- package/plugins/_lib/mail/folders.js +192 -0
- package/plugins/_lib/mail/handoff.js +274 -0
- package/plugins/_lib/mail/imapPool.js +293 -0
- package/plugins/_lib/mail/mbox.js +74 -0
- package/plugins/_lib/mail/pollSchedule.js +79 -0
- package/plugins/_lib/mail/poller.js +135 -0
- package/plugins/_lib/mail/priority.js +138 -0
- package/plugins/_lib/mail/readRoutes.js +680 -0
- package/plugins/_lib/mail/render.js +291 -0
- package/plugins/_lib/mail/ruleRunner.js +152 -0
- package/plugins/_lib/mail/scheduler.js +254 -0
- package/plugins/_lib/mail/secretbox.js +229 -0
- package/plugins/_lib/mail/send.js +396 -0
- package/plugins/_lib/mail/store.js +1022 -0
- package/plugins/_lib/mail/sync.js +292 -0
- package/plugins/_lib/mail/syncPlan.js +126 -0
- package/plugins/_lib/mail/syncSelection.js +82 -0
- package/plugins/_lib/mail/unsubscribe.js +183 -0
- package/plugins/_lib/mail/vacationRunner.js +114 -0
- package/plugins/_lib/mail/write.js +473 -0
- package/plugins/_lib/schemaSync.js +83 -0
- package/plugins/_template/admin/css/index.css +0 -0
- package/plugins/_template/admin/templates/index.html +4 -4
- package/plugins/_template/admin/views/index.js +7 -0
- package/plugins/blog/CLAUDE.md +237 -0
- package/plugins/blog/admin/css/index.css +178 -0
- package/plugins/blog/admin/templates/blog.html +35 -41
- 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 +63 -40
- package/plugins/blog/admin/templates/settings.html +3 -9
- package/plugins/blog/admin/views/blog.js +288 -159
- package/plugins/blog/admin/views/categories.js +212 -206
- 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 +306 -39
- package/plugins/blog/admin/views/settings-cog.js +56 -0
- package/plugins/blog/admin/views/settings.js +16 -84
- 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/categories/schema.json +7 -6
- package/plugins/blog/collections/comments/schema.json +11 -10
- package/plugins/blog/collections/posts/schema.json +16 -13
- 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 +259 -26
- package/plugins/blog/plugin.json +21 -73
- package/plugins/blog/plugin.public.js +217 -0
- 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/admin/views/mail.js +19 -0
- package/plugins/mail-reader/config.js +7 -0
- package/plugins/mail-reader/plugin.js +48 -0
- package/plugins/mail-reader/plugin.json +34 -0
- package/public/css/forms.css +1 -1
- package/public/css/search.css +1 -0
- package/public/css/site.css +1 -1
- package/public/css/theme-switcher.css +1 -0
- package/public/js/analytics.js +1 -0
- package/public/js/collection-context.js +2 -2
- package/public/js/context-menus.js +1 -0
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/forms.js +2 -2
- package/public/js/search.js +1 -0
- package/public/js/site.js +1 -1
- package/public/js/theme-switcher.js +1 -0
- package/scripts/build.js +77 -4
- package/scripts/copy-domma.js +48 -0
- package/scripts/run-tests.mjs +92 -0
- package/scripts/seed.js +1996 -0
- package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
- package/server/routes/api/collections.js +34 -0
- package/server/routes/api/contacts.js +506 -0
- package/server/routes/api/context-menus.js +104 -0
- package/server/routes/api/dashboard.js +8 -2
- package/server/routes/api/forms.js +42 -3
- package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
- package/server/routes/api/notifications.js +69 -19
- package/server/routes/api/plugin-marketplace.js +78 -23
- package/server/routes/api/plugins.js +198 -9
- package/server/routes/api/search.js +43 -0
- package/server/routes/api/theme.js +69 -0
- package/server/routes/api/todo.js +178 -0
- package/server/routes/public.js +42 -7
- package/server/server.js +135 -1
- package/server/services/adapters/FileAdapter.js +6 -1
- package/server/services/collections.js +13 -3
- package/server/services/content.js +26 -0
- package/server/services/contextMenus.js +477 -0
- package/server/services/managerClient.js +72 -136
- package/server/services/markdown.js +124 -10
- package/server/services/permissionRegistry.js +74 -0
- package/server/services/pluginEntitlement.js +171 -0
- package/server/services/pluginEnvelope.js +242 -0
- package/server/services/pluginFiles.js +52 -11
- package/server/services/pluginInstaller.js +246 -26
- package/server/services/pluginScaffold.js +5 -0
- package/server/services/plugins.js +340 -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 +224 -22
- package/server/services/roles.js +1 -1
- package/server/services/search-migration.js +82 -0
- package/server/services/search.js +413 -0
- package/server/services/sidebar-migration.js +1 -0
- package/server/services/sidebarBadges.js +37 -0
- package/server/services/siteGitignore.js +307 -0
- package/server/services/themeSettings.js +580 -0
- package/server/services/users.js +8 -0
- package/server/templates/page.html +4 -2
- package/plugins/analytics/admin/templates/analytics.html +0 -61
- package/plugins/analytics/admin/views/analytics.js +0 -176
- package/plugins/analytics/config.js +0 -14
- package/plugins/analytics/plugin.json +0 -57
- 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 -126
- package/plugins/contacts/admin/views/contacts.js +0 -710
- package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
- package/plugins/contacts/collections/user-contacts/schema.json +0 -71
- package/plugins/contacts/config.js +0 -6
- package/plugins/contacts/data/contacts.json +0 -20
- package/plugins/contacts/plugin.js +0 -291
- package/plugins/contacts/plugin.json +0 -33
- 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/templates/editor.html +0 -129
- package/plugins/invoice/admin/templates/index.html +0 -43
- package/plugins/invoice/admin/templates/issuers.html +0 -5
- package/plugins/invoice/admin/templates/receivers.html +0 -5
- package/plugins/invoice/admin/views/editor.js +0 -267
- package/plugins/invoice/admin/views/index.js +0 -155
- package/plugins/invoice/admin/views/issuers.js +0 -23
- package/plugins/invoice/admin/views/party-view.js +0 -148
- package/plugins/invoice/admin/views/receivers.js +0 -22
- package/plugins/invoice/collections/invoice-issuers/schema.json +0 -16
- package/plugins/invoice/collections/invoice-receivers/schema.json +0 -15
- package/plugins/invoice/collections/invoices/schema.json +0 -27
- package/plugins/invoice/config.js +0 -16
- package/plugins/invoice/plugin.js +0 -283
- package/plugins/invoice/plugin.json +0 -85
- package/plugins/invoice/templates/invoice-print.html +0 -213
- 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/data/notes.json +0 -1
- package/plugins/notes/plugin.json +0 -33
- package/plugins/site-search/admin/views/site-search.js +0 -116
- package/plugins/site-search/config.js +0 -15
- package/plugins/site-search/plugin.js +0 -188
- package/plugins/site-search/plugin.json +0 -40
- package/plugins/site-search/public/inject-body.html +0 -17
- package/plugins/site-search/public/inject-head.html +0 -1
- package/plugins/site-search/public/search.css +0 -1
- package/plugins/site-search/public/search.js +0 -1
- 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/plugin.js +0 -174
- package/plugins/surveys/plugin.json +0 -65
- 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/data/todos.json +0 -1
- package/plugins/todo/plugin.js +0 -102
- package/plugins/todo/plugin.json +0 -33
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "invoice-issuers",
|
|
3
|
-
"label": "Invoice Issuers",
|
|
4
|
-
"slug": "invoice-issuers",
|
|
5
|
-
"storage": { "adapter": "file" },
|
|
6
|
-
"fields": [
|
|
7
|
-
{ "key": "name", "label": "Trading Name", "type": "text", "required": true },
|
|
8
|
-
{ "key": "email", "label": "Email", "type": "text" },
|
|
9
|
-
{ "key": "phone", "label": "Phone", "type": "text" },
|
|
10
|
-
{ "key": "website", "label": "Website", "type": "text" },
|
|
11
|
-
{ "key": "address", "label": "Address", "type": "textarea" },
|
|
12
|
-
{ "key": "vatNumber", "label": "VAT Number", "type": "text" },
|
|
13
|
-
{ "key": "bankDetails", "label": "Bank Details", "type": "textarea" },
|
|
14
|
-
{ "key": "logoUrl", "label": "Logo URL", "type": "text" }
|
|
15
|
-
]
|
|
16
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "invoice-receivers",
|
|
3
|
-
"label": "Invoice Receivers",
|
|
4
|
-
"slug": "invoice-receivers",
|
|
5
|
-
"storage": { "adapter": "file" },
|
|
6
|
-
"fields": [
|
|
7
|
-
{ "key": "name", "label": "Name", "type": "text", "required": true },
|
|
8
|
-
{ "key": "company", "label": "Company", "type": "text" },
|
|
9
|
-
{ "key": "email", "label": "Email", "type": "text" },
|
|
10
|
-
{ "key": "phone", "label": "Phone", "type": "text" },
|
|
11
|
-
{ "key": "address", "label": "Address", "type": "textarea" },
|
|
12
|
-
{ "key": "vatNumber", "label": "VAT Number", "type": "text" },
|
|
13
|
-
{ "key": "reference", "label": "Reference", "type": "text" }
|
|
14
|
-
]
|
|
15
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "invoices",
|
|
3
|
-
"label": "Invoices",
|
|
4
|
-
"slug": "invoices",
|
|
5
|
-
"storage": { "adapter": "file" },
|
|
6
|
-
"fields": [
|
|
7
|
-
{ "key": "number", "label": "Number", "type": "text", "required": true },
|
|
8
|
-
{ "key": "issuerId", "label": "Issuer", "type": "text", "required": true },
|
|
9
|
-
{ "key": "receiverId", "label": "Receiver", "type": "text", "required": true },
|
|
10
|
-
{ "key": "issueDate", "label": "Issue Date", "type": "date", "required": true },
|
|
11
|
-
{ "key": "dueDate", "label": "Due Date", "type": "date" },
|
|
12
|
-
{ "key": "status", "label": "Status", "type": "select", "required": true,
|
|
13
|
-
"options": [
|
|
14
|
-
{ "value": "draft", "label": "Draft" },
|
|
15
|
-
{ "value": "sent", "label": "Sent" },
|
|
16
|
-
{ "value": "paid", "label": "Paid" },
|
|
17
|
-
{ "value": "overdue", "label": "Overdue" },
|
|
18
|
-
{ "value": "cancelled", "label": "Cancelled" }
|
|
19
|
-
]
|
|
20
|
-
},
|
|
21
|
-
{ "key": "currency", "label": "Currency", "type": "text" },
|
|
22
|
-
{ "key": "vatEnabled", "label": "VAT Enabled", "type": "boolean" },
|
|
23
|
-
{ "key": "vatRate", "label": "VAT Rate %", "type": "number" },
|
|
24
|
-
{ "key": "lineItems", "label": "Line Items", "type": "json" },
|
|
25
|
-
{ "key": "notes", "label": "Notes", "type": "textarea" }
|
|
26
|
-
]
|
|
27
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Invoice Plugin - Default Configuration
|
|
3
|
-
*
|
|
4
|
-
* numberPrefix used in generated invoice numbers (e.g. INV-2026-0001)
|
|
5
|
-
* numberPadding minimum digits for the sequential portion
|
|
6
|
-
* defaultVatRate applied when VAT is enabled and no rate is set on the invoice
|
|
7
|
-
* defaultCurrency ISO 4217 code; controls formatting + symbol
|
|
8
|
-
* footerNote optional small print appended to every printed invoice
|
|
9
|
-
*/
|
|
10
|
-
export default {
|
|
11
|
-
numberPrefix: 'INV',
|
|
12
|
-
numberPadding: 4,
|
|
13
|
-
defaultVatRate: 20,
|
|
14
|
-
defaultCurrency: 'GBP',
|
|
15
|
-
footerNote: 'Thank you for your business.'
|
|
16
|
-
};
|
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Invoice Plugin - Server
|
|
3
|
-
*
|
|
4
|
-
* Endpoints (mounted at /api/plugins/invoice):
|
|
5
|
-
* GET /issuers admin list issuers
|
|
6
|
-
* POST /issuers admin create issuer
|
|
7
|
-
* PUT /issuers/:id admin update issuer
|
|
8
|
-
* DELETE /issuers/:id admin delete issuer
|
|
9
|
-
*
|
|
10
|
-
* GET /receivers admin list receivers
|
|
11
|
-
* POST /receivers admin create receiver
|
|
12
|
-
* PUT /receivers/:id admin update receiver
|
|
13
|
-
* DELETE /receivers/:id admin delete receiver
|
|
14
|
-
*
|
|
15
|
-
* GET /invoices admin list invoices (with computed totals)
|
|
16
|
-
* GET /invoices/:id admin single invoice with computed totals
|
|
17
|
-
* POST /invoices admin create invoice (auto-numbers if number omitted)
|
|
18
|
-
* PUT /invoices/:id admin update invoice
|
|
19
|
-
* DELETE /invoices/:id admin delete invoice
|
|
20
|
-
* GET /invoices/:id/print admin printable HTML (browser to PDF)
|
|
21
|
-
*/
|
|
22
|
-
import path from 'path';
|
|
23
|
-
import fs from 'fs/promises';
|
|
24
|
-
import {fileURLToPath} from 'url';
|
|
25
|
-
|
|
26
|
-
import defaultConfig from './config.js';
|
|
27
|
-
import {createEntry, deleteEntry, getEntry, listEntries, updateEntry} from '../../server/services/collections.js';
|
|
28
|
-
|
|
29
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
30
|
-
const TEMPLATE_PATH = path.join(__dirname, 'templates', 'invoice-print.html');
|
|
31
|
-
|
|
32
|
-
const ISSUERS_SLUG = 'invoice-issuers';
|
|
33
|
-
const RECEIVERS_SLUG = 'invoice-receivers';
|
|
34
|
-
const INVOICES_SLUG = 'invoices';
|
|
35
|
-
|
|
36
|
-
function escapeHtml(str) {
|
|
37
|
-
return String(str ?? '')
|
|
38
|
-
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
39
|
-
.replace(/"/g, '"').replace(/'/g, ''');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function flattenList(result) {
|
|
43
|
-
const arr = Array.isArray(result) ? result : (result?.entries || []);
|
|
44
|
-
return arr.map((e) => ({ id: e.id, ...(e.data ?? e) }));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function toRecord(entry) {
|
|
48
|
-
return { id: entry.id, ...(entry.data ?? entry) };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function round2(n) {
|
|
52
|
-
return Math.round((Number(n) + Number.EPSILON) * 100) / 100;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function computeTotals(invoice) {
|
|
56
|
-
const items = Array.isArray(invoice.lineItems) ? invoice.lineItems : [];
|
|
57
|
-
let subtotal = 0;
|
|
58
|
-
for (const li of items) {
|
|
59
|
-
const qty = Number(li.quantity) || 0;
|
|
60
|
-
const price = Number(li.unitPrice) || 0;
|
|
61
|
-
subtotal += qty * price;
|
|
62
|
-
}
|
|
63
|
-
subtotal = round2(subtotal);
|
|
64
|
-
const vatRate = invoice.vatEnabled ? (Number(invoice.vatRate) || 0) : 0;
|
|
65
|
-
const vat = round2(subtotal * (vatRate / 100));
|
|
66
|
-
const total = round2(subtotal + vat);
|
|
67
|
-
return { subtotal, vat, total, vatRate };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function withTotals(invoice) {
|
|
71
|
-
return { ...invoice, totals: computeTotals(invoice) };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function formatMoney(amount, currency = 'GBP') {
|
|
75
|
-
try {
|
|
76
|
-
return new Intl.NumberFormat('en-GB', { style: 'currency', currency }).format(Number(amount) || 0);
|
|
77
|
-
} catch {
|
|
78
|
-
return `${currency} ${Number(amount).toFixed(2)}`;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function formatDate(iso) {
|
|
83
|
-
if (!iso) return '';
|
|
84
|
-
return new Date(iso).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function nextInvoiceNumber(prefix, padding) {
|
|
88
|
-
const year = new Date().getFullYear();
|
|
89
|
-
const all = flattenList(await listEntries(INVOICES_SLUG, { limit: 100000 }));
|
|
90
|
-
const re = new RegExp(`^${prefix}-${year}-(\\d+)$`);
|
|
91
|
-
let max = 0;
|
|
92
|
-
for (const inv of all) {
|
|
93
|
-
const m = re.exec(String(inv.number || ''));
|
|
94
|
-
if (m) {
|
|
95
|
-
const n = parseInt(m[1], 10);
|
|
96
|
-
if (n > max) max = n;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return `${prefix}-${year}-${String(max + 1).padStart(padding, '0')}`;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function loadPrintTemplate() {
|
|
103
|
-
return fs.readFile(TEMPLATE_PATH, 'utf8');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function fillTemplate(template, vars) {
|
|
107
|
-
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
108
|
-
const v = vars[key];
|
|
109
|
-
return v === undefined || v === null ? '' : String(v);
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export default async function invoicePlugin(fastify, options) {
|
|
114
|
-
const { authenticate, requireAdmin } = options.auth;
|
|
115
|
-
const settings = { ...defaultConfig, ...(options.settings || {}) };
|
|
116
|
-
const adminGuard = { preHandler: [authenticate, requireAdmin] };
|
|
117
|
-
|
|
118
|
-
// Issuers
|
|
119
|
-
fastify.get('/issuers', adminGuard, async () => {
|
|
120
|
-
return flattenList(await listEntries(ISSUERS_SLUG, { limit: 1000 }));
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
fastify.post('/issuers', adminGuard, async (request, reply) => {
|
|
124
|
-
const body = request.body ?? {};
|
|
125
|
-
if (!body.name || typeof body.name !== 'string' || !body.name.trim()) {
|
|
126
|
-
return reply.code(400).send({ error: 'name is required' });
|
|
127
|
-
}
|
|
128
|
-
const entry = await createEntry(ISSUERS_SLUG, body, { source: 'admin' });
|
|
129
|
-
return reply.code(201).send(toRecord(entry));
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
fastify.put('/issuers/:id', adminGuard, async (request, reply) => {
|
|
133
|
-
const existing = await getEntry(ISSUERS_SLUG, request.params.id);
|
|
134
|
-
if (!existing) return reply.code(404).send({ error: 'Issuer not found' });
|
|
135
|
-
const updated = await updateEntry(ISSUERS_SLUG, request.params.id, { ...existing.data, ...(request.body ?? {}) });
|
|
136
|
-
return toRecord(updated);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
fastify.delete('/issuers/:id', adminGuard, async (request, reply) => {
|
|
140
|
-
const existing = await getEntry(ISSUERS_SLUG, request.params.id);
|
|
141
|
-
if (!existing) return reply.code(404).send({ error: 'Issuer not found' });
|
|
142
|
-
await deleteEntry(ISSUERS_SLUG, request.params.id);
|
|
143
|
-
return { ok: true };
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// Receivers
|
|
147
|
-
fastify.get('/receivers', adminGuard, async () => {
|
|
148
|
-
return flattenList(await listEntries(RECEIVERS_SLUG, { limit: 1000 }));
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
fastify.post('/receivers', adminGuard, async (request, reply) => {
|
|
152
|
-
const body = request.body ?? {};
|
|
153
|
-
if (!body.name || typeof body.name !== 'string' || !body.name.trim()) {
|
|
154
|
-
return reply.code(400).send({ error: 'name is required' });
|
|
155
|
-
}
|
|
156
|
-
const entry = await createEntry(RECEIVERS_SLUG, body, { source: 'admin' });
|
|
157
|
-
return reply.code(201).send(toRecord(entry));
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
fastify.put('/receivers/:id', adminGuard, async (request, reply) => {
|
|
161
|
-
const existing = await getEntry(RECEIVERS_SLUG, request.params.id);
|
|
162
|
-
if (!existing) return reply.code(404).send({ error: 'Receiver not found' });
|
|
163
|
-
const updated = await updateEntry(RECEIVERS_SLUG, request.params.id, { ...existing.data, ...(request.body ?? {}) });
|
|
164
|
-
return toRecord(updated);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
fastify.delete('/receivers/:id', adminGuard, async (request, reply) => {
|
|
168
|
-
const existing = await getEntry(RECEIVERS_SLUG, request.params.id);
|
|
169
|
-
if (!existing) return reply.code(404).send({ error: 'Receiver not found' });
|
|
170
|
-
await deleteEntry(RECEIVERS_SLUG, request.params.id);
|
|
171
|
-
return { ok: true };
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// Invoices
|
|
175
|
-
fastify.get('/invoices', adminGuard, async () => {
|
|
176
|
-
const list = flattenList(await listEntries(INVOICES_SLUG, { limit: 100000, sort: 'createdAt', order: 'desc' }));
|
|
177
|
-
return list.map(withTotals);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
fastify.get('/invoices/:id', adminGuard, async (request, reply) => {
|
|
181
|
-
const entry = await getEntry(INVOICES_SLUG, request.params.id);
|
|
182
|
-
if (!entry) return reply.code(404).send({ error: 'Invoice not found' });
|
|
183
|
-
return withTotals(toRecord(entry));
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
fastify.post('/invoices', adminGuard, async (request, reply) => {
|
|
187
|
-
const body = request.body ?? {};
|
|
188
|
-
if (!body.issuerId) return reply.code(400).send({ error: 'issuerId is required' });
|
|
189
|
-
if (!body.receiverId) return reply.code(400).send({ error: 'receiverId is required' });
|
|
190
|
-
|
|
191
|
-
const data = {
|
|
192
|
-
number: body.number || await nextInvoiceNumber(settings.numberPrefix, settings.numberPadding),
|
|
193
|
-
issuerId: String(body.issuerId),
|
|
194
|
-
receiverId: String(body.receiverId),
|
|
195
|
-
issueDate: body.issueDate || new Date().toISOString().slice(0, 10),
|
|
196
|
-
dueDate: body.dueDate || '',
|
|
197
|
-
status: body.status || 'draft',
|
|
198
|
-
currency: body.currency || settings.defaultCurrency,
|
|
199
|
-
vatEnabled: body.vatEnabled !== undefined ? Boolean(body.vatEnabled) : true,
|
|
200
|
-
vatRate: body.vatRate !== undefined ? Number(body.vatRate) : settings.defaultVatRate,
|
|
201
|
-
lineItems: Array.isArray(body.lineItems) ? body.lineItems : [],
|
|
202
|
-
notes: body.notes ?? ''
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
const entry = await createEntry(INVOICES_SLUG, data, { source: 'admin' });
|
|
206
|
-
return reply.code(201).send(withTotals(toRecord(entry)));
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
fastify.put('/invoices/:id', adminGuard, async (request, reply) => {
|
|
210
|
-
const existing = await getEntry(INVOICES_SLUG, request.params.id);
|
|
211
|
-
if (!existing) return reply.code(404).send({ error: 'Invoice not found' });
|
|
212
|
-
const merged = { ...existing.data, ...(request.body ?? {}) };
|
|
213
|
-
if (Array.isArray(request.body?.lineItems)) merged.lineItems = request.body.lineItems;
|
|
214
|
-
const updated = await updateEntry(INVOICES_SLUG, request.params.id, merged);
|
|
215
|
-
return withTotals(toRecord(updated));
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
fastify.delete('/invoices/:id', adminGuard, async (request, reply) => {
|
|
219
|
-
const existing = await getEntry(INVOICES_SLUG, request.params.id);
|
|
220
|
-
if (!existing) return reply.code(404).send({ error: 'Invoice not found' });
|
|
221
|
-
await deleteEntry(INVOICES_SLUG, request.params.id);
|
|
222
|
-
return { ok: true };
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// Print view (HTML; user prints to PDF from browser)
|
|
226
|
-
fastify.get('/invoices/:id/print', adminGuard, async (request, reply) => {
|
|
227
|
-
const entry = await getEntry(INVOICES_SLUG, request.params.id);
|
|
228
|
-
if (!entry) return reply.code(404).send('Invoice not found');
|
|
229
|
-
const invoice = withTotals(toRecord(entry));
|
|
230
|
-
|
|
231
|
-
const [issuerEntry, receiverEntry] = await Promise.all([
|
|
232
|
-
invoice.issuerId ? getEntry(ISSUERS_SLUG, invoice.issuerId) : null,
|
|
233
|
-
invoice.receiverId ? getEntry(RECEIVERS_SLUG, invoice.receiverId) : null
|
|
234
|
-
]);
|
|
235
|
-
const issuer = issuerEntry ? toRecord(issuerEntry) : {};
|
|
236
|
-
const receiver = receiverEntry ? toRecord(receiverEntry) : {};
|
|
237
|
-
|
|
238
|
-
const currency = invoice.currency || settings.defaultCurrency;
|
|
239
|
-
const itemsHtml = (invoice.lineItems || []).map((li) => {
|
|
240
|
-
const qty = Number(li.quantity) || 0;
|
|
241
|
-
const price = Number(li.unitPrice) || 0;
|
|
242
|
-
const lineTotal = round2(qty * price);
|
|
243
|
-
return `<tr>
|
|
244
|
-
<td>${escapeHtml(li.description || '')}</td>
|
|
245
|
-
<td class="num">${qty}</td>
|
|
246
|
-
<td class="num">${formatMoney(price, currency)}</td>
|
|
247
|
-
<td class="num">${formatMoney(lineTotal, currency)}</td>
|
|
248
|
-
</tr>`;
|
|
249
|
-
}).join('');
|
|
250
|
-
|
|
251
|
-
const vatRow = invoice.vatEnabled
|
|
252
|
-
? `<tr><th>VAT (${escapeHtml(String(invoice.totals.vatRate))}%)</th><td class="num">${formatMoney(invoice.totals.vat, currency)}</td></tr>`
|
|
253
|
-
: '';
|
|
254
|
-
|
|
255
|
-
const template = await loadPrintTemplate();
|
|
256
|
-
const html = fillTemplate(template, {
|
|
257
|
-
number: escapeHtml(invoice.number || ''),
|
|
258
|
-
issueDate: escapeHtml(formatDate(invoice.issueDate)),
|
|
259
|
-
dueDate: invoice.dueDate ? `<div><strong>Due:</strong> ${escapeHtml(formatDate(invoice.dueDate))}</div>` : '',
|
|
260
|
-
status: escapeHtml((invoice.status || 'draft').toUpperCase()),
|
|
261
|
-
issuerName: escapeHtml(issuer.name || ''),
|
|
262
|
-
issuerLogo: issuer.logoUrl ? `<img src="${escapeHtml(issuer.logoUrl)}" alt="${escapeHtml(issuer.name || '')}" class="logo">` : '',
|
|
263
|
-
issuerAddress: escapeHtml(issuer.address || '').replace(/\n/g, '<br>'),
|
|
264
|
-
issuerEmail: escapeHtml(issuer.email || ''),
|
|
265
|
-
issuerPhone: escapeHtml(issuer.phone || ''),
|
|
266
|
-
issuerVat: issuer.vatNumber ? `<div>VAT: ${escapeHtml(issuer.vatNumber)}</div>` : '',
|
|
267
|
-
receiverName: escapeHtml(receiver.company || receiver.name || ''),
|
|
268
|
-
receiverAddress: escapeHtml(receiver.address || '').replace(/\n/g, '<br>'),
|
|
269
|
-
receiverEmail: escapeHtml(receiver.email || ''),
|
|
270
|
-
receiverVat: receiver.vatNumber ? `<div>VAT: ${escapeHtml(receiver.vatNumber)}</div>` : '',
|
|
271
|
-
items: itemsHtml || '<tr><td colspan="4" class="muted">No line items.</td></tr>',
|
|
272
|
-
subtotal: formatMoney(invoice.totals.subtotal, currency),
|
|
273
|
-
vatRow,
|
|
274
|
-
total: formatMoney(invoice.totals.total, currency),
|
|
275
|
-
notes: invoice.notes ? `<div class="notes"><strong>Notes</strong><br>${escapeHtml(invoice.notes).replace(/\n/g, '<br>')}</div>` : '',
|
|
276
|
-
bankDetails: issuer.bankDetails ? `<div class="bank"><strong>Payment</strong><br>${escapeHtml(issuer.bankDetails).replace(/\n/g, '<br>')}</div>` : '',
|
|
277
|
-
footerNote: escapeHtml(settings.footerNote || ''),
|
|
278
|
-
autoPrint: request.query.print === '1' ? '<script>window.addEventListener("load",()=>setTimeout(()=>window.print(),200));</script>' : ''
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
reply.type('text/html').send(html);
|
|
282
|
-
});
|
|
283
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "invoice",
|
|
3
|
-
"displayName": "Invoices",
|
|
4
|
-
"version": "1.0.0",
|
|
5
|
-
"description": "Lightweight invoice generator. Manage issuers, receivers, line items, optional VAT, and produce a print-ready invoice you can save as PDF from the browser.",
|
|
6
|
-
"author": "Darryl Waterhouse",
|
|
7
|
-
"date": "2026-05-04",
|
|
8
|
-
"icon": "file-text",
|
|
9
|
-
"admin": {
|
|
10
|
-
"css": [
|
|
11
|
-
"admin/css/index.css"
|
|
12
|
-
],
|
|
13
|
-
"sidebar": [
|
|
14
|
-
{
|
|
15
|
-
"id": "invoice",
|
|
16
|
-
"text": "Invoices",
|
|
17
|
-
"icon": "file-text",
|
|
18
|
-
"url": "#/plugins/invoice",
|
|
19
|
-
"section": "#/plugins/invoice",
|
|
20
|
-
"children": [
|
|
21
|
-
{
|
|
22
|
-
"id": "invoice-list",
|
|
23
|
-
"text": "Invoices",
|
|
24
|
-
"url": "#/plugins/invoice"
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"id": "invoice-issuers",
|
|
28
|
-
"text": "Issuers",
|
|
29
|
-
"url": "#/plugins/invoice/issuers"
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
"id": "invoice-receivers",
|
|
33
|
-
"text": "Receivers",
|
|
34
|
-
"url": "#/plugins/invoice/receivers"
|
|
35
|
-
}
|
|
36
|
-
]
|
|
37
|
-
}
|
|
38
|
-
],
|
|
39
|
-
"routes": [
|
|
40
|
-
{
|
|
41
|
-
"path": "/plugins/invoice",
|
|
42
|
-
"view": "plugin-invoice-list",
|
|
43
|
-
"title": "Invoices - Domma CMS"
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
"path": "/plugins/invoice/new",
|
|
47
|
-
"view": "plugin-invoice-editor",
|
|
48
|
-
"title": "New Invoice - Domma CMS"
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
"path": "/plugins/invoice/edit/:id",
|
|
52
|
-
"view": "plugin-invoice-editor",
|
|
53
|
-
"title": "Edit Invoice - Domma CMS"
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
"path": "/plugins/invoice/issuers",
|
|
57
|
-
"view": "plugin-invoice-issuers",
|
|
58
|
-
"title": "Issuers - Domma CMS"
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
"path": "/plugins/invoice/receivers",
|
|
62
|
-
"view": "plugin-invoice-receivers",
|
|
63
|
-
"title": "Receivers - Domma CMS"
|
|
64
|
-
}
|
|
65
|
-
],
|
|
66
|
-
"views": {
|
|
67
|
-
"plugin-invoice-list": {
|
|
68
|
-
"entry": "invoice/admin/views/index.js?v=9c70ea80",
|
|
69
|
-
"exportName": "indexView"
|
|
70
|
-
},
|
|
71
|
-
"plugin-invoice-editor": {
|
|
72
|
-
"entry": "invoice/admin/views/editor.js?v=e5e153c1",
|
|
73
|
-
"exportName": "editorView"
|
|
74
|
-
},
|
|
75
|
-
"plugin-invoice-issuers": {
|
|
76
|
-
"entry": "invoice/admin/views/issuers.js?v=ac69a08a",
|
|
77
|
-
"exportName": "issuersView"
|
|
78
|
-
},
|
|
79
|
-
"plugin-invoice-receivers": {
|
|
80
|
-
"entry": "invoice/admin/views/receivers.js?v=f016e1ff",
|
|
81
|
-
"exportName": "receiversView"
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<title>Invoice {{number}}</title>
|
|
6
|
-
<style>
|
|
7
|
-
:root { color-scheme: light; }
|
|
8
|
-
* { box-sizing: border-box; }
|
|
9
|
-
body {
|
|
10
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
11
|
-
color: #222;
|
|
12
|
-
background: #f3f3f3;
|
|
13
|
-
margin: 0;
|
|
14
|
-
padding: 24px;
|
|
15
|
-
font-size: 14px;
|
|
16
|
-
line-height: 1.5;
|
|
17
|
-
}
|
|
18
|
-
.invoice {
|
|
19
|
-
max-width: 800px;
|
|
20
|
-
margin: 0 auto;
|
|
21
|
-
background: #fff;
|
|
22
|
-
padding: 48px 56px;
|
|
23
|
-
box-shadow: 0 4px 20px rgba(0,0,0,.08);
|
|
24
|
-
}
|
|
25
|
-
.top {
|
|
26
|
-
display: flex;
|
|
27
|
-
justify-content: space-between;
|
|
28
|
-
align-items: flex-start;
|
|
29
|
-
gap: 24px;
|
|
30
|
-
border-bottom: 2px solid #e5e5e5;
|
|
31
|
-
padding-bottom: 24px;
|
|
32
|
-
margin-bottom: 32px;
|
|
33
|
-
}
|
|
34
|
-
.logo { max-height: 80px; max-width: 240px; display: block; margin-bottom: 12px; }
|
|
35
|
-
.meta { text-align: right; }
|
|
36
|
-
.meta h1 { margin: 0; font-size: 28px; letter-spacing: 1px; color: #000; }
|
|
37
|
-
.meta .number { font-size: 18px; color: #555; margin-top: 4px; }
|
|
38
|
-
.status {
|
|
39
|
-
display: inline-block;
|
|
40
|
-
padding: 4px 10px;
|
|
41
|
-
border-radius: 4px;
|
|
42
|
-
font-size: 11px;
|
|
43
|
-
font-weight: 600;
|
|
44
|
-
letter-spacing: .5px;
|
|
45
|
-
background: #eee;
|
|
46
|
-
color: #444;
|
|
47
|
-
margin-top: 8px;
|
|
48
|
-
}
|
|
49
|
-
.parties {
|
|
50
|
-
display: grid;
|
|
51
|
-
grid-template-columns: 1fr 1fr;
|
|
52
|
-
gap: 32px;
|
|
53
|
-
margin-bottom: 32px;
|
|
54
|
-
}
|
|
55
|
-
.party h3 {
|
|
56
|
-
margin: 0 0 8px;
|
|
57
|
-
font-size: 11px;
|
|
58
|
-
text-transform: uppercase;
|
|
59
|
-
letter-spacing: 1px;
|
|
60
|
-
color: #888;
|
|
61
|
-
}
|
|
62
|
-
.party .name { font-weight: 700; font-size: 15px; margin-bottom: 4px; }
|
|
63
|
-
.party .lines { color: #555; font-size: 13px; }
|
|
64
|
-
table.items {
|
|
65
|
-
width: 100%;
|
|
66
|
-
border-collapse: collapse;
|
|
67
|
-
margin-bottom: 24px;
|
|
68
|
-
}
|
|
69
|
-
table.items th {
|
|
70
|
-
text-align: left;
|
|
71
|
-
font-size: 11px;
|
|
72
|
-
text-transform: uppercase;
|
|
73
|
-
letter-spacing: 1px;
|
|
74
|
-
color: #888;
|
|
75
|
-
padding: 8px 10px;
|
|
76
|
-
border-bottom: 2px solid #e5e5e5;
|
|
77
|
-
}
|
|
78
|
-
table.items td {
|
|
79
|
-
padding: 10px;
|
|
80
|
-
border-bottom: 1px solid #f0f0f0;
|
|
81
|
-
}
|
|
82
|
-
table.items td.num, table.items th.num { text-align: right; }
|
|
83
|
-
.totals {
|
|
84
|
-
width: 320px;
|
|
85
|
-
margin-left: auto;
|
|
86
|
-
margin-bottom: 32px;
|
|
87
|
-
}
|
|
88
|
-
.totals table { width: 100%; border-collapse: collapse; }
|
|
89
|
-
.totals th {
|
|
90
|
-
text-align: left;
|
|
91
|
-
padding: 8px 10px;
|
|
92
|
-
font-weight: 500;
|
|
93
|
-
color: #555;
|
|
94
|
-
}
|
|
95
|
-
.totals td.num {
|
|
96
|
-
text-align: right;
|
|
97
|
-
padding: 8px 10px;
|
|
98
|
-
font-variant-numeric: tabular-nums;
|
|
99
|
-
}
|
|
100
|
-
.totals .grand th, .totals .grand td {
|
|
101
|
-
font-size: 17px;
|
|
102
|
-
font-weight: 700;
|
|
103
|
-
color: #000;
|
|
104
|
-
border-top: 2px solid #222;
|
|
105
|
-
padding-top: 12px;
|
|
106
|
-
}
|
|
107
|
-
.footer {
|
|
108
|
-
margin-top: 32px;
|
|
109
|
-
padding-top: 16px;
|
|
110
|
-
border-top: 1px solid #e5e5e5;
|
|
111
|
-
font-size: 12px;
|
|
112
|
-
color: #888;
|
|
113
|
-
}
|
|
114
|
-
.notes, .bank {
|
|
115
|
-
margin-top: 24px;
|
|
116
|
-
padding: 12px 16px;
|
|
117
|
-
background: #fafafa;
|
|
118
|
-
border-radius: 4px;
|
|
119
|
-
font-size: 13px;
|
|
120
|
-
}
|
|
121
|
-
.muted { color: #999; font-style: italic; text-align: center; }
|
|
122
|
-
.actions {
|
|
123
|
-
max-width: 800px;
|
|
124
|
-
margin: 0 auto 16px;
|
|
125
|
-
display: flex;
|
|
126
|
-
justify-content: flex-end;
|
|
127
|
-
gap: 8px;
|
|
128
|
-
}
|
|
129
|
-
.actions button {
|
|
130
|
-
background: #222;
|
|
131
|
-
color: #fff;
|
|
132
|
-
border: 0;
|
|
133
|
-
padding: 8px 16px;
|
|
134
|
-
font-size: 13px;
|
|
135
|
-
border-radius: 4px;
|
|
136
|
-
cursor: pointer;
|
|
137
|
-
}
|
|
138
|
-
@media print {
|
|
139
|
-
body { background: #fff; padding: 0; }
|
|
140
|
-
.invoice { box-shadow: none; padding: 24px; }
|
|
141
|
-
.actions { display: none; }
|
|
142
|
-
}
|
|
143
|
-
</style>
|
|
144
|
-
</head>
|
|
145
|
-
<body>
|
|
146
|
-
|
|
147
|
-
<div class="actions">
|
|
148
|
-
<button onclick="window.print()">Print / Save as PDF</button>
|
|
149
|
-
</div>
|
|
150
|
-
|
|
151
|
-
<div class="invoice">
|
|
152
|
-
<div class="top">
|
|
153
|
-
<div>
|
|
154
|
-
{{issuerLogo}}
|
|
155
|
-
<div class="party">
|
|
156
|
-
<div class="name">{{issuerName}}</div>
|
|
157
|
-
<div class="lines">{{issuerAddress}}</div>
|
|
158
|
-
<div class="lines">{{issuerEmail}}</div>
|
|
159
|
-
<div class="lines">{{issuerPhone}}</div>
|
|
160
|
-
{{issuerVat}}
|
|
161
|
-
</div>
|
|
162
|
-
</div>
|
|
163
|
-
<div class="meta">
|
|
164
|
-
<h1>INVOICE</h1>
|
|
165
|
-
<div class="number">{{number}}</div>
|
|
166
|
-
<div>{{issueDate}}</div>
|
|
167
|
-
{{dueDate}}
|
|
168
|
-
<div class="status">{{status}}</div>
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
171
|
-
|
|
172
|
-
<div class="parties">
|
|
173
|
-
<div class="party">
|
|
174
|
-
<h3>Billed To</h3>
|
|
175
|
-
<div class="name">{{receiverName}}</div>
|
|
176
|
-
<div class="lines">{{receiverAddress}}</div>
|
|
177
|
-
<div class="lines">{{receiverEmail}}</div>
|
|
178
|
-
{{receiverVat}}
|
|
179
|
-
</div>
|
|
180
|
-
</div>
|
|
181
|
-
|
|
182
|
-
<table class="items">
|
|
183
|
-
<thead>
|
|
184
|
-
<tr>
|
|
185
|
-
<th>Description</th>
|
|
186
|
-
<th class="num">Qty</th>
|
|
187
|
-
<th class="num">Unit</th>
|
|
188
|
-
<th class="num">Total</th>
|
|
189
|
-
</tr>
|
|
190
|
-
</thead>
|
|
191
|
-
<tbody>
|
|
192
|
-
{{items}}
|
|
193
|
-
</tbody>
|
|
194
|
-
</table>
|
|
195
|
-
|
|
196
|
-
<div class="totals">
|
|
197
|
-
<table>
|
|
198
|
-
<tr><th>Subtotal</th><td class="num">{{subtotal}}</td></tr>
|
|
199
|
-
{{vatRow}}
|
|
200
|
-
<tr class="grand"><th>Total</th><td class="num">{{total}}</td></tr>
|
|
201
|
-
</table>
|
|
202
|
-
</div>
|
|
203
|
-
|
|
204
|
-
{{notes}}
|
|
205
|
-
{{bankDetails}}
|
|
206
|
-
|
|
207
|
-
<div class="footer">{{footerNote}}</div>
|
|
208
|
-
</div>
|
|
209
|
-
|
|
210
|
-
{{autoPrint}}
|
|
211
|
-
|
|
212
|
-
</body>
|
|
213
|
-
</html>
|