domma-cms 1.6.34 → 1.6.36
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/admin/js/api.js +1 -1
- package/admin/js/lib/effects-builder.js +1 -1
- package/admin/js/lib/pages-arrange.js +1 -1
- package/admin/js/lib/scribe-composer.js +1 -1
- package/admin/js/lib/scribe-model.js +1 -1
- package/admin/js/lib/shortcode-preview.js +1 -1
- package/admin/js/lib/shortcode-registry.js +2 -2
- package/admin/js/templates/docs/usage-site-settings.html +2 -1
- package/admin/js/templates/page-editor.html +2 -1
- package/admin/js/templates/pages.html +1 -1
- package/admin/js/templates/theme.html +1 -1
- package/admin/js/views/pages.js +3 -3
- package/admin/js/views/theme.js +5 -5
- package/package.json +1 -1
- package/plugins/_lib/call/ice.js +5 -2
- package/plugins/_lib/call/rooms.js +9 -3
- package/plugins/_lib/call/routes.js +7 -4
- package/plugins/_lib/mail/defaults.js +12 -0
- package/plugins/_lib/mail/hostGuard.js +86 -0
- package/plugins/_lib/mail/readRoutes.js +20 -1
- package/plugins/_lib/outreach/audience.js +76 -0
- package/plugins/_lib/outreach/collections/outreach-contacts.schema.json +12 -0
- package/plugins/_lib/outreach/collections/outreach-groups.schema.json +10 -0
- package/plugins/_lib/outreach/collections/outreach-unsubscribes.schema.json +11 -0
- package/plugins/_lib/outreach/expiry.js +46 -0
- package/plugins/_lib/outreach/index.js +15 -0
- package/plugins/_lib/outreach/ledger.js +127 -0
- package/plugins/_lib/outreach/sending.js +121 -0
- package/plugins/_lib/outreach/suppression.js +62 -0
- package/plugins/_lib/outreach/tokens.js +35 -0
- package/plugins/_lib/outreach/unsubscribe.js +46 -0
- package/plugins/_lib/public/call/call.js +3 -1
- package/plugins/blog/CLAUDE.md +11 -1
- package/plugins/blog/docs/guide.md +1 -1
- package/plugins/blog/lib/page.js +32 -0
- package/plugins/blog/plugin.js +58 -16
- package/plugins/blog/plugin.json +2 -2
- package/plugins/blog/plugin.public.js +5 -4
- package/plugins/blog/tests/public.test.js +30 -1
- package/plugins/booking/CLAUDE.md +4 -1
- package/plugins/booking/docs/guide.md +1 -1
- package/plugins/booking/plugin.js +16 -21
- package/plugins/booking/plugin.json +2 -2
- package/plugins/booking/tests/api.test.js +26 -1
- package/plugins/feedback/CLAUDE.md +7 -2
- package/plugins/feedback/admin/views/feedback.js +1 -1
- package/plugins/feedback/lib/receiver.js +1 -1
- package/plugins/feedback/plugin.json +3 -3
- package/plugins/free-tier.lock.json +63 -51
- package/plugins/mail-reader/CLAUDE.md +6 -0
- package/plugins/mail-reader/docs/guide.md +1 -0
- package/plugins/mail-reader/plugin.json +2 -2
- package/plugins/shopping-cart/CLAUDE.md +62 -1
- package/plugins/shopping-cart/admin/css/index.css +16 -0
- package/plugins/shopping-cart/admin/lib/chart.js +64 -0
- package/plugins/shopping-cart/admin/lib/kit.js +15 -0
- package/plugins/shopping-cart/admin/views/orders.js +1 -0
- package/plugins/shopping-cart/admin/views/overview.js +4 -53
- package/plugins/shopping-cart/admin/views/sales.js +133 -0
- package/plugins/shopping-cart/admin/views/settings.js +3 -1
- package/plugins/shopping-cart/admin/views/shop.js +2 -1
- package/plugins/shopping-cart/config.js +1 -0
- package/plugins/shopping-cart/docs/guide.md +26 -1
- package/plugins/shopping-cart/inject/body.html +2 -2
- package/plugins/shopping-cart/lib/accounts.js +185 -0
- package/plugins/shopping-cart/lib/commission.js +90 -0
- package/plugins/shopping-cart/lib/extensions.js +12 -4
- package/plugins/shopping-cart/lib/payments.js +7 -4
- package/plugins/shopping-cart/lib/render.js +13 -1
- package/plugins/shopping-cart/lib/sales.js +193 -0
- package/plugins/shopping-cart/lib/shop.js +21 -3
- package/plugins/shopping-cart/plugin.js +180 -35
- package/plugins/shopping-cart/plugin.json +3 -3
- package/plugins/shopping-cart/plugin.public.js +17 -1
- package/plugins/shopping-cart/public/account.js +277 -0
- package/plugins/shopping-cart/public/cart.js +16 -0
- package/plugins/shopping-cart/public/lib/session.js +82 -0
- package/plugins/shopping-cart/public/shop.css +27 -0
- package/plugins/shopping-cart/roles/shop-customer.json +8 -0
- package/plugins/shopping-cart/tests/accounts.test.js +136 -0
- package/plugins/shopping-cart/tests/api.test.js +34 -0
- package/plugins/shopping-cart/tests/commission.test.js +90 -0
- package/plugins/shopping-cart/tests/pure.test.js +16 -1
- package/plugins/shopping-cart/tests/sales.test.js +73 -0
- package/plugins/support/CLAUDE.md +6 -0
- package/plugins/support/admin/views/support.js +1 -1
- package/plugins/support/docs/guide.md +90 -0
- package/plugins/support/lib/receiver.js +1 -1
- package/plugins/support/plugin.json +3 -3
- package/public/css/site.css +1 -1
- package/public/js/effects.js +1 -1
- package/public/js/scribe-style.mjs +1 -1
- package/public/js/site.js +1 -1
- package/public/js/text-style.mjs +1 -1
- package/server/routes/api/analytics.js +32 -0
- package/server/routes/api/collections.js +148 -15
- package/server/routes/api/theme.js +12 -2
- package/server/routes/outreach-public.js +68 -0
- package/server/server.js +10 -1
- package/server/services/authChecks.js +8 -0
- package/server/services/collectionAccess.js +191 -0
- package/server/services/collections.js +8 -2
- package/server/services/markdown.js +7 -2
- package/server/services/outreachUnsubscribe.js +54 -0
- package/server/services/request-log.js +70 -0
- package/server/templates/page.html +3 -3
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"note": "Written by `make sync-free-tier` in dcms-marketplace. Do not edit these plugins here - edit them there and sync.",
|
|
3
|
-
"source": "dcms-marketplace@
|
|
3
|
+
"source": "dcms-marketplace@b1edb76",
|
|
4
4
|
"plugins": {
|
|
5
5
|
"blog": {
|
|
6
|
-
"version": "1.9.
|
|
6
|
+
"version": "1.9.6",
|
|
7
7
|
"files": {
|
|
8
|
-
"CLAUDE.md": "
|
|
8
|
+
"CLAUDE.md": "5164b58785b979b828a707ac94088d80eb1a1655fc48a23404aeca8cef316e70",
|
|
9
9
|
"admin/css/index.css": "6142a6b756e1218157842528830b1e84f592f0b84b7d5ce6449db421b6f4b039",
|
|
10
10
|
"admin/templates/blog.html": "a6054c1febe764bb1edf5c9e466d44a9080d9c83ba5486437b7a8244acf5ba54",
|
|
11
11
|
"admin/templates/categories.html": "ab7d6fc2fedd48876c585bc216a1532b3bdd78c946dee976c1171bfe4664feb6",
|
|
@@ -41,17 +41,17 @@
|
|
|
41
41
|
"collections/comments/schema.json": "29cbd289cbfcfc4158c9357a218db4ce06eddc3370bfbf9afb7d7f52065a849e",
|
|
42
42
|
"collections/posts/schema.json": "19a5a770d3eefea73f4117454bd1312a7a7e3d727cfcdfde141139fbc81cabd7",
|
|
43
43
|
"config.js": "ee67396262762f5c31269d258affd75f7392a3e364f72f7792c2f1cdafd94bba",
|
|
44
|
-
"docs/guide.md": "
|
|
44
|
+
"docs/guide.md": "d9f70afe4612a2464fc537a0348e987873d09cc30ce44692a22cf79c40a4c0b4",
|
|
45
45
|
"lib/access.js": "af47440c7233a56000641a3231d7e9ff81b12f40dfb9528aa6dd4e2dd9bca8df",
|
|
46
46
|
"lib/layouts.js": "dbb6ae9e514ccaf600a732702925b905131dea166d90fe759a62b6652e78e1de",
|
|
47
47
|
"lib/nav-link.js": "6a8f9368bb8aee1c21e90fb335a691859cb9a9bc250c2b9167a33ca9189fa2ff",
|
|
48
48
|
"lib/notices.js": "a9a2918ee6f5f7e0bbaeced0d3539584279bdbe68d15472a8615bdddb4ef905b",
|
|
49
|
-
"lib/page.js": "
|
|
49
|
+
"lib/page.js": "ca84803672c3815c9606402bd24e9c6bdef167f7a342eb6d3d51a8b9a9c692ba",
|
|
50
50
|
"lib/render.js": "0e97c6a950bc962c8d8ea4b2f21d06cb73fbcdc738d8942d6935d01309a29b1d",
|
|
51
51
|
"lib/samples.js": "e8cdb77ce22e43937707f6854d3a5cb722437dc584e22ac7b24480bef686cf85",
|
|
52
|
-
"plugin.js": "
|
|
53
|
-
"plugin.json": "
|
|
54
|
-
"plugin.public.js": "
|
|
52
|
+
"plugin.js": "278d269eb8fb6df2f64f23ae26b5e5b5fe96e7f9d07241539d261d66d4ccd7f7",
|
|
53
|
+
"plugin.json": "ff7466d42c41dd64117861d4d88914be1d367854ade67cb4ca42d8b42068bcf4",
|
|
54
|
+
"plugin.public.js": "33387fa7636b877dec3881b0698918bc1a7379d5690d010c826aedc4091284d8",
|
|
55
55
|
"public/blog.css": "fd733e6b624190da1e70dc1b6613e2376efd7f7570a28fab311d81006d03d960",
|
|
56
56
|
"public/blog.js": "d01c72a9834dcc7c5241923371f2b164ac30d20ac9f1a5c5dc5b71fa3cb09f08",
|
|
57
57
|
"public/samples/aurora.svg": "feb29e2b46465913fc29de9eda221b00b1541f1fa3ea7120f0c1d1cd11a9a4f6",
|
|
@@ -69,13 +69,13 @@
|
|
|
69
69
|
"tests/layouts.test.js": "554cff8194bd650cfb5f2a83c1f51c25f427c07f7dbfdc2ad9ad69bb38fca6a0",
|
|
70
70
|
"tests/nav-link.test.js": "d36e33ad7483abca27ae6e2098ad2f75bafa18d91f3beba70372ebe6b9b88994",
|
|
71
71
|
"tests/notices.test.js": "2b02aa84e780dc4c82e6c503fc8fe17c64a63bee0709627866ef97c99cb4e131",
|
|
72
|
-
"tests/public.test.js": "
|
|
72
|
+
"tests/public.test.js": "cd4cc53da9709d400d082ef6df01b3f49e6fc419e4b020b588464306a5d907e9"
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
"booking": {
|
|
76
|
-
"version": "1.6.
|
|
76
|
+
"version": "1.6.3",
|
|
77
77
|
"files": {
|
|
78
|
-
"CLAUDE.md": "
|
|
78
|
+
"CLAUDE.md": "1709e58b31a3ac7c1cbadbe6d4f8b0a01cf696b72981ccfa14cd1861133fac85",
|
|
79
79
|
"admin/css/index.css": "4f7731921c0c5038cb1566a6ab2d43ffc35bf9b161e58f87a0cff423e81b47fa",
|
|
80
80
|
"admin/lib/details.js": "8b37536bfa9ccfcc5265cc957ba8266d3f5f777133e7c0946ae49540f1c80e3c",
|
|
81
81
|
"admin/lib/diary-layout.js": "d818c79001fb9a9ecc15c96239ba961b0a1d98b9e8fce903eefb57d85adf2a73",
|
|
@@ -93,14 +93,14 @@
|
|
|
93
93
|
"collections/booking-bookings.json": "fa0b15cc81b4a2114734a0bc8adc4b4d25049d16cf62549b83634cdbeb543b52",
|
|
94
94
|
"collections/booking-services.json": "cecb500a9215a0eba894e6b4c9ae78f5fbeb209830d07d47050c56c75143f66a",
|
|
95
95
|
"config.js": "5565fd627481cf809322257cc300a7cbb7ccbf6cae076b4cf8a7453fcf554bd3",
|
|
96
|
-
"docs/guide.md": "
|
|
96
|
+
"docs/guide.md": "8f5898627abb1b21f90809b4f2e4c29f80d478bd11c0f1aec5319cddc40d14f9",
|
|
97
97
|
"inject/body.html": "ab55ce945eec2302eb0c836a4626cffa5e091d050c6a59c6f2af332b05570777",
|
|
98
98
|
"lib/call.js": "87c11b8531e1a6d247d52d4e03ea8e5aa06a1af49cbd7cbd0af97e2586237f7b",
|
|
99
99
|
"lib/extensions.js": "8cec28ff8685daad8083b23f034f1c3bf82fd2322943dd19d46570ccdc311105",
|
|
100
100
|
"lib/mail.js": "cad8f409df62d6e42f0f4024ec364bbe018a62b842fa3f7c38786fd4e7f92408",
|
|
101
101
|
"lib/store.js": "7aa7967e4694e5a3b85378f60302e5a47394b1d90c7dd86e09578156f1c13e03",
|
|
102
|
-
"plugin.js": "
|
|
103
|
-
"plugin.json": "
|
|
102
|
+
"plugin.js": "4a303855a5a7f746e07e45a937dfeac8984dc9254c2f9b1a60b6f6306912ed84",
|
|
103
|
+
"plugin.json": "d099b136335028b2bca2bd21065c6ae343099f71635f9c793c95567a6e8a63db",
|
|
104
104
|
"plugin.public.js": "4d5b4e8ad6943182ee3b6c6e6f6762a2d3f676d8d7ee363fcb07bd47d3d6532e",
|
|
105
105
|
"public/booking.css": "8b88989fdd04e50efc1688a2f32fe2e422299d7d13b81bb8fce0cc162fa99df6",
|
|
106
106
|
"public/booking.js": "7dfc3d205696967013886567f67a867443c2e370bac29019d69911d695fdbade",
|
|
@@ -110,42 +110,42 @@
|
|
|
110
110
|
"public/lib/time.js": "48e13a23e3d7a4916e2097e8f87c03a87d37b71c4ef037a5a16143c6a4185c41",
|
|
111
111
|
"templates/booking.html": "a4995de46dfdab7e692b56f21347f29c76320cf3b0477889dc3c5d14e0b6dcd7",
|
|
112
112
|
"tests/admin-diary.test.js": "4e34b447d62b41272ceec49bad9d491ae00dd04f4760d8a7a3046be1912f4abe",
|
|
113
|
-
"tests/api.test.js": "
|
|
113
|
+
"tests/api.test.js": "efb3767f7efde08c11c8edcb5753889773b421188798f725aff0e4e533860aff",
|
|
114
114
|
"tests/public.test.js": "0b7b626d60df5a3d73d7c0077b73f59b62a9fded98ce84a8d93cd32cd0333159",
|
|
115
115
|
"tests/pure.test.js": "8ba0c8f76b4c9efcc422f39ff29509612544d81f760025a4b9a5594af14e7bcf"
|
|
116
116
|
}
|
|
117
117
|
},
|
|
118
118
|
"feedback": {
|
|
119
|
-
"version": "1.1.
|
|
119
|
+
"version": "1.1.4",
|
|
120
120
|
"files": {
|
|
121
|
-
"CLAUDE.md": "
|
|
121
|
+
"CLAUDE.md": "af921af5c262cac7ac784fa86b780e4f31dac1baa510b4b3fd32a52521d4cccf",
|
|
122
122
|
"admin/css/index.css": "f2e934c8b94f883340148aba17945dfa725eeb2b0ddb6cabb8a8c17e2487c1de",
|
|
123
123
|
"admin/lib/kit.js": "913761a2cc19b69bed3f05267d45fdb1c08338b1edb081b9272231733d9b5790",
|
|
124
124
|
"admin/templates/feedback.html": "a366dce78f6c3e84a7cb0ac63eba53e1980a4eb44154496a0536f5214c5bb36f",
|
|
125
|
-
"admin/views/feedback.js": "
|
|
125
|
+
"admin/views/feedback.js": "768d76da473dc4905e8e8e9133a715c1db8c3cb53d91756b5c7d067a92971463",
|
|
126
126
|
"admin/views/inbox.js": "649b86844230be73939d82aa7c97432252c0b119baa102ebf53808e1d5f23737",
|
|
127
127
|
"admin/views/send.js": "5868038a4020158746c5705d2a0ab5f552ac5021328a1dcf64a2def890495427",
|
|
128
128
|
"collections/feedback-reports.json": "a072291b52463f7785a77f5f77d36b8238ba8f89cfc2672c6642eccdb3144486",
|
|
129
129
|
"config.js": "29d9999d1028c2c11931f67fb9abdf35132430b7cf58c161681e734c0b45143a",
|
|
130
130
|
"docs/guide.md": "e006cc3f3c114583285964bc1b5d22b2a01285c2d2015d132434bfed363917d4",
|
|
131
|
-
"lib/receiver.js": "
|
|
131
|
+
"lib/receiver.js": "d77962c895617f9adf2fb16d29992fd83b75a3419a4aa71ac3e3bbe8c2089839",
|
|
132
132
|
"lib/sender.js": "037f9b4469a34ffe1d302a633e664860d625f7fb3be4dfadd01b8a3d094849b7",
|
|
133
133
|
"lib/shape.js": "67acf62f812c2c9a01b52776641ed7ae1601c59ebab8ea278a07d1087a9f299b",
|
|
134
134
|
"plugin.js": "c388bbc2a49b5902773b15a74379c40eeaea58396058136e7901e405b3a396f8",
|
|
135
|
-
"plugin.json": "
|
|
135
|
+
"plugin.json": "612357f1b3166f5bad11a563d0e7759c262a2328eba9890c5937dc98d7752363",
|
|
136
136
|
"tests/api.test.js": "3fd27c561af593168ff0c2d717900feda89da4e306f8e0ecac0a6ad68c6d3359",
|
|
137
137
|
"tests/shape.test.js": "5133578142a79cc1a100b3215436372129f01fb0de8c757fd743ff99e1d7e37f"
|
|
138
138
|
}
|
|
139
139
|
},
|
|
140
140
|
"mail-reader": {
|
|
141
|
-
"version": "1.2.
|
|
141
|
+
"version": "1.2.7",
|
|
142
142
|
"files": {
|
|
143
|
-
"CLAUDE.md": "
|
|
143
|
+
"CLAUDE.md": "6c1fe7956f0d2cc637bff3441061d083e6d938ccf58045473ec49c8fb38f41c1",
|
|
144
144
|
"admin/views/mail.js": "c5c6483f9618c75cac8f4be32d3aea99511db76d4b9d38b8ef43bdc1d35db3c5",
|
|
145
145
|
"config.js": "6e65803ad822a9acda40c203631b8eb32a87cf9d9e5f1c7cb59d01aa406075c6",
|
|
146
|
-
"docs/guide.md": "
|
|
146
|
+
"docs/guide.md": "d6b5944740ea1bbd689034ef726d8bc0f9bfeb28ea62b63c258292962d45cf18",
|
|
147
147
|
"plugin.js": "7f602a60c4eca7d3637659d03d5de9e242584666846b16c9e5a1b758e4856c9f",
|
|
148
|
-
"plugin.json": "
|
|
148
|
+
"plugin.json": "05a5fa1e0ff69eb2cfe0c07a1abbe2cb4a1cd57a75511d0ae6fb1a74524a65e4"
|
|
149
149
|
}
|
|
150
150
|
},
|
|
151
151
|
"security": {
|
|
@@ -175,36 +175,43 @@
|
|
|
175
175
|
}
|
|
176
176
|
},
|
|
177
177
|
"shopping-cart": {
|
|
178
|
-
"version": "1.
|
|
178
|
+
"version": "1.6.1",
|
|
179
179
|
"files": {
|
|
180
|
-
"CLAUDE.md": "
|
|
181
|
-
"admin/css/index.css": "
|
|
182
|
-
"admin/lib/
|
|
180
|
+
"CLAUDE.md": "2759884523ebf0f9323cdefdd220fc9c0ad952f9964c065f19b97530e61f7533",
|
|
181
|
+
"admin/css/index.css": "dfe1ffc05b9161af3d8a74e82bd36c46991cb3b0409223b213eb8eca6fba9d55",
|
|
182
|
+
"admin/lib/chart.js": "b01ce5cfac773c8df8efccab50b67d3dfa99fbead0037d817553db4a6688d4d6",
|
|
183
|
+
"admin/lib/kit.js": "d9aa90469b3f589beaff0c3fb6ad3d983ab3fd6d94c06965be639a665bfe052f",
|
|
183
184
|
"admin/templates/shop.html": "c5bb03f1f7cbabfbd8202f3533783eb1848103755b1ba25fdcf1fd8e9478cb8e",
|
|
184
|
-
"admin/views/orders.js": "
|
|
185
|
-
"admin/views/overview.js": "
|
|
185
|
+
"admin/views/orders.js": "97699e18f0fe4be8f3a811cd2fa7c07ad6139ac312e05b70ab5e80a21e40f09b",
|
|
186
|
+
"admin/views/overview.js": "97045c069c6560b3f67e1b2230bdc9c4133ed92ec6f732886470f886f11c755a",
|
|
186
187
|
"admin/views/products.js": "e109660355aaec8db07eb65fa1169e7d2ec20bc2ec45293cc20e34f7c1bd62bf",
|
|
187
|
-
"admin/views/
|
|
188
|
-
"admin/views/
|
|
188
|
+
"admin/views/sales.js": "da71b9fbb7d39e39ecd8be35b1187e88bc4b5b28f18baaeddc6ea4cd202bb330",
|
|
189
|
+
"admin/views/settings.js": "54d4b52409d0bdd1eb4c6e4d86c07a285914659eb6b7053183f77e2fb4b9feaa",
|
|
190
|
+
"admin/views/shop.js": "83d1cb3b053ff16768959c6fd243a9428b274a3f7cdfd98929a8adc51a71d03e",
|
|
189
191
|
"collections/shop-orders.json": "0bca8eef2d5f9108651f73d273480265c167dc983aa115ce59613d13015ca255",
|
|
190
192
|
"collections/shop-products.json": "4d4613ecec7b8a5a0744c4f0a57b17a4429b461e060206a4bbdea5ad7c8c75dc",
|
|
191
|
-
"config.js": "
|
|
192
|
-
"docs/guide.md": "
|
|
193
|
-
"inject/body.html": "
|
|
194
|
-
"lib/
|
|
195
|
-
"lib/
|
|
193
|
+
"config.js": "31b336560d21a802d799ed487d5c3116de898446ba3521068b10d192f4ebbb2d",
|
|
194
|
+
"docs/guide.md": "5b63bd03b015e273c2880ac431a9faccdcc389e8c5f367697aeb4502be8e280b",
|
|
195
|
+
"inject/body.html": "7e7e5b408f47abb8650e0a7d5e277daf8a634f4c6877c5259f837b1bf3d6c8e8",
|
|
196
|
+
"lib/accounts.js": "5115cec6a145da18c106dfdbf5fd0b2a1bad0c68e8fac4ef623287e85db75e5c",
|
|
197
|
+
"lib/commission.js": "5b10475d98cff2aaf4c76b9950f8d698d6262e36754a8963669690a8e3e9bd52",
|
|
198
|
+
"lib/extensions.js": "73d3515b52c066e3e616c425d91a7a48a6aab153e7d14ae7278b38ee32649352",
|
|
199
|
+
"lib/payments.js": "de093035ce732cd4f7f569fcd32cb5a86e68596a3ae54ec7d00e949b3024f7a7",
|
|
196
200
|
"lib/pricing.js": "5644f2dd55fc3cfd7f40b0169dea5fb547e8b302239b8c17f23cfa177e42eab5",
|
|
197
|
-
"lib/render.js": "
|
|
201
|
+
"lib/render.js": "2a2b9ab4a24728186e1e45e6429c7406a6d5ca3d4b43db1758bbd54405d105b7",
|
|
202
|
+
"lib/sales.js": "16804f0ba4e61e8a2887a7c4172da37533850a1cd975a177b754f3350268e946",
|
|
198
203
|
"lib/samples.js": "912b3d5bd543dffa5f870caf3c2b873f642fd55e3ebf3f97e1b31eeaaac77d3b",
|
|
199
|
-
"lib/shop.js": "
|
|
204
|
+
"lib/shop.js": "190ad53acffb88ca9462900091918ea5d3e40fb666e89b9c060832035284115c",
|
|
200
205
|
"lib/summary.js": "cdb13ce3d1799d429059fa03f179281711dc00427ccf2eb6b8f8aa52fef26295",
|
|
201
|
-
"plugin.js": "
|
|
202
|
-
"plugin.json": "
|
|
203
|
-
"plugin.public.js": "
|
|
204
|
-
"public/
|
|
206
|
+
"plugin.js": "f776e6bbdd98bf0b487e8247bc7334e78a1f8601a54b26b8c30df817619c2ebc",
|
|
207
|
+
"plugin.json": "abc448993e39b008d2f8d69deb55176bd090f5272003164a0931059dca4a5fc5",
|
|
208
|
+
"plugin.public.js": "2f92e8340c9da653401c11dd99dab033dc6772d63fa2114489192c48e9e0be00",
|
|
209
|
+
"public/account.js": "5f644016c098c7879c59948d662f5997cb61680996204ecb7897baab8945d434",
|
|
210
|
+
"public/cart.js": "7338141392ec653ff616eb75688d2a2ad08cff8427d7020d381f540be0037b73",
|
|
205
211
|
"public/lib/money.js": "d22b2b6f80dde6f7887b69e24188c05ea93c02217aba639b5331aa3bd616f4fc",
|
|
206
212
|
"public/lib/orders.js": "09478e20d238adc8e387b3d9efcb13c441898fab18387ce6a1a8a92304ac4c5b",
|
|
207
213
|
"public/lib/products.js": "65a033b4b03c1b6d9cd7035e6be4c3fe47c7ad309dc14c4d417b6ac0fc10e2f4",
|
|
214
|
+
"public/lib/session.js": "4f71e98818a52cf2321c8dc57e33e5591deb04ebb2c0bea19dd6758827719a08",
|
|
208
215
|
"public/samples/candle-2.svg": "aca5691b9168467caf6feb7622ab154ee92504be8b887ac6c818db22476e7bed",
|
|
209
216
|
"public/samples/candle.svg": "ef75e95a53ce13e88928004c6f1290c45a1094765e2ba741ae1fc55d404b881f",
|
|
210
217
|
"public/samples/mug-2.svg": "5f37d23e90b7e79e42d38009a56b3ea26adae03586830a0cd418565491bd4532",
|
|
@@ -217,30 +224,35 @@
|
|
|
217
224
|
"public/samples/tee.svg": "287a82d7fc00acb1543b3209927ae20dcbe3d4b2a2b3de03574ab0c463385f3c",
|
|
218
225
|
"public/samples/tote-2.svg": "9c7f41cc57866e71973e413a6c87f46be48f7488d3490ceea58e24559c334813",
|
|
219
226
|
"public/samples/tote.svg": "01de817a32c8d3d8ea47ff491049f439ca3e4d4fce943dbdc62a4187139e016c",
|
|
220
|
-
"public/shop.css": "
|
|
227
|
+
"public/shop.css": "c01603589727c91ff4204222e594b3f52e3ca165c1ebf9e6cef6c9f006f6e57b",
|
|
228
|
+
"roles/shop-customer.json": "3f3366c536c9fbf2b2ce146bb3090b008d5fbfe22752e764eb8e5a1de9cd611c",
|
|
221
229
|
"templates/shop.html": "4649e57d23458833a157799dc80141824eb0f658d96734bd1859c68e0ba9a8b9",
|
|
222
|
-
"tests/
|
|
223
|
-
"tests/
|
|
230
|
+
"tests/accounts.test.js": "e64adc2a855d4ca017682bba7f75b052534f826556d8d35d825fb79cbc66596a",
|
|
231
|
+
"tests/api.test.js": "8bccf43e520bca7bb73837a74b3c3cf1fdb3a8db8fb5026a27bc0e5ed9d5ca80",
|
|
232
|
+
"tests/commission.test.js": "1dfb26a90d3dfcf9f57f2174ac5d9b2310570b21f82c79bf9a2011e7f4911e85",
|
|
233
|
+
"tests/pure.test.js": "13620f708f21e2590979e8579e2e7788d6109e1682bf73d7b1eb64838da35196",
|
|
234
|
+
"tests/sales.test.js": "9025bc608b04bcc14f5bb2c98b4f7c35de99f3ddab4ffa59772d045795bf2cb7"
|
|
224
235
|
}
|
|
225
236
|
},
|
|
226
237
|
"support": {
|
|
227
|
-
"version": "1.1.
|
|
238
|
+
"version": "1.1.2",
|
|
228
239
|
"files": {
|
|
229
|
-
"CLAUDE.md": "
|
|
240
|
+
"CLAUDE.md": "9b35a7df9335e69278c261f3e68944a4fea87c495c2ff500c14990dc8a6c7337",
|
|
230
241
|
"admin/css/index.css": "bad02a487ebb131b64ee55e2a4533c8b79d6ada80fffd34b0952bf3b68204a36",
|
|
231
242
|
"admin/lib/kit.js": "22f02768625bcad9d9c8e64bf0a8fe95f7c5ff6dcf8abb189df8203362a7a4e1",
|
|
232
243
|
"admin/templates/support.html": "2bb69d0443a367a402e6160fe68f4af3e8983b578cfff9575b4df9a534fcb480",
|
|
233
244
|
"admin/views/desk.js": "ac043630ad08c8f2053f3f908be18586d9a4798976d20427cf5215ee042c807c",
|
|
234
245
|
"admin/views/inbox.js": "f570595d7b2c831d151e0aaf85ff903e6e2b7066124b3f9d23c2c24f5b1734cf",
|
|
235
|
-
"admin/views/support.js": "
|
|
246
|
+
"admin/views/support.js": "4be6cce46067ee7af140f43253da36e060add2a9190e91f013fd1aad668fcd7b",
|
|
236
247
|
"collections/support-tickets.json": "55a0856bacd66c2e8f1e3c85bdf3638e5ff826560943cda854a8f2d336a88905",
|
|
237
248
|
"config.js": "eb7ccc1169fa8b2be52a213536825492fedaec5fa73fd749cba9f498dd54b503",
|
|
249
|
+
"docs/guide.md": "d0d38dacbdf68bfaba1ed4e62c8d182372a683d255a8ff75ca57bd0be2a0b082",
|
|
238
250
|
"lib/mail.js": "d14aae3a59aad6af61451863cdaaf94fc4a87810b16e1dcb9c655dda6c803b3b",
|
|
239
|
-
"lib/receiver.js": "
|
|
251
|
+
"lib/receiver.js": "64c292281bde5a942de4d04a16911d0730a4da7e80ce6d91a9da864b9f886b59",
|
|
240
252
|
"lib/sender.js": "b20aaee34ade89814a897f68467f9902693843fa6b5c8eb0090c4059193c8227",
|
|
241
253
|
"lib/shape.js": "04d840b589d20700ccb2cfa4c72072f8d9f8e03f0573e2ea1177a7a83a383bea",
|
|
242
254
|
"plugin.js": "e3c50a05fa99cb24375782bbb3ca8c58cf5ddc3036937e6115f4d963715c120e",
|
|
243
|
-
"plugin.json": "
|
|
255
|
+
"plugin.json": "c45819016588e7e26426830ce30fd6d7561b61ffcac2a8ad48bb4c178c138188",
|
|
244
256
|
"tests/api.test.js": "1fa87f6ae12c48fc34a5fb2b5bbeed6cfaa36329610674d4fae05d64734dbb15",
|
|
245
257
|
"tests/shape.test.js": "364c6b7680699f59fcd449cd448184486488838300f155135daea4c05387cf74"
|
|
246
258
|
}
|
|
@@ -108,3 +108,9 @@ Right-click on the open message is engine code (`_lib/admin/mail/body-menu.js`,
|
|
|
108
108
|
The page-size picker under the list is engine code (`listPageSize()`/`pageAfterResize()` in
|
|
109
109
|
`_lib/admin/mail/settings-section.js`, sent as `limit` by `reader-view.js`, capped by `maxListLimit`);
|
|
110
110
|
this version only documents it in docs/guide.md. 0.116.8 is already released, so it can ship now.
|
|
111
|
+
|
|
112
|
+
## Private hosts refused (guide only, 1.2.7, 2026-10-13; engine `_lib/mail/hostGuard.js`)
|
|
113
|
+
|
|
114
|
+
Saving or testing a mailbox on a private, loopback or link-local address is refused by the shared read routes
|
|
115
|
+
unless it is the site's own outgoing server or `allowPrivateMailHosts` is set; `/accounts/test` is limited to
|
|
116
|
+
10 a minute. Engine code - this version only documents the option in docs/guide.md. Ship with the matching core.
|
|
@@ -99,6 +99,7 @@ Whoever runs the site can change these in the plugin's configuration file (`conf
|
|
|
99
99
|
| `listLimit` | 50 | Messages per page until you pick another size under the list |
|
|
100
100
|
| `allowRemoteImages` | false | Load remote images without asking |
|
|
101
101
|
| `allowInsecureTLS` | false | Accept mail server certificates that do not verify (see Tips) |
|
|
102
|
+
| `allowPrivateMailHosts` | false | Let a mailbox be on the site's own private network (a LAN or loopback address). Off, a mailbox on such an address is refused; the site's own outgoing server is always allowed |
|
|
102
103
|
| `connectionTimeoutMs` | 20000 | How long to wait for a mail server, in milliseconds |
|
|
103
104
|
| `maxConnections` | 20 | Most mail server connections kept open at once |
|
|
104
105
|
| `maxMessageBytes` | 25000000 | Largest message that will be opened, in bytes |
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mail-reader",
|
|
3
3
|
"displayName": "Mail Reader",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.7",
|
|
5
5
|
"tier": "free",
|
|
6
6
|
"description": "Read-only IMAP mailbox reader. Connects to a standard IMAP server and reads mail without ever writing to it.",
|
|
7
7
|
"author": "Darryl Waterhouse",
|
|
8
|
-
"date": "2026-
|
|
8
|
+
"date": "2026-10-13",
|
|
9
9
|
"icon": "mail",
|
|
10
10
|
"uses": {
|
|
11
11
|
"contacts": "Add sender to contacts"
|
|
@@ -94,7 +94,8 @@ A cached page can show a stale stock flag until it is re-rendered; checkout re-p
|
|
|
94
94
|
- Storefront CSS tokens are defined on `body`, not `:root`: themes set --dm-* on body's dm-theme-* class.
|
|
95
95
|
- The site's own input/link styles reach the storefront: see the "reach in here" block at the end of shop.css.
|
|
96
96
|
- `siteBase()` uses `request.host` (with the port); `request.hostname` drops it under Fastify 5.
|
|
97
|
-
- A test site on a non-standard port needs `site.baseUrl` for Stripe/PayPal return addresses behind a proxy
|
|
97
|
+
- A test site on a non-standard port needs `site.baseUrl` for Stripe/PayPal return addresses behind a proxy, and
|
|
98
|
+
emails have no links to the order until it (or a real sign-in) gives the site a trusted address (1.6.1).
|
|
98
99
|
- Admin views: item templates live in JS (a templateUrl goes through _.render); checkboxes are set after render
|
|
99
100
|
and listened to; slideovers via kit.openPanel (disposed and removed on close).
|
|
100
101
|
|
|
@@ -156,3 +157,63 @@ these, behaves as 1.1.
|
|
|
156
157
|
offline instructions; on the order and in `publicOrder`.
|
|
157
158
|
- **`services.refund(id, {note, restock, viaProvider})`** - the admin's refund (through Stripe / PayPal first),
|
|
158
159
|
`refundOrder()` in plugin.js, shared by `POST /orders/:id/refund`.
|
|
160
|
+
|
|
161
|
+
## 1.4.0: customer accounts (optional; plan dcms-marketplace/docs/customer-accounts-plan.md)
|
|
162
|
+
|
|
163
|
+
`settings.accounts` (off by default). `lib/accounts.js`, routes `/account/*`, pages `<shop>/account`, `/account/confirm`,
|
|
164
|
+
`/account/reset` (plugin.public.js -> `accountHtml`; `public/account.js` does the work), browser session in
|
|
165
|
+
`public/lib/session.js` (shared with extensions - Takeaway imports it).
|
|
166
|
+
- A customer = core user, role `shop-customer` (roles/shop-customer.json: no permissions, admin confined to My Profile),
|
|
167
|
+
created INACTIVE with `meta.shop.pending` {hash, until (48 h)}; the emailed link activates it. Register answers the
|
|
168
|
+
same for a taken email (the holder is emailed instead) - no account discovery. A pending account that ran out is
|
|
169
|
+
replaced. A confirmation email that fails removes the account again (502). Needs SMTP (`mailReady`).
|
|
170
|
+
- Signing in, refresh, logout and the new password are CORE's routes (`/api/auth/login|refresh|logout|reset-password`);
|
|
171
|
+
the Cart issues core's reset token (`issueResetToken`) and mails a link to `<shop>/account/reset`, never /admin.
|
|
172
|
+
Tokens live in localStorage `dm-shop:account`; account routes take core's Bearer (`authenticate`).
|
|
173
|
+
- An account's orders = orders with its (confirmed) email, any time - nothing linked. `meta.shop` keeps phone and up
|
|
174
|
+
to 8 addresses. Delete my account: customers only (staff 409), with the password; orders stay.
|
|
175
|
+
- Checkout fills in a signed-in customer (details + first saved address); the receipt offers "Save your details"
|
|
176
|
+
(`[data-shop-join]`); the storefront and basket link to the account page.
|
|
177
|
+
- Extension hook `account(order, {settings})` -> HTML beside the order on the account page (`registry.accountParts`;
|
|
178
|
+
`receiptParts` and it share `registry.parts(kind)`). Takeaway: Reorder.
|
|
179
|
+
- Trap fixed on the way: `if (await fail(reply, …))` - awaiting a Fastify reply resolves EMPTY once sent, so the route
|
|
180
|
+
ran on after its 404. `accountsOff` returns a boolean and sends the reply on the side.
|
|
181
|
+
|
|
182
|
+
## 1.5.0: Sales (plan dcms-marketplace/docs/sales-commission-tenancy-plan.md, part 1)
|
|
183
|
+
|
|
184
|
+
A **Sales** tab (admin/views/sales.js): ranges (today, this week, last 7 days, this month, last month, last 30 days,
|
|
185
|
+
this year, dates), by day / week / month; tiles, takings chart, where it came from, how it was paid, the figures, a
|
|
186
|
+
table of periods, CSV. `GET /sales?range|from&to&by`, `GET /sales.csv` (shop:read). All figures are `lib/sales.js` (PURE):
|
|
187
|
+
- A sale = paid / fulfilled / refunded order, dated by `paidAt` in the shop's zone (`settings.timeZone`, Europe/London
|
|
188
|
+
unless set - there is no setting in the UI yet); a refund dated by `refundedAt`, so a period's net is what stayed.
|
|
189
|
+
- Sources per line: `takeaway` (ref takeaway:…), `addons` (any other ref), `downloads` (a product Pro gave a link or
|
|
190
|
+
key for on that order), `shop`. Line amounts are before discounts; discount, delivery and tax are order-level.
|
|
191
|
+
- `admin/lib/chart.js` - the one bar chart (single series, primary hue, rounded ends, tooltips), shared with Overview.
|
|
192
|
+
|
|
193
|
+
## 1.6.0: platform commission (plan part 2; the manager side is site-manager's Customers › Commission)
|
|
194
|
+
|
|
195
|
+
A value on each sale, per tenant, set by the platform's super admin on the manager - which writes this site's rates to
|
|
196
|
+
`config/commission.json` (`{rates: {default?, shop?, takeaway?, addons?: {percent, fixed}}, updatedAt, by}`); the
|
|
197
|
+
Cart only READS it (`lib/commission.js currentCommission`, re-read when the file's mtime changes - no restart).
|
|
198
|
+
- When an order becomes paid (`markPaid`, `moveOrder` -> paid) it gets `order.data.commission` = `{type, base, percent,
|
|
199
|
+
fixed, amount, at}` - the rate of that day, kept for good (a later rate never rewrites it). `percent` is of the
|
|
200
|
+
order total; `fixed` per sale; never more than the sale; a free order gets none. Type: `takeaway` (ref takeaway:),
|
|
201
|
+
`addons` (other ref), `shop`. A refund sets `reversed`, `reversedAt`.
|
|
202
|
+
- Never shown to the customer (`publicOrder` leaves it out). Shown to the shop: Sales (commission, returned, after
|
|
203
|
+
commission; a CSV column), the order, Shop settings › Payment ("set by your provider" - read-only).
|
|
204
|
+
- The manager's statements add up what the orders carry - nothing is recalculated there.
|
|
205
|
+
|
|
206
|
+
## 1.6.1 (2026-10-13): no Host headers in email links; confirm says nothing; refunds under the lock
|
|
207
|
+
|
|
208
|
+
- `siteBase(request)` is for a payment provider's return addresses only (Site URL, else the shopper's own origin) and
|
|
209
|
+
remembers nothing: a forged Host on one `/quote` used to become the address - with the receipt tokens - in every email
|
|
210
|
+
until the next request. Email links use `quietBase()` (async: Site URL, else core's `trustedOrigin` - an address someone
|
|
211
|
+
has really signed in on, as password resets do), exposed to extensions as `services.linkBase()`; `services.siteBase()`
|
|
212
|
+
stays sync and is the Site URL or ''. `receiptUrl` is '' when there is none and the log says so once; `startCheckout`
|
|
213
|
+
prefers the Site URL to the extension's `base`. Account confirm and forgotten-password links use the same rule for the
|
|
214
|
+
request (`linkBase(request)`); with no trusted address, register answers 409 and forgot sends nothing.
|
|
215
|
+
- `POST /account/confirm` needs the token (shape-checked) and a waiting account, answers `{confirmed: true}` only, and
|
|
216
|
+
is off when accounts are: user ids are public (`createdBy`), so "already confirmed -> here is the email" leaked emails.
|
|
217
|
+
- `refundOrder` runs under `withLock` (provider call and `moveOrderNow` in one), and Stripe refunds carry
|
|
218
|
+
`Idempotency-Key: shopping-cart-refund-<orderId>` (sessions: `shop-session-<orderId>`) so a retry after a timeout
|
|
219
|
+
returns the first result.
|
|
@@ -224,3 +224,19 @@ a.shp-tile:hover { border-color: var(--dm-primary); }
|
|
|
224
224
|
.shp-copy code { flex: 1; overflow-wrap: anywhere; font-size: var(--dm-text-xs); }
|
|
225
225
|
.shp-steps li .shp-step-btns { grid-column: 3; grid-row: 1 / 3; display: flex; gap: .3rem; color: inherit; font-size: inherit; }
|
|
226
226
|
.shp-steps li .shp-step-btns .btn { grid-column: auto; grid-row: auto; }
|
|
227
|
+
|
|
228
|
+
/* Sales */
|
|
229
|
+
.shp-sales { display: flex; flex-direction: column; gap: 1rem; }
|
|
230
|
+
.shp-sales-bar { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; }
|
|
231
|
+
.shp-sales-custom { display: inline-flex; align-items: center; gap: .4rem; }
|
|
232
|
+
.shp-sales-custom input { width: auto; padding-top: .25rem; padding-bottom: .25rem; }
|
|
233
|
+
.shp-sales-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); gap: 1rem; }
|
|
234
|
+
.shp-sales-note { margin: .6rem 0 0; font-size: var(--dm-text-sm); }
|
|
235
|
+
.shp-sales-table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; font-size: var(--dm-text-sm); }
|
|
236
|
+
.shp-sales-table th, .shp-sales-table td { padding: .4rem .5rem; border-bottom: 1px solid var(--dm-border); text-align: right; }
|
|
237
|
+
.shp-sales-table th:first-child, .shp-sales-table td:first-child { text-align: left; font-weight: 500; }
|
|
238
|
+
.shp-sales-table thead th { color: var(--dm-text-muted); font-size: var(--dm-text-xs); font-weight: 600; }
|
|
239
|
+
.shp-sales-table tr.is-sum th, .shp-sales-table tr.is-sum td { font-weight: 600; }
|
|
240
|
+
.shp-sales-table tr.is-total th, .shp-sales-table tr.is-total td { font-weight: 800; font-size: 1rem; border-bottom: 0; }
|
|
241
|
+
.shp-sales-table tr.is-muted th, .shp-sales-table tr.is-muted td { color: var(--dm-text-muted); font-weight: 400; }
|
|
242
|
+
.shp-sales-periods summary { cursor: pointer; font-weight: 600; }
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Shop's one chart: a single series of bars - one hue (the theme's primary), 2px apart, 4px rounded ends on the
|
|
3
|
+
* baseline, a recessive grid with three ticks, three date labels, and a tooltip on hover or keyboard focus. Shared by
|
|
4
|
+
* the Overview and Sales. One series, so no legend: the card's title names it.
|
|
5
|
+
*
|
|
6
|
+
* @module shopping-cart/admin/lib/chart
|
|
7
|
+
*/
|
|
8
|
+
import {esc} from './kit.js';
|
|
9
|
+
|
|
10
|
+
/** A round top for the axis: 1, 2 or 5 × a power of ten, in minor units. */
|
|
11
|
+
export function niceMax(v) {
|
|
12
|
+
if (v <= 0) return 1000;
|
|
13
|
+
const p = 10 ** Math.floor(Math.log10(v));
|
|
14
|
+
for (const m of [1, 2, 5, 10]) if (m * p >= v) return m * p;
|
|
15
|
+
return 10 * p;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {HTMLElement} el
|
|
20
|
+
* @param {{value: number, label: string, tip: string}[]} series - `label` under the axis, `tip` beside the value
|
|
21
|
+
* @param {{money: Function, ariaLabel: string, scope: Function}} o
|
|
22
|
+
*/
|
|
23
|
+
export function barChart(el, series, {money, ariaLabel, scope}) {
|
|
24
|
+
const W = Math.max(360, Math.round(el.clientWidth) || 600), H = 170, padL = 44, padB = 22, padT = 8;
|
|
25
|
+
const top = niceMax(Math.max(0, ...series.map(d => d.value)));
|
|
26
|
+
const n = series.length || 1;
|
|
27
|
+
const slot = (W - padL) / n;
|
|
28
|
+
const bw = Math.max(2, slot - 2);
|
|
29
|
+
const y = (v) => padT + (H - padT - padB) * (1 - v / top);
|
|
30
|
+
const bars = series.map((d, i) => {
|
|
31
|
+
const x = padL + i * slot + 1;
|
|
32
|
+
const h = Math.max(0, H - padB - y(Math.max(0, d.value)));
|
|
33
|
+
const r = Math.min(4, bw / 2, h);
|
|
34
|
+
const path = h > 0 ? `M${x},${H - padB} v${-(h - r)} q0,${-r} ${r},${-r} h${bw - 2 * r} q${r},0 ${r},${r} v${h - r} z` : '';
|
|
35
|
+
return `<g class="shp-bar" data-i="${i}" tabindex="0" aria-label="${esc(d.label)}: ${esc(money(d.value))}, ${esc(d.tip)}">
|
|
36
|
+
<rect x="${padL + i * slot}" y="${padT}" width="${slot}" height="${H - padT - padB}" fill="transparent"/>
|
|
37
|
+
${path ? `<path d="${path}"/>` : `<rect class="shp-bar-zero" x="${x}" y="${H - padB - 1}" width="${bw}" height="1"/>`}
|
|
38
|
+
</g>`;
|
|
39
|
+
}).join('');
|
|
40
|
+
const at = [...new Set([0, Math.floor((n - 1) / 2), n - 1])].filter(i => series[i]);
|
|
41
|
+
const labels = at.map(i => `<text x="${padL + i * slot + slot / 2}" y="${H - 6}" text-anchor="middle">${esc(series[i].label)}</text>`).join('');
|
|
42
|
+
el.innerHTML = `<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(ariaLabel)}">
|
|
43
|
+
${[0, top / 2, top].map(t => `<line class="shp-gridline" x1="${padL}" x2="${W}" y1="${y(t)}" y2="${y(t)}"/><text class="shp-axis" x="${padL - 6}" y="${y(t) + 4}" text-anchor="end">${esc(money(t, {compact: true}))}</text>`).join('')}
|
|
44
|
+
${bars}
|
|
45
|
+
<g class="shp-axis">${labels}</g>
|
|
46
|
+
</svg><div class="shp-tip" hidden></div>`;
|
|
47
|
+
const tip = el.querySelector('.shp-tip');
|
|
48
|
+
const show = (g) => {
|
|
49
|
+
const d = series[Number(g.dataset.i)];
|
|
50
|
+
tip.innerHTML = `<strong>${esc(money(d.value))}</strong><span>${esc(d.label)} · ${esc(d.tip)}</span>`;
|
|
51
|
+
tip.hidden = false;
|
|
52
|
+
const r = g.getBoundingClientRect(), box = el.getBoundingClientRect();
|
|
53
|
+
tip.style.left = `${Math.min(box.width - 140, Math.max(0, r.left - box.left + r.width / 2 - 70))}px`;
|
|
54
|
+
el.querySelectorAll('.shp-bar.is-on').forEach(b => b.classList.remove('is-on'));
|
|
55
|
+
g.classList.add('is-on');
|
|
56
|
+
};
|
|
57
|
+
const hide = () => { tip.hidden = true; el.querySelectorAll('.shp-bar.is-on').forEach(b => b.classList.remove('is-on')); };
|
|
58
|
+
const over = (e) => { const g = e.target.closest('.shp-bar'); if (g) show(g); };
|
|
59
|
+
el.addEventListener('mouseover', over);
|
|
60
|
+
el.addEventListener('focusin', over);
|
|
61
|
+
el.addEventListener('mouseleave', hide);
|
|
62
|
+
el.addEventListener('focusout', hide);
|
|
63
|
+
scope?.(() => { el.removeEventListener('mouseover', over); el.removeEventListener('focusin', over); el.removeEventListener('mouseleave', hide); el.removeEventListener('focusout', hide); });
|
|
64
|
+
}
|
|
@@ -42,6 +42,21 @@ export function loadKit() {
|
|
|
42
42
|
const seg = (id) => encodeURIComponent(String(id));
|
|
43
43
|
export const api = {
|
|
44
44
|
summary: () => H.get(`${BASE}/summary`),
|
|
45
|
+
sales: (q) => H.get(`${BASE}/sales?${new URLSearchParams(Object.entries(q).filter(([, v]) => v && v !== 'custom'))}`),
|
|
46
|
+
/** The CSV: fetched with the session (authenticate is Bearer-only) and saved as a file. */
|
|
47
|
+
async salesCsv(q) {
|
|
48
|
+
let token = null;
|
|
49
|
+
try { token = S.get('auth_token'); } catch { /* none */ }
|
|
50
|
+
const res = await fetch(`${BASE}/sales.csv?${new URLSearchParams(Object.entries(q).filter(([, v]) => v && v !== 'custom'))}`, {headers: token ? {Authorization: `Bearer ${token}`} : {}});
|
|
51
|
+
if (!res.ok) throw new Error(`the server answered ${res.status}`);
|
|
52
|
+
const name = (res.headers.get('content-disposition') || '').match(/filename="([^"]+)"/)?.[1] || 'sales.csv';
|
|
53
|
+
const a = document.createElement('a');
|
|
54
|
+
a.href = URL.createObjectURL(await res.blob());
|
|
55
|
+
a.download = name;
|
|
56
|
+
document.body.appendChild(a);
|
|
57
|
+
a.click();
|
|
58
|
+
setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
|
|
59
|
+
},
|
|
45
60
|
extensions: () => H.get(`${BASE}/extensions`),
|
|
46
61
|
products: {
|
|
47
62
|
list: () => H.get(`${BASE}/products`),
|
|
@@ -301,6 +301,7 @@ ${(d.lines || []).map(l => `<tr><td class="q">${l.qty}</td><td>${esc(l.name)}${l
|
|
|
301
301
|
<label class="form-label">Staff note <span data-help="Only you see this. Saved when you click away."></span></label>
|
|
302
302
|
<textarea class="form-input" rows="2" data-model="staffNote.value" data-part="staff" placeholder="Wrapped as a gift, sent second class…"></textarea>
|
|
303
303
|
${d.paymentRef || d.providerRef ? `<p class="shp-muted shp-ref">Payment reference: <code>${esc(d.paymentRef || d.providerRef)}</code></p>` : ''}
|
|
304
|
+
${d.commission ? `<p class="shp-muted shp-ref">Platform commission: ${esc(money(d.commission.amount))} (${esc(`${d.commission.percent}%${d.commission.fixed ? ` + ${money(d.commission.fixed)}` : ''}`)})${d.commission.reversed ? ' - returned with the refund' : ''}</p>` : ''}
|
|
304
305
|
${ext.length ? `<div class="shp-ext">${ext.map(x => `<span class="shp-chip is-muted">${esc(x.k)}: ${esc(String(x.v))}</span>`).join('')}</div>` : ''}
|
|
305
306
|
<div data-part="ext-panels" hidden></div>
|
|
306
307
|
<h4>History</h4>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* steps to its first sale instead.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import {barChart} from '../lib/chart.js';
|
|
10
11
|
import {api, esc, fmtDate, money, when} from '../lib/kit.js';
|
|
11
12
|
import {STATUS_LABEL, STATUS_TONE} from '/plugins/shopping-cart/public/lib/orders.js';
|
|
12
13
|
|
|
@@ -157,58 +158,8 @@ export async function mount(root, ctx) {
|
|
|
157
158
|
I.scan(view);
|
|
158
159
|
}
|
|
159
160
|
|
|
160
|
-
/** Thirty
|
|
161
|
+
/** Thirty days of takings, one bar a day. */
|
|
161
162
|
function drawChart(el, series, scope) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const top = niceMax(max);
|
|
165
|
-
const n = series.length;
|
|
166
|
-
const slot = (W - padL) / n;
|
|
167
|
-
const bw = Math.max(2, slot - 2);
|
|
168
|
-
const y = (v) => padT + (H - padT - padB) * (1 - v / top);
|
|
169
|
-
const ticks = [0, top / 2, top];
|
|
170
|
-
const day = (k) => fmtDate(k);
|
|
171
|
-
const bars = series.map((d, i) => {
|
|
172
|
-
const x = padL + i * slot + 1;
|
|
173
|
-
const h = Math.max(0, H - padB - y(d.revenue));
|
|
174
|
-
const r = Math.min(4, bw / 2, h);
|
|
175
|
-
const path = h > 0
|
|
176
|
-
? `M${x},${H - padB} v${-(h - r)} q0,${-r} ${r},${-r} h${bw - 2 * r} q${r},0 ${r},${r} v${h - r} z`
|
|
177
|
-
: '';
|
|
178
|
-
return `<g class="shp-bar" data-i="${i}" tabindex="0" aria-label="${esc(day(d.day))}: ${esc(money(d.revenue))}, ${d.orders} orders">
|
|
179
|
-
<rect x="${padL + i * slot}" y="${padT}" width="${slot}" height="${H - padT - padB}" fill="transparent"/>
|
|
180
|
-
${path ? `<path d="${path}"/>` : `<rect class="shp-bar-zero" x="${x}" y="${H - padB - 1}" width="${bw}" height="1"/>`}
|
|
181
|
-
</g>`;
|
|
182
|
-
}).join('');
|
|
183
|
-
const labels = [0, Math.floor(n / 2), n - 1].map(i => `<text x="${padL + i * slot + slot / 2}" y="${H - 6}" text-anchor="middle">${esc(day(series[i].day))}</text>`).join('');
|
|
184
|
-
el.innerHTML = `<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="Takings for each of the last 30 days">
|
|
185
|
-
${ticks.map(t => `<line class="shp-gridline" x1="${padL}" x2="${W}" y1="${y(t)}" y2="${y(t)}"/><text class="shp-axis" x="${padL - 6}" y="${y(t) + 4}" text-anchor="end">${esc(money(t, {compact: true}))}</text>`).join('')}
|
|
186
|
-
${bars}
|
|
187
|
-
<g class="shp-axis">${labels}</g>
|
|
188
|
-
</svg><div class="shp-tip" hidden></div>`;
|
|
189
|
-
const tip = el.querySelector('.shp-tip');
|
|
190
|
-
const show = (g) => {
|
|
191
|
-
const d = series[Number(g.dataset.i)];
|
|
192
|
-
tip.innerHTML = `<strong>${esc(money(d.revenue))}</strong><span>${esc(day(d.day))} · ${d.orders} order${d.orders === 1 ? '' : 's'}</span>`;
|
|
193
|
-
tip.hidden = false;
|
|
194
|
-
const r = g.getBoundingClientRect(), box = el.getBoundingClientRect();
|
|
195
|
-
tip.style.left = `${Math.min(box.width - 120, Math.max(0, r.left - box.left + r.width / 2 - 60))}px`;
|
|
196
|
-
el.querySelectorAll('.shp-bar.is-on').forEach(b => b.classList.remove('is-on'));
|
|
197
|
-
g.classList.add('is-on');
|
|
198
|
-
};
|
|
199
|
-
const hide = () => { tip.hidden = true; el.querySelectorAll('.shp-bar.is-on').forEach(b => b.classList.remove('is-on')); };
|
|
200
|
-
const over = (e) => { const g = e.target.closest('.shp-bar'); if (g) show(g); };
|
|
201
|
-
el.addEventListener('mouseover', over);
|
|
202
|
-
el.addEventListener('focusin', over);
|
|
203
|
-
el.addEventListener('mouseleave', hide);
|
|
204
|
-
el.addEventListener('focusout', hide);
|
|
205
|
-
scope(() => { el.removeEventListener('mouseover', over); el.removeEventListener('focusin', over); });
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** A round top for the axis: 1, 2 or 5 × a power of ten, in minor units. */
|
|
209
|
-
function niceMax(v) {
|
|
210
|
-
if (v <= 0) return 1000;
|
|
211
|
-
const p = 10 ** Math.floor(Math.log10(v));
|
|
212
|
-
for (const m of [1, 2, 5, 10]) if (m * p >= v) return m * p;
|
|
213
|
-
return 10 * p;
|
|
163
|
+
barChart(el, series.map(d => ({value: d.revenue, label: fmtDate(d.day), tip: `${d.orders} order${d.orders === 1 ? '' : 's'}`})),
|
|
164
|
+
{money, ariaLabel: 'Takings for each of the last 30 days', scope});
|
|
214
165
|
}
|