@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.
- package/README.md +2 -2
- package/package.json +13 -13
- package/src/lib/components/console/commerce-pdf-download.d.ts +30 -0
- package/src/lib/components/console/commerce-pdf-download.js +53 -0
- package/src/lib/components/console/commerce-pdf-download.js.map +1 -0
- package/src/lib/components/console/host-quotes-card.component.js +20 -4
- package/src/lib/components/console/host-quotes-card.component.js.map +1 -1
- package/src/lib/components/console/order-invoice.component.d.ts +6 -4
- package/src/lib/components/console/order-invoice.component.js +67 -11
- package/src/lib/components/console/order-invoice.component.js.map +1 -1
- package/src/lib/components/console/quote-editor-dialog.component.js +53 -2
- package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/quote-proofs.component.d.ts +37 -0
- package/src/lib/components/console/quote-proofs.component.js +626 -0
- package/src/lib/components/console/quote-proofs.component.js.map +1 -0
- package/src/lib/components/quote-page-body.js +29 -1
- package/src/lib/components/quote-page-body.js.map +1 -1
- package/src/lib/components/quote-proof-panel.d.ts +42 -0
- package/src/lib/components/quote-proof-panel.js +436 -0
- package/src/lib/components/quote-proof-panel.js.map +1 -0
- package/src/lib/model/commerce-quote-pay.d.ts +27 -0
- package/src/lib/model/commerce-quote-pay.js +36 -3
- package/src/lib/model/commerce-quote-pay.js.map +1 -1
- package/src/lib/model/commerce-quote-proofs.d.ts +159 -0
- package/src/lib/model/commerce-quote-proofs.js +348 -0
- package/src/lib/model/commerce-quote-proofs.js.map +1 -0
- package/src/lib/model/commerce-quotes.d.ts +59 -3
- package/src/lib/model/commerce-quotes.js.map +1 -1
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/server/commerce-pdf.d.ts +100 -0
- package/src/lib/server/commerce-pdf.js +283 -0
- package/src/lib/server/commerce-pdf.js.map +1 -0
- package/src/lib/server/order-print.d.ts +6 -0
- package/src/lib/server/order-print.js +22 -5
- package/src/lib/server/order-print.js.map +1 -1
- package/src/lib/server/pdf-response.d.ts +31 -0
- package/src/lib/server/pdf-response.js +30 -0
- package/src/lib/server/pdf-response.js.map +1 -0
- package/src/lib/server/pos-reader-splash.d.ts +2 -18
- package/src/lib/server/pos-reader-splash.js +8 -93
- package/src/lib/server/pos-reader-splash.js.map +1 -1
- package/src/lib/server/quote-pay.d.ts +31 -4
- package/src/lib/server/quote-pay.js +11 -11
- package/src/lib/server/quote-pay.js.map +1 -1
- package/src/lib/server/quote-proofs.d.ts +51 -0
- package/src/lib/server/quote-proofs.js +500 -0
- package/src/lib/server/quote-proofs.js.map +1 -0
- package/src/lib/server/quote-uploads.d.ts +33 -0
- package/src/lib/server/quote-uploads.js +29 -0
- package/src/lib/server/quote-uploads.js.map +1 -0
- package/src/lib/server/quote.d.ts +14 -0
- package/src/lib/server/quote.js +336 -3
- package/src/lib/server/quote.js.map +1 -1
- package/src/lib/server/site-image-source.d.ts +38 -0
- package/src/lib/server/site-image-source.js +130 -0
- package/src/lib/server/site-image-source.js.map +1 -0
- package/src/lib/server.js +19 -1
- package/src/lib/server.js.map +1 -1
- package/src/lib/utils/commerce-document-pdf.d.ts +77 -0
- package/src/lib/utils/commerce-document-pdf.js +608 -0
- package/src/lib/utils/commerce-document-pdf.js.map +1 -0
- package/src/lib/utils/order-invoice.d.ts +52 -3
- package/src/lib/utils/order-invoice.js +116 -64
- 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 {
|
|
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;
|