@aglyn/plugins-commerce 1.0.0-beta.289 → 1.0.0-beta.290

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +2 -2
  2. package/package.json +13 -13
  3. package/src/lib/components/console/commerce-pdf-download.d.ts +30 -0
  4. package/src/lib/components/console/commerce-pdf-download.js +53 -0
  5. package/src/lib/components/console/commerce-pdf-download.js.map +1 -0
  6. package/src/lib/components/console/host-quotes-card.component.js +20 -4
  7. package/src/lib/components/console/host-quotes-card.component.js.map +1 -1
  8. package/src/lib/components/console/order-invoice.component.d.ts +6 -4
  9. package/src/lib/components/console/order-invoice.component.js +67 -11
  10. package/src/lib/components/console/order-invoice.component.js.map +1 -1
  11. package/src/lib/components/console/quote-editor-dialog.component.js +53 -2
  12. package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
  13. package/src/lib/components/console/quote-proofs.component.d.ts +37 -0
  14. package/src/lib/components/console/quote-proofs.component.js +626 -0
  15. package/src/lib/components/console/quote-proofs.component.js.map +1 -0
  16. package/src/lib/components/quote-page-body.js +29 -1
  17. package/src/lib/components/quote-page-body.js.map +1 -1
  18. package/src/lib/components/quote-proof-panel.d.ts +42 -0
  19. package/src/lib/components/quote-proof-panel.js +436 -0
  20. package/src/lib/components/quote-proof-panel.js.map +1 -0
  21. package/src/lib/model/commerce-quote-pay.d.ts +27 -0
  22. package/src/lib/model/commerce-quote-pay.js +36 -3
  23. package/src/lib/model/commerce-quote-pay.js.map +1 -1
  24. package/src/lib/model/commerce-quote-proofs.d.ts +159 -0
  25. package/src/lib/model/commerce-quote-proofs.js +348 -0
  26. package/src/lib/model/commerce-quote-proofs.js.map +1 -0
  27. package/src/lib/model/commerce-quotes.d.ts +59 -3
  28. package/src/lib/model/commerce-quotes.js.map +1 -1
  29. package/src/lib/model/index.d.ts +1 -0
  30. package/src/lib/model/index.js +1 -0
  31. package/src/lib/model/index.js.map +1 -1
  32. package/src/lib/server/commerce-pdf.d.ts +100 -0
  33. package/src/lib/server/commerce-pdf.js +283 -0
  34. package/src/lib/server/commerce-pdf.js.map +1 -0
  35. package/src/lib/server/order-print.d.ts +6 -0
  36. package/src/lib/server/order-print.js +22 -5
  37. package/src/lib/server/order-print.js.map +1 -1
  38. package/src/lib/server/pdf-response.d.ts +31 -0
  39. package/src/lib/server/pdf-response.js +30 -0
  40. package/src/lib/server/pdf-response.js.map +1 -0
  41. package/src/lib/server/pos-reader-splash.d.ts +2 -18
  42. package/src/lib/server/pos-reader-splash.js +8 -93
  43. package/src/lib/server/pos-reader-splash.js.map +1 -1
  44. package/src/lib/server/quote-pay.d.ts +31 -4
  45. package/src/lib/server/quote-pay.js +11 -11
  46. package/src/lib/server/quote-pay.js.map +1 -1
  47. package/src/lib/server/quote-proofs.d.ts +51 -0
  48. package/src/lib/server/quote-proofs.js +500 -0
  49. package/src/lib/server/quote-proofs.js.map +1 -0
  50. package/src/lib/server/quote-uploads.d.ts +33 -0
  51. package/src/lib/server/quote-uploads.js +29 -0
  52. package/src/lib/server/quote-uploads.js.map +1 -0
  53. package/src/lib/server/quote.d.ts +14 -0
  54. package/src/lib/server/quote.js +336 -3
  55. package/src/lib/server/quote.js.map +1 -1
  56. package/src/lib/server/site-image-source.d.ts +38 -0
  57. package/src/lib/server/site-image-source.js +130 -0
  58. package/src/lib/server/site-image-source.js.map +1 -0
  59. package/src/lib/server.js +19 -1
  60. package/src/lib/server.js.map +1 -1
  61. package/src/lib/utils/commerce-document-pdf.d.ts +77 -0
  62. package/src/lib/utils/commerce-document-pdf.js +608 -0
  63. package/src/lib/utils/commerce-document-pdf.js.map +1 -0
  64. package/src/lib/utils/order-invoice.d.ts +52 -3
  65. package/src/lib/utils/order-invoice.js +116 -64
  66. package/src/lib/utils/order-invoice.js.map +1 -1
@@ -0,0 +1,436 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * @license
5
+ * Copyright 2026 Aglyn LLC
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */ import { checkVisitorUploadSelection, describeVisitorUploadTypes, formatVisitorUploadBytes, VISITOR_UPLOAD_ACCEPT, VISITOR_UPLOAD_HONEYPOT_FIELD, VISITOR_UPLOAD_MAX_BYTES, VISITOR_UPLOAD_MAX_FILES } from "@aglyn/aglyn/app-utils/visitor-upload";
19
+ import { sendVisitorUpload } from "@aglyn/aglyn/app-utils/visitor-upload-client";
20
+ import Alert from "@mui/material/Alert";
21
+ import Box from "@mui/material/Box";
22
+ import Button from "@mui/material/Button";
23
+ import Chip from "@mui/material/Chip";
24
+ import Paper from "@mui/material/Paper";
25
+ import Stack from "@mui/material/Stack";
26
+ import TextField from "@mui/material/TextField";
27
+ import Typography from "@mui/material/Typography";
28
+ import { useCallback, useRef, useState } from "react";
29
+ function dateLabel(atMs) {
30
+ return new Date(atMs).toLocaleDateString(undefined, {
31
+ month: 'short',
32
+ day: 'numeric',
33
+ year: 'numeric'
34
+ });
35
+ }
36
+ const newKey = ()=>{
37
+ var _ref;
38
+ var _globalThis_crypto_randomUUID, _globalThis_crypto;
39
+ return (_ref = (_globalThis_crypto = globalThis.crypto) == null ? void 0 : (_globalThis_crypto_randomUUID = _globalThis_crypto.randomUUID) == null ? void 0 : _globalThis_crypto_randomUUID.call(_globalThis_crypto)) != null ? _ref : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
40
+ };
41
+ /**
42
+ * The quote's proofs on the customer's page (AGL-4096): each image shown,
43
+ * every other file a download, and while the quote is open for an answer,
44
+ * "Approve this proof" (under a typed name) and "Request changes" (a comment
45
+ * and any artwork). The latest answer is the one that counts, so a customer
46
+ * who asked for changes can still approve, and the panel says what they last
47
+ * said. A paid quote shows its proofs and nothing to press.
48
+ */ export function QuoteProofPanel(props) {
49
+ const { view, hostId, quoteId, token, siteFetch, onView, onReload, onReplaced } = props;
50
+ const [mode, setMode] = useState('idle');
51
+ const [name, setName] = useState('');
52
+ const [comment, setComment] = useState('');
53
+ const [files, setFiles] = useState([]);
54
+ const [uploading, setUploading] = useState(null);
55
+ const [sending, setSending] = useState(false);
56
+ const [error, setError] = useState('');
57
+ // Whatever a bot types into the hidden field, sent as typed.
58
+ const [honeypot, setHoneypot] = useState('');
59
+ const picker = useRef(null);
60
+ const attempt = useRef('');
61
+ const link = `hostId=${encodeURIComponent(hostId)}&q=${encodeURIComponent(quoteId)}&t=${encodeURIComponent(token)}`;
62
+ const proofHref = (id)=>`/api/commerce/quote-proof?${link}&file=${encodeURIComponent(id)}`;
63
+ const business = view.storeName || 'the business';
64
+ const post = useCallback(async (path, body, headers = {})=>{
65
+ const response = await siteFetch(path, {
66
+ method: 'POST',
67
+ headers: _extends({
68
+ 'Content-Type': 'application/json'
69
+ }, headers),
70
+ body: JSON.stringify(_extends({
71
+ hostId,
72
+ q: quoteId,
73
+ t: token
74
+ }, body))
75
+ });
76
+ return {
77
+ response,
78
+ payload: await response.json().catch(()=>({}))
79
+ };
80
+ }, [
81
+ siteFetch,
82
+ hostId,
83
+ quoteId,
84
+ token
85
+ ]);
86
+ const attach = useCallback(async (picked)=>{
87
+ setError('');
88
+ const problem = checkVisitorUploadSelection(picked, files.length);
89
+ if (problem) return void setError(problem);
90
+ setUploading({
91
+ done: 0,
92
+ total: picked.length
93
+ });
94
+ try {
95
+ for (const [index, file] of picked.entries()){
96
+ const sent = await sendVisitorUpload({
97
+ file,
98
+ requestTicket: async (request)=>{
99
+ var _payload_error;
100
+ const { response, payload } = await post('/api/commerce/quote-upload', {
101
+ action: 'ticket',
102
+ file: request,
103
+ [VISITOR_UPLOAD_HONEYPOT_FIELD]: honeypot
104
+ });
105
+ return response.ok && payload['ticket'] ? payload : {
106
+ error: String((_payload_error = payload['error']) != null ? _payload_error : 'The file could not be sent. Try again.')
107
+ };
108
+ },
109
+ finalize: async (ticket)=>{
110
+ var _payload_error;
111
+ const { response, payload } = await post('/api/commerce/quote-upload', {
112
+ action: 'finalize',
113
+ ticket
114
+ });
115
+ return response.ok && payload['file'] ? {
116
+ file: payload['file']
117
+ } : {
118
+ error: String((_payload_error = payload['error']) != null ? _payload_error : 'The file could not be sent. Try again.')
119
+ };
120
+ }
121
+ });
122
+ if ('error' in sent) {
123
+ setError(sent.error);
124
+ break;
125
+ }
126
+ setFiles((prev)=>[
127
+ ...prev,
128
+ sent.file
129
+ ]);
130
+ setUploading({
131
+ done: index + 1,
132
+ total: picked.length
133
+ });
134
+ }
135
+ } finally{
136
+ setUploading(null);
137
+ }
138
+ }, [
139
+ files.length,
140
+ post,
141
+ honeypot
142
+ ]);
143
+ const respond = useCallback(async ()=>{
144
+ if (sending) return;
145
+ setError('');
146
+ if (mode === 'approve' && name.trim().length < 2) return void setError('Type your name to approve the proof.');
147
+ if (mode === 'changes' && !comment.trim()) return void setError('Tell us what to change.');
148
+ setSending(true);
149
+ // One key per answer: a retry of the same press is answered from the first.
150
+ if (!attempt.current) attempt.current = newKey();
151
+ try {
152
+ var _payload_error;
153
+ const { response, payload } = await post('/api/commerce/quote-respond', _extends({
154
+ v: view.version,
155
+ action: mode === 'approve' ? 'approve' : 'changes'
156
+ }, mode === 'approve' ? {
157
+ name: name.trim()
158
+ } : {
159
+ comment: comment.trim(),
160
+ fileIds: files.map((file)=>file.id)
161
+ }, {
162
+ [VISITOR_UPLOAD_HONEYPOT_FIELD]: honeypot
163
+ }), {
164
+ 'Idempotency-Key': attempt.current
165
+ });
166
+ attempt.current = '';
167
+ if (response.ok && payload['view']) {
168
+ setMode('idle');
169
+ setName('');
170
+ setComment('');
171
+ setFiles([]);
172
+ onView(payload['view']);
173
+ return;
174
+ }
175
+ if (payload['replaced']) return onReplaced();
176
+ if (payload['reason'] === 'updated' || payload['reason'] === 'paid') onReload();
177
+ setError(String((_payload_error = payload['error']) != null ? _payload_error : 'Your answer could not be sent. Try again.'));
178
+ } catch (unused) {
179
+ setError('Your answer could not be sent. Check your connection and try again.');
180
+ } finally{
181
+ setSending(false);
182
+ }
183
+ }, [
184
+ sending,
185
+ mode,
186
+ name,
187
+ comment,
188
+ files,
189
+ view.version,
190
+ honeypot,
191
+ post,
192
+ onView,
193
+ onReload,
194
+ onReplaced
195
+ ]);
196
+ if (!view.proofs.length) return null;
197
+ const response = view.response;
198
+ return /*#__PURE__*/ _jsx(Paper, {
199
+ variant: "outlined",
200
+ sx: {
201
+ p: {
202
+ xs: 2,
203
+ sm: 3
204
+ }
205
+ },
206
+ children: /*#__PURE__*/ _jsxs(Stack, {
207
+ spacing: 2,
208
+ children: [
209
+ /*#__PURE__*/ _jsxs(Stack, {
210
+ spacing: 0.5,
211
+ children: [
212
+ /*#__PURE__*/ _jsx(Typography, {
213
+ variant: "h6",
214
+ component: "h2",
215
+ children: view.proofs.length === 1 ? 'Your proof' : 'Your proofs'
216
+ }),
217
+ /*#__PURE__*/ _jsx(Typography, {
218
+ variant: "body2",
219
+ color: "text.secondary",
220
+ children: view.status === 'unpaid' ? `Revision ${view.revision}. Look it over, then approve it or tell ${business} what to change.` : `Revision ${view.revision}.`
221
+ })
222
+ ]
223
+ }),
224
+ /*#__PURE__*/ _jsx(Stack, {
225
+ spacing: 1.5,
226
+ children: view.proofs.map((proof)=>proof.inline ? /*#__PURE__*/ _jsxs(Stack, {
227
+ spacing: 0.5,
228
+ children: [
229
+ /*#__PURE__*/ _jsx(Box, {
230
+ component: "img",
231
+ src: proofHref(proof.id),
232
+ alt: proof.name,
233
+ loading: "lazy",
234
+ sx: {
235
+ display: 'block',
236
+ maxWidth: 1,
237
+ height: 'auto',
238
+ borderRadius: 1,
239
+ border: 1,
240
+ borderColor: 'divider'
241
+ }
242
+ }),
243
+ /*#__PURE__*/ _jsx(Typography, {
244
+ variant: "caption",
245
+ color: "text.secondary",
246
+ children: proof.name
247
+ })
248
+ ]
249
+ }, proof.id) : /*#__PURE__*/ _jsx(Box, {
250
+ children: /*#__PURE__*/ _jsx(Button, {
251
+ variant: "outlined",
252
+ size: "small",
253
+ href: proofHref(proof.id),
254
+ download: proof.name,
255
+ children: `Download ${proof.name} (${formatVisitorUploadBytes(proof.size)})`
256
+ })
257
+ }, proof.id))
258
+ }),
259
+ response ? response.action === 'approve' ? /*#__PURE__*/ _jsx(Alert, {
260
+ severity: "success",
261
+ children: `You approved this proof on ${dateLabel(response.atMs)}${response.name ? ` as ${response.name}` : ''}.`
262
+ }) : /*#__PURE__*/ _jsx(Alert, {
263
+ severity: "info",
264
+ children: `You asked for changes on ${dateLabel(response.atMs)}. ${business.charAt(0).toUpperCase()}${business.slice(1)} will send you a new proof.`
265
+ }) : null,
266
+ view.respondable ? /*#__PURE__*/ _jsxs(Stack, {
267
+ spacing: 2,
268
+ children: [
269
+ /*#__PURE__*/ _jsx(Box, {
270
+ component: "input",
271
+ type: "text",
272
+ name: VISITOR_UPLOAD_HONEYPOT_FIELD,
273
+ tabIndex: -1,
274
+ autoComplete: "off",
275
+ "aria-hidden": "true",
276
+ value: honeypot,
277
+ onChange: (event)=>setHoneypot(event.target.value),
278
+ sx: {
279
+ position: 'absolute',
280
+ left: '-100vw',
281
+ width: 0,
282
+ height: 0,
283
+ opacity: 0
284
+ }
285
+ }),
286
+ mode === 'idle' ? /*#__PURE__*/ _jsxs(Stack, {
287
+ direction: {
288
+ xs: 'column',
289
+ sm: 'row'
290
+ },
291
+ spacing: 1,
292
+ children: [
293
+ (response == null ? void 0 : response.action) !== 'approve' ? /*#__PURE__*/ _jsx(Button, {
294
+ variant: "contained",
295
+ onClick: ()=>setMode('approve'),
296
+ children: 'Approve this proof'
297
+ }) : null,
298
+ /*#__PURE__*/ _jsx(Button, {
299
+ variant: (response == null ? void 0 : response.action) === 'approve' ? 'outlined' : 'text',
300
+ onClick: ()=>setMode('changes'),
301
+ children: 'Request changes'
302
+ })
303
+ ]
304
+ }) : mode === 'approve' ? /*#__PURE__*/ _jsxs(Stack, {
305
+ spacing: 1.5,
306
+ children: [
307
+ /*#__PURE__*/ _jsx(Typography, {
308
+ variant: "body2",
309
+ children: `Type your name to approve revision ${view.revision}. ${business.charAt(0).toUpperCase()}${business.slice(1)} keeps your name and the time as the record of your approval.`
310
+ }),
311
+ /*#__PURE__*/ _jsx(TextField, {
312
+ label: "Your name",
313
+ value: name,
314
+ autoComplete: "name",
315
+ onChange: (event)=>setName(event.target.value),
316
+ slotProps: {
317
+ htmlInput: {
318
+ maxLength: 120
319
+ }
320
+ }
321
+ }),
322
+ /*#__PURE__*/ _jsxs(Stack, {
323
+ direction: "row",
324
+ spacing: 1,
325
+ children: [
326
+ /*#__PURE__*/ _jsx(Button, {
327
+ variant: "contained",
328
+ disabled: sending,
329
+ onClick: ()=>void respond(),
330
+ children: sending ? 'Approving…' : 'Approve'
331
+ }),
332
+ /*#__PURE__*/ _jsx(Button, {
333
+ disabled: sending,
334
+ onClick: ()=>setMode('idle'),
335
+ children: 'Cancel'
336
+ })
337
+ ]
338
+ })
339
+ ]
340
+ }) : /*#__PURE__*/ _jsxs(Stack, {
341
+ spacing: 1.5,
342
+ children: [
343
+ /*#__PURE__*/ _jsx(TextField, {
344
+ label: "What should change?",
345
+ value: comment,
346
+ multiline: true,
347
+ minRows: 3,
348
+ required: true,
349
+ onChange: (event)=>setComment(event.target.value),
350
+ slotProps: {
351
+ htmlInput: {
352
+ maxLength: 5000
353
+ }
354
+ }
355
+ }),
356
+ /*#__PURE__*/ _jsx("input", {
357
+ ref: picker,
358
+ type: "file",
359
+ multiple: true,
360
+ hidden: true,
361
+ accept: VISITOR_UPLOAD_ACCEPT,
362
+ "data-testid": "quote-artwork-picker",
363
+ onChange: (event)=>{
364
+ var _event_target_files;
365
+ const picked = Array.from((_event_target_files = event.target.files) != null ? _event_target_files : []);
366
+ event.target.value = '';
367
+ if (picked.length) void attach(picked);
368
+ }
369
+ }),
370
+ files.length ? /*#__PURE__*/ _jsx(Stack, {
371
+ direction: "row",
372
+ spacing: 1,
373
+ useFlexGap: true,
374
+ sx: {
375
+ flexWrap: 'wrap'
376
+ },
377
+ children: files.map((file)=>/*#__PURE__*/ _jsx(Chip, {
378
+ label: `${file.name} · ${formatVisitorUploadBytes(file.size)}`,
379
+ onDelete: sending ? undefined : ()=>setFiles((prev)=>prev.filter((entry)=>entry.id !== file.id))
380
+ }, file.id))
381
+ }) : null,
382
+ /*#__PURE__*/ _jsxs(Box, {
383
+ children: [
384
+ /*#__PURE__*/ _jsx(Button, {
385
+ size: "small",
386
+ variant: "outlined",
387
+ disabled: sending || Boolean(uploading) || files.length >= VISITOR_UPLOAD_MAX_FILES,
388
+ onClick: ()=>{
389
+ var _picker_current;
390
+ return (_picker_current = picker.current) == null ? void 0 : _picker_current.click();
391
+ },
392
+ children: uploading ? `Sending ${uploading.done + 1} of ${uploading.total}…` : 'Attach artwork'
393
+ }),
394
+ /*#__PURE__*/ _jsx(Typography, {
395
+ variant: "caption",
396
+ color: "text.secondary",
397
+ component: "p",
398
+ sx: {
399
+ mt: 0.5
400
+ },
401
+ children: `Optional. ${describeVisitorUploadTypes()}, up to ${VISITOR_UPLOAD_MAX_FILES} files of ${formatVisitorUploadBytes(VISITOR_UPLOAD_MAX_BYTES)} each.`
402
+ })
403
+ ]
404
+ }),
405
+ /*#__PURE__*/ _jsxs(Stack, {
406
+ direction: "row",
407
+ spacing: 1,
408
+ children: [
409
+ /*#__PURE__*/ _jsx(Button, {
410
+ variant: "contained",
411
+ disabled: sending || Boolean(uploading),
412
+ onClick: ()=>void respond(),
413
+ children: sending ? 'Sending…' : 'Send changes'
414
+ }),
415
+ /*#__PURE__*/ _jsx(Button, {
416
+ disabled: sending,
417
+ onClick: ()=>setMode('idle'),
418
+ children: 'Cancel'
419
+ })
420
+ ]
421
+ })
422
+ ]
423
+ }),
424
+ error ? /*#__PURE__*/ _jsx(Alert, {
425
+ severity: "error",
426
+ children: error
427
+ }) : null
428
+ ]
429
+ }) : null
430
+ ]
431
+ })
432
+ });
433
+ }
434
+ export default QuoteProofPanel;
435
+
436
+ //# sourceMappingURL=quote-proof-panel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/components/quote-proof-panel.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n checkVisitorUploadSelection,\n describeVisitorUploadTypes,\n formatVisitorUploadBytes,\n VISITOR_UPLOAD_ACCEPT,\n VISITOR_UPLOAD_HONEYPOT_FIELD,\n VISITOR_UPLOAD_MAX_BYTES,\n VISITOR_UPLOAD_MAX_FILES,\n type VisitorUploadFileView,\n} from '@aglyn/aglyn/app-utils/visitor-upload'\nimport { sendVisitorUpload } from '@aglyn/aglyn/app-utils/visitor-upload-client'\nimport Alert from '@mui/material/Alert'\nimport Box from '@mui/material/Box'\nimport Button from '@mui/material/Button'\nimport Chip from '@mui/material/Chip'\nimport Paper from '@mui/material/Paper'\nimport Stack from '@mui/material/Stack'\nimport TextField from '@mui/material/TextField'\nimport Typography from '@mui/material/Typography'\nimport { useCallback, useRef, useState, type ChangeEvent } from 'react'\nimport type { QuotePageView } from '../model'\n\n/** The page's own fetch: the site's API, with the site's preview rules. */\ntype SiteFetch = (input: string, init?: RequestInit) => Promise<Response>\n\nexport interface QuoteProofPanelProps {\n view: QuotePageView\n hostId: string\n quoteId: string\n token: string\n siteFetch: SiteFetch\n /** The quote as the server answered after a response, or after it changed. */\n onView: (view: QuotePageView) => void\n /** Reload the quote: it changed since the page loaded. */\n onReload: () => void\n /** The link was replaced. */\n onReplaced: () => void\n}\n\nfunction dateLabel(atMs: number): string {\n return new Date(atMs).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })\n}\n\nconst newKey = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`\n\n/**\n * The quote's proofs on the customer's page (AGL-4096): each image shown,\n * every other file a download, and while the quote is open for an answer,\n * \"Approve this proof\" (under a typed name) and \"Request changes\" (a comment\n * and any artwork). The latest answer is the one that counts, so a customer\n * who asked for changes can still approve, and the panel says what they last\n * said. A paid quote shows its proofs and nothing to press.\n */\nexport function QuoteProofPanel(props: QuoteProofPanelProps) {\n const { view, hostId, quoteId, token, siteFetch, onView, onReload, onReplaced } = props\n const [mode, setMode] = useState<'idle' | 'approve' | 'changes'>('idle')\n const [name, setName] = useState('')\n const [comment, setComment] = useState('')\n const [files, setFiles] = useState<VisitorUploadFileView[]>([])\n const [uploading, setUploading] = useState<{ done: number; total: number } | null>(null)\n const [sending, setSending] = useState(false)\n const [error, setError] = useState('')\n // Whatever a bot types into the hidden field, sent as typed.\n const [honeypot, setHoneypot] = useState('')\n const picker = useRef<HTMLInputElement>(null)\n const attempt = useRef('')\n\n const link = `hostId=${encodeURIComponent(hostId)}&q=${encodeURIComponent(quoteId)}&t=${encodeURIComponent(token)}`\n const proofHref = (id: string) => `/api/commerce/quote-proof?${link}&file=${encodeURIComponent(id)}`\n const business = view.storeName || 'the business'\n\n const post = useCallback(\n async (path: string, body: Record<string, unknown>, headers: Record<string, string> = {}) => {\n const response = await siteFetch(path, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...headers },\n body: JSON.stringify({ hostId, q: quoteId, t: token, ...body }),\n })\n return { response, payload: (await response.json().catch(() => ({}))) as Record<string, any> }\n },\n [siteFetch, hostId, quoteId, token],\n )\n\n const attach = useCallback(\n async (picked: File[]) => {\n setError('')\n const problem = checkVisitorUploadSelection(picked, files.length)\n if (problem) return void setError(problem)\n setUploading({ done: 0, total: picked.length })\n try {\n for (const [index, file] of picked.entries()) {\n const sent = await sendVisitorUpload({\n file,\n requestTicket: async (request) => {\n const { response, payload } = await post('/api/commerce/quote-upload', {\n action: 'ticket',\n file: request,\n [VISITOR_UPLOAD_HONEYPOT_FIELD]: honeypot,\n })\n return response.ok && payload['ticket']\n ? (payload as { ticket: string; upload: { url: string; fields: Record<string, string> } })\n : { error: String(payload['error'] ?? 'The file could not be sent. Try again.') }\n },\n finalize: async (ticket) => {\n const { response, payload } = await post('/api/commerce/quote-upload', { action: 'finalize', ticket })\n return response.ok && payload['file']\n ? { file: payload['file'] as VisitorUploadFileView }\n : { error: String(payload['error'] ?? 'The file could not be sent. Try again.') }\n },\n })\n if ('error' in sent) {\n setError(sent.error)\n break\n }\n setFiles((prev) => [...prev, sent.file])\n setUploading({ done: index + 1, total: picked.length })\n }\n } finally {\n setUploading(null)\n }\n },\n [files.length, post, honeypot],\n )\n\n const respond = useCallback(async () => {\n if (sending) return\n setError('')\n if (mode === 'approve' && name.trim().length < 2) return void setError('Type your name to approve the proof.')\n if (mode === 'changes' && !comment.trim()) return void setError('Tell us what to change.')\n setSending(true)\n // One key per answer: a retry of the same press is answered from the first.\n if (!attempt.current) attempt.current = newKey()\n try {\n const { response, payload } = await post(\n '/api/commerce/quote-respond',\n {\n v: view.version,\n action: mode === 'approve' ? 'approve' : 'changes',\n ...(mode === 'approve' ? { name: name.trim() } : { comment: comment.trim(), fileIds: files.map((file) => file.id) }),\n [VISITOR_UPLOAD_HONEYPOT_FIELD]: honeypot,\n },\n { 'Idempotency-Key': attempt.current },\n )\n attempt.current = ''\n if (response.ok && payload['view']) {\n setMode('idle')\n setName('')\n setComment('')\n setFiles([])\n onView(payload['view'] as QuotePageView)\n return\n }\n if (payload['replaced']) return onReplaced()\n if (payload['reason'] === 'updated' || payload['reason'] === 'paid') onReload()\n setError(String(payload['error'] ?? 'Your answer could not be sent. Try again.'))\n } catch {\n setError('Your answer could not be sent. Check your connection and try again.')\n } finally {\n setSending(false)\n }\n }, [sending, mode, name, comment, files, view.version, honeypot, post, onView, onReload, onReplaced])\n\n if (!view.proofs.length) return null\n const response = view.response\n\n return (\n <Paper variant=\"outlined\" sx={{ p: { xs: 2, sm: 3 } }}>\n <Stack spacing={2}>\n <Stack spacing={0.5}>\n <Typography variant=\"h6\" component=\"h2\">\n {view.proofs.length === 1 ? 'Your proof' : 'Your proofs'}\n </Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {view.status === 'unpaid'\n ? `Revision ${view.revision}. Look it over, then approve it or tell ${business} what to change.`\n : `Revision ${view.revision}.`}\n </Typography>\n </Stack>\n\n <Stack spacing={1.5}>\n {view.proofs.map((proof) =>\n proof.inline ? (\n <Stack key={proof.id} spacing={0.5}>\n <Box\n component=\"img\"\n src={proofHref(proof.id)}\n alt={proof.name}\n loading=\"lazy\"\n sx={{ display: 'block', maxWidth: 1, height: 'auto', borderRadius: 1, border: 1, borderColor: 'divider' }}\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {proof.name}\n </Typography>\n </Stack>\n ) : (\n <Box key={proof.id}>\n <Button variant=\"outlined\" size=\"small\" href={proofHref(proof.id)} download={proof.name}>\n {`Download ${proof.name} (${formatVisitorUploadBytes(proof.size)})`}\n </Button>\n </Box>\n ),\n )}\n </Stack>\n\n {response ? (\n response.action === 'approve' ? (\n <Alert severity=\"success\">\n {`You approved this proof on ${dateLabel(response.atMs)}${response.name ? ` as ${response.name}` : ''}.`}\n </Alert>\n ) : (\n <Alert severity=\"info\">\n {`You asked for changes on ${dateLabel(response.atMs)}. ${business.charAt(0).toUpperCase()}${business.slice(1)} will send you a new proof.`}\n </Alert>\n )\n ) : null}\n\n {view.respondable ? (\n <Stack spacing={2}>\n {/* Honeypot: people never see or fill this. */}\n <Box\n component=\"input\"\n type=\"text\"\n name={VISITOR_UPLOAD_HONEYPOT_FIELD}\n tabIndex={-1}\n autoComplete=\"off\"\n aria-hidden=\"true\"\n value={honeypot}\n onChange={(event: ChangeEvent<HTMLInputElement>) => setHoneypot(event.target.value)}\n sx={{ position: 'absolute', left: '-100vw', width: 0, height: 0, opacity: 0 }}\n />\n {mode === 'idle' ? (\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>\n {response?.action !== 'approve' ? (\n <Button variant=\"contained\" onClick={() => setMode('approve')}>\n {'Approve this proof'}\n </Button>\n ) : null}\n <Button variant={response?.action === 'approve' ? 'outlined' : 'text'} onClick={() => setMode('changes')}>\n {'Request changes'}\n </Button>\n </Stack>\n ) : mode === 'approve' ? (\n <Stack spacing={1.5}>\n <Typography variant=\"body2\">\n {`Type your name to approve revision ${view.revision}. ${business.charAt(0).toUpperCase()}${business.slice(1)} keeps your name and the time as the record of your approval.`}\n </Typography>\n <TextField\n label=\"Your name\"\n value={name}\n autoComplete=\"name\"\n onChange={(event) => setName(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n />\n <Stack direction=\"row\" spacing={1}>\n <Button variant=\"contained\" disabled={sending} onClick={() => void respond()}>\n {sending ? 'Approving…' : 'Approve'}\n </Button>\n <Button disabled={sending} onClick={() => setMode('idle')}>\n {'Cancel'}\n </Button>\n </Stack>\n </Stack>\n ) : (\n <Stack spacing={1.5}>\n <TextField\n label=\"What should change?\"\n value={comment}\n multiline\n minRows={3}\n required\n onChange={(event) => setComment(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 5000 } }}\n />\n <input\n ref={picker}\n type=\"file\"\n multiple\n hidden\n accept={VISITOR_UPLOAD_ACCEPT}\n data-testid=\"quote-artwork-picker\"\n onChange={(event) => {\n const picked = Array.from(event.target.files ?? [])\n event.target.value = ''\n if (picked.length) void attach(picked)\n }}\n />\n {files.length ? (\n <Stack direction=\"row\" spacing={1} useFlexGap sx={{ flexWrap: 'wrap' }}>\n {files.map((file) => (\n <Chip\n key={file.id}\n label={`${file.name} · ${formatVisitorUploadBytes(file.size)}`}\n onDelete={sending ? undefined : () => setFiles((prev) => prev.filter((entry) => entry.id !== file.id))}\n />\n ))}\n </Stack>\n ) : null}\n <Box>\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={sending || Boolean(uploading) || files.length >= VISITOR_UPLOAD_MAX_FILES}\n onClick={() => picker.current?.click()}\n >\n {uploading ? `Sending ${uploading.done + 1} of ${uploading.total}…` : 'Attach artwork'}\n </Button>\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"p\" sx={{ mt: 0.5 }}>\n {`Optional. ${describeVisitorUploadTypes()}, up to ${VISITOR_UPLOAD_MAX_FILES} files of ${formatVisitorUploadBytes(VISITOR_UPLOAD_MAX_BYTES)} each.`}\n </Typography>\n </Box>\n <Stack direction=\"row\" spacing={1}>\n <Button variant=\"contained\" disabled={sending || Boolean(uploading)} onClick={() => void respond()}>\n {sending ? 'Sending…' : 'Send changes'}\n </Button>\n <Button disabled={sending} onClick={() => setMode('idle')}>\n {'Cancel'}\n </Button>\n </Stack>\n </Stack>\n )}\n {error ? <Alert severity=\"error\">{error}</Alert> : null}\n </Stack>\n ) : null}\n </Stack>\n </Paper>\n )\n}\n\nexport default QuoteProofPanel\n"],"names":["checkVisitorUploadSelection","describeVisitorUploadTypes","formatVisitorUploadBytes","VISITOR_UPLOAD_ACCEPT","VISITOR_UPLOAD_HONEYPOT_FIELD","VISITOR_UPLOAD_MAX_BYTES","VISITOR_UPLOAD_MAX_FILES","sendVisitorUpload","Alert","Box","Button","Chip","Paper","Stack","TextField","Typography","useCallback","useRef","useState","dateLabel","atMs","Date","toLocaleDateString","undefined","month","day","year","newKey","globalThis","crypto","randomUUID","now","Math","random","toString","slice","QuoteProofPanel","props","view","hostId","quoteId","token","siteFetch","onView","onReload","onReplaced","mode","setMode","name","setName","comment","setComment","files","setFiles","uploading","setUploading","sending","setSending","error","setError","honeypot","setHoneypot","picker","attempt","link","encodeURIComponent","proofHref","id","business","storeName","post","path","body","headers","response","method","JSON","stringify","q","t","payload","json","catch","attach","picked","problem","length","done","total","index","file","entries","sent","requestTicket","request","action","ok","String","finalize","ticket","prev","respond","trim","current","v","version","fileIds","map","proofs","variant","sx","p","xs","sm","spacing","component","color","status","revision","proof","inline","src","alt","loading","display","maxWidth","height","borderRadius","border","borderColor","size","href","download","severity","charAt","toUpperCase","respondable","type","tabIndex","autoComplete","aria-hidden","value","onChange","event","target","position","left","width","opacity","direction","onClick","label","slotProps","htmlInput","maxLength","disabled","multiline","minRows","required","input","ref","multiple","hidden","accept","data-testid","Array","from","useFlexGap","flexWrap","onDelete","filter","entry","Boolean","click","mt"],"mappings":"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"}
@@ -48,6 +48,25 @@ export interface QuotePageLine {
48
48
  }
49
49
  /** Where the quote stands, as its customer sees it. */
50
50
  export type QuotePageStatus = 'unpaid' | 'paid' | 'canceled' | 'refunded';
51
+ /** A proof as the quote page lists it: no storage path, nothing about who attached it. */
52
+ export interface QuotePageProof {
53
+ id: string;
54
+ name: string;
55
+ contentType: string;
56
+ size: number;
57
+ /** Shown in the page as a picture; everything else is a download. */
58
+ inline: boolean;
59
+ }
60
+ /** The customer's latest answer to the revision the page shows. */
61
+ export interface QuotePageResponse {
62
+ action: 'approve' | 'changes';
63
+ atMs: number;
64
+ /** With an approval: the name it was given under. */
65
+ name: string | null;
66
+ comment: string | null;
67
+ /** How many files came with it. */
68
+ files: number;
69
+ }
51
70
  /**
52
71
  * What the quote page shows. Allow-listed, never spread: the order document
53
72
  * carries the customer's email and phone, Stripe ids, the business's own
@@ -96,6 +115,14 @@ export interface QuotePageView {
96
115
  approvalRequired: boolean;
97
116
  /** A Pay button belongs on the page. */
98
117
  payable: boolean;
118
+ /** The revision the page shows (AGL-4096). */
119
+ revision: number;
120
+ /** That revision's proofs. */
121
+ proofs: QuotePageProof[];
122
+ /** The customer's latest answer to it, if any. */
123
+ response: QuotePageResponse | null;
124
+ /** Approve and Request changes belong on the page. */
125
+ respondable: boolean;
99
126
  }
100
127
  /** What a signed link for an older version of the quote opens: nothing of the quote. */
101
128
  export interface QuotePageReplacedView {
@@ -1,3 +1,4 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
1
2
  /**
2
3
  * @license
3
4
  * Copyright 2026 Aglyn LLC
@@ -13,9 +14,10 @@
13
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
15
  * See the License for the specific language governing permissions and
15
16
  * limitations under the License.
16
- */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ */ import { visitorUploadIsInline } from "@aglyn/aglyn/app-utils/visitor-upload";
17
18
  import { appendOrderEvent, formatOrderNumber } from "./commerce-orders.js";
18
19
  import { computeQuoteTotals, describeQuoteTax, formatQuoteNumber, quoteLineAmounts, quotePaymentBlockedByApproval } from "./commerce-quotes.js";
20
+ import { currentQuoteProofs, latestQuoteResponse, quoteRespondRefusal } from "./commerce-quote-proofs.js";
19
21
  import { engineLineTaxPercentages } from "./commerce-tax-engine.js";
20
22
  import { withOrderListFields } from "./order-list-fields.js";
21
23
  /*==========================================
@@ -99,7 +101,7 @@ function discountLabel(discount) {
99
101
  * are the stored lines' through {@link computeQuoteTotals} until the quote is
100
102
  * paid, and the paid order's own totals (what Stripe charged) after.
101
103
  */ export function buildQuotePageView(input) {
102
- var _order_lineItems, _ref, _text, _input_receiptUrl;
104
+ var _order_lineItems, _ref, _text, _input_receiptUrl, _ref1;
103
105
  var _quote_tax, _quote_customer, _quote_customer1, _quote_customer2, _quote_customer3, _quote_tax1;
104
106
  const { order } = input;
105
107
  const quote = order.quote;
@@ -162,7 +164,38 @@ function discountLabel(discount) {
162
164
  paidAtMs: paidAt(order),
163
165
  receiptUrl: unpaid ? null : (_input_receiptUrl = input.receiptUrl) != null ? _input_receiptUrl : null,
164
166
  approvalRequired,
165
- payable: unpaid && !approvalRequired && quotePayRefusal(order, input.orderId) === null
167
+ payable: unpaid && !approvalRequired && quotePayRefusal(order, input.orderId) === null,
168
+ revision: Math.max(1, Number((_ref1 = quote == null ? void 0 : quote.revision) != null ? _ref1 : 1) || 1),
169
+ proofs: currentQuoteProofs(quote).map((proof)=>{
170
+ var _proof_name, _proof_contentType, _proof_contentType1;
171
+ return {
172
+ id: String(proof.id),
173
+ name: String((_proof_name = proof.name) != null ? _proof_name : 'Proof'),
174
+ contentType: String((_proof_contentType = proof.contentType) != null ? _proof_contentType : ''),
175
+ size: Math.max(0, cents(proof.size)),
176
+ inline: visitorUploadIsInline(String((_proof_contentType1 = proof.contentType) != null ? _proof_contentType1 : ''))
177
+ };
178
+ }),
179
+ response: pageResponse(quote),
180
+ // The page is only ever a quote's, whatever the caller picked off the order.
181
+ respondable: quoteRespondRefusal({
182
+ channel: 'quote',
183
+ status: order.status,
184
+ quote
185
+ }) === null
186
+ };
187
+ }
188
+ /** The latest answer, as the page may show it back to the link's holder: never the address or browser. */ function pageResponse(quote) {
189
+ var _ref;
190
+ var _latest_files;
191
+ const latest = latestQuoteResponse(quote);
192
+ if (!latest) return null;
193
+ return {
194
+ action: latest.action === 'approve' ? 'approve' : 'changes',
195
+ atMs: Number(latest.atMs) || 0,
196
+ name: latest.action === 'approve' ? text(latest.name) : null,
197
+ comment: latest.action === 'changes' ? text(latest.comment) : null,
198
+ files: (_ref = (_latest_files = latest.files) == null ? void 0 : _latest_files.length) != null ? _ref : 0
166
199
  };
167
200
  }
168
201
  const LINE_DESCRIPTION_MAX = 500;