@riosst100/pwa-marketplace 3.2.9 → 3.3.0
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/package.json +1 -1
- package/src/components/LiveChat/MessagesModal.js +3 -3
- package/src/components/Messages/messages.js +99 -116
- package/src/components/Messages/messages.module.css +15 -0
- package/src/components/RFQ/modalRfq.js +90 -18
- package/src/components/RFQPage/quoteDetail.js +48 -30
- package/src/overwrites/venia-ui/lib/components/ProductFullDetail/productFullDetail.js +8 -2
- package/src/talons/MessagesPage/messagesPage.gql.js +4 -0
- package/src/talons/RFQ/rfq.gql.js +8 -0
- package/src/talons/RFQ/useRFQ.js +31 -6
- package/src/talons/Seller/useSeller.js +4 -1
package/package.json
CHANGED
|
@@ -173,7 +173,7 @@ const MessagesModal = ({
|
|
|
173
173
|
onClick={(e) => e.stopPropagation()}
|
|
174
174
|
>
|
|
175
175
|
<div className='flex items-center justify-between mb-3'>
|
|
176
|
-
<h3 className='text-[18px] font-semibold'>
|
|
176
|
+
<h3 className='text-[18px] font-semibold'>Send Message</h3>
|
|
177
177
|
<button onClick={onClose} aria-label='Close' className='text-gray-600 text-[30px] hover:text-black'>×</button>
|
|
178
178
|
</div>
|
|
179
179
|
<div className='flex gap-4'>
|
|
@@ -184,7 +184,7 @@ const MessagesModal = ({
|
|
|
184
184
|
|
|
185
185
|
<form onSubmit={handleSubmitNewMessage} className='mb-3 rounded-[6px] p-1 flex flex-col gap-2'>
|
|
186
186
|
<div className='text-[13px] font-semibold flex items-center justify-between'>
|
|
187
|
-
<span>
|
|
187
|
+
<span>Send Message to {seller?.name}</span>
|
|
188
188
|
{/* {(sortedThreads.length > 0) && (
|
|
189
189
|
<button
|
|
190
190
|
type='button'
|
|
@@ -217,7 +217,7 @@ const MessagesModal = ({
|
|
|
217
217
|
aria-busy={isSendingNew}
|
|
218
218
|
>
|
|
219
219
|
{isSendingNew
|
|
220
|
-
? formatMessage({ id: 'messages.compose.sending', defaultMessage: '
|
|
220
|
+
? formatMessage({ id: 'messages.compose.sending', defaultMessage: 'Sending Message...' })
|
|
221
221
|
: formatMessage({ id: 'messages.compose.send', defaultMessage: 'Send Message' })}
|
|
222
222
|
</button>
|
|
223
223
|
</div>
|
|
@@ -8,6 +8,8 @@ import defaultClasses from './messages.module.css';
|
|
|
8
8
|
import cn from 'classnames';
|
|
9
9
|
import React, { useMemo, useState, useEffect } from 'react';
|
|
10
10
|
import ChatContent from '@riosst100/pwa-marketplace/src/components/LiveChat/chatContent';
|
|
11
|
+
import { Link } from "react-router-dom";
|
|
12
|
+
import { Star1, Verify, Sms, Message, Shop, ArrowUp2 } from 'iconsax-react';
|
|
11
13
|
|
|
12
14
|
const Messages = props => {
|
|
13
15
|
const {
|
|
@@ -61,29 +63,32 @@ const Messages = props => {
|
|
|
61
63
|
}, [messages, seller]);
|
|
62
64
|
|
|
63
65
|
const sortedThreads = useMemo(() => {
|
|
64
|
-
return [...threads].sort((a, b) => new Date(
|
|
66
|
+
return [...threads].sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
|
65
67
|
}, [threads]);
|
|
68
|
+
|
|
69
|
+
const { location } = globalThis;
|
|
70
|
+
|
|
71
|
+
const query = new URLSearchParams(location.search);
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
// console.log('query',query)
|
|
75
|
+
// console.log('messageId',messageId)
|
|
66
76
|
|
|
67
77
|
useEffect(() => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (selectLatestAfterSend) {
|
|
80
|
-
setSelectedThread(latest);
|
|
81
|
-
setSelectLatestAfterSend(false);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
78
|
+
const messageId = query.get('id') || null;
|
|
79
|
+
console.log('query',query)
|
|
80
|
+
console.log('messageId',messageId)
|
|
81
|
+
|
|
82
|
+
if (messageId && sortedThreads?.length) {
|
|
83
|
+
const selectedThread = sortedThreads.find(t => t.message_id == messageId);
|
|
84
|
+
console.log('sortedThreads',sortedThreads)
|
|
85
|
+
console.log('selectedThread',selectedThread)
|
|
86
|
+
if (selectedThread) {
|
|
87
|
+
console.log('selectedThread',selectedThread)
|
|
88
|
+
setSelectedThread(selectedThread);
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
|
-
}, [sortedThreads,
|
|
91
|
+
}, [sortedThreads, query]);
|
|
87
92
|
|
|
88
93
|
// Tandai sudah pernah load minimal sekali untuk mencegah flicker
|
|
89
94
|
useEffect(() => {
|
|
@@ -165,6 +170,9 @@ const Messages = props => {
|
|
|
165
170
|
setSelectLatestAfterSend(true);
|
|
166
171
|
};
|
|
167
172
|
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
168
176
|
return (
|
|
169
177
|
<>
|
|
170
178
|
<div className={cn(classes.leftContent, '')}>
|
|
@@ -176,124 +184,99 @@ const Messages = props => {
|
|
|
176
184
|
</StoreTitle>
|
|
177
185
|
<div className={cn(classes.leftContentContainer, 'border !border-gray-100 lg_rounded-md !rounded-lg')}>
|
|
178
186
|
<div class="auth-left">
|
|
179
|
-
<div className='px-3 py-
|
|
187
|
+
<div className='text-[16px] px-3 py-3 border-gray-100 border-b font-medium flex items-center justify-between'>
|
|
180
188
|
<span>{formatMessage({ id: 'messages.sidebar.title', defaultMessage: 'Messages' })}</span>
|
|
181
|
-
<button
|
|
182
|
-
type='button'
|
|
183
|
-
onClick={() => {
|
|
184
|
-
setIsComposing(true);
|
|
185
|
-
setSelectedThread(null);
|
|
186
|
-
}}
|
|
187
|
-
className='text-[12px] px-2 py-1 rounded border border-[#FF6E26] text-[#FF6E26] hover:bg-[#FF6E26] hover:text-white'
|
|
188
|
-
>
|
|
189
|
-
{formatMessage({ id: 'messages.compose.newMessage', defaultMessage: 'Send New Message' })}
|
|
190
|
-
</button>
|
|
191
189
|
</div>
|
|
192
190
|
<div className='max-h-[420px] overflow-y-auto'>
|
|
193
|
-
{(!
|
|
191
|
+
{(!hasLoadedOnce && loading) ? (
|
|
194
192
|
<div className='p-3 text-sm text-gray-500'>{formatMessage({ id: 'messages.loading', defaultMessage: 'Loading…' })}</div>
|
|
193
|
+
) : !sortedThreads || sortedThreads.length === 0 ? (
|
|
194
|
+
<div className={cn(classes.noMessages, 'text-sm text-gray-500')}>{formatMessage({ id: 'messages.detail.noMessages', defaultMessage: 'No messages yet.' })}</div>
|
|
195
195
|
) : (
|
|
196
|
-
sortedThreads.map(t =>
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
196
|
+
sortedThreads && sortedThreads.length && sortedThreads.map(t =>
|
|
197
|
+
// console.log('fffff',t)
|
|
198
|
+
<div>
|
|
199
|
+
<hr className="!border-gray-100" style={{"width": "100%"}} />
|
|
200
|
+
<button
|
|
201
|
+
key={t.message_id}
|
|
202
|
+
onClick={() => setSelectedThread(t)}
|
|
203
|
+
className={cn('w-full text-left px-3 py-2 border-b hover:bg-gray-50',
|
|
204
|
+
selectedThread?.message_id === t.message_id && 'bg-gray-100')}
|
|
205
|
+
>
|
|
206
|
+
<div className='flex items-center justify-between gap-2'>
|
|
207
|
+
<div style={{"color": "#f76b1c"}}className='text-[12px] font-semibold line-clamp-1'>{t.seller ? t.seller.name : 'Unknown Seller' }</div>
|
|
208
|
+
{/* <div className='text-[12px] flex-shrink-0'>
|
|
209
|
+
{renderStatusBadge(t.status)}
|
|
210
|
+
</div> */}
|
|
211
|
+
</div>
|
|
212
|
+
<div className='flex items-center justify-between gap-2'>
|
|
213
|
+
<div className='text-[12px] line-clamp-1'>Subject: {t.subject || formatMessage({ id: 'messages.noSubject', defaultMessage: 'No Subject' })}</div>
|
|
214
|
+
{/* <div className='text-[12px] flex-shrink-0'>
|
|
215
|
+
{renderStatusBadge(t.status)}
|
|
216
|
+
</div> */}
|
|
217
|
+
</div>
|
|
218
|
+
<div className='text-[11px] text-gray-500 mt-1'>
|
|
219
|
+
{t.details.total_count ? t.details.items[t.details.items.length-1].content : t.description}
|
|
220
|
+
</div>
|
|
221
|
+
<div className='text-[10px] text-gray-500 mt-2'>{new Date(t.created_at).toLocaleString()}</div>
|
|
222
|
+
</button>
|
|
223
|
+
</div>
|
|
224
|
+
)
|
|
212
225
|
)}
|
|
213
226
|
</div>
|
|
214
227
|
</div>
|
|
215
228
|
</div>
|
|
216
229
|
</div>
|
|
217
230
|
<div className={classes.root}>
|
|
218
|
-
<div className="lg_border-2 lg_border-solid lg_border-subtle lg_rounded-md !border !border-gray-100 p-6 !rounded-lg">
|
|
231
|
+
<div style={{"padding": "20px"}} className="lg_border-2 lg_border-solid lg_border-subtle lg_rounded-md !border !border-gray-100 p-6 !rounded-lg">
|
|
219
232
|
{/* Compose new message (shown only when composing OR when no thread exists) */}
|
|
220
|
-
{
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
{
|
|
225
|
-
<
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
/>
|
|
241
|
-
<textarea
|
|
242
|
-
rows={3}
|
|
243
|
-
className='border border-gray-100 rounded px-3 py-2 outline-none focus:ring-2 focus:ring-[#FF6E26]'
|
|
244
|
-
placeholder={formatMessage({ id: 'messages.compose.contentPlaceholder', defaultMessage: 'Write a message to the seller…' })}
|
|
245
|
-
value={newContent}
|
|
246
|
-
onChange={(e) => setNewContent(e.target.value)}
|
|
247
|
-
/>
|
|
248
|
-
<div className='flex justify-end'>
|
|
233
|
+
{sortedThreads.length > 0 && (
|
|
234
|
+
<div className='flex items-center justify-between mb-2'>
|
|
235
|
+
{selectedThread && (
|
|
236
|
+
<>
|
|
237
|
+
<div style={{"textAlign": "left"}}>
|
|
238
|
+
{selectedThread.seller && <Link to={"/seller/"+selectedThread.seller.url_key}>
|
|
239
|
+
{/* <div class="flex items-center justify-center gap-[10px] relative">
|
|
240
|
+
<Shop color="#f76b1c" size={14} variant="Outline" className='stroke-[#f76b1c]' />
|
|
241
|
+
<div class="relative xs_hidden lg_flex w-fit text-[12px] text-[#f76b1c] text-[14px] tracking-[0] leading-[15px] whitespace-nowrap">
|
|
242
|
+
Visit Store
|
|
243
|
+
</div>
|
|
244
|
+
</div> */}
|
|
245
|
+
<div style={{"color": "#f76b1c"}}className='text-[14px] font-semibold line-clamp-1'>{selectedThread.seller ? selectedThread.seller.name : 'Unknown Seller' }</div>
|
|
246
|
+
</Link>}
|
|
247
|
+
<div className='mt-3 mb-1 text-[12px]'>Subject: {selectedThread?.subject || formatMessage({ id: 'messages.noSubject', defaultMessage: 'No Subject' })}</div>
|
|
248
|
+
{/* <div className='text-[12px] text-gray-600 flex items-center gap-2'>
|
|
249
|
+
<span>{formatMessage({ id: 'messages.detail.status', defaultMessage: 'Status:' })}</span>
|
|
250
|
+
{selectedThread && renderStatusBadge(selectedThread.status)}
|
|
251
|
+
</div> */}
|
|
252
|
+
</div>
|
|
249
253
|
<button
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
disabled
|
|
253
|
-
aria-busy={isSendingNew}
|
|
254
|
+
onClick={handleDeleteThread}
|
|
255
|
+
disabled={isDeleting}
|
|
256
|
+
className='text-[12px] px-3 py-1 rounded border text-red-600 border-red-300 hover:bg-red-50 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2'
|
|
254
257
|
>
|
|
255
|
-
{
|
|
256
|
-
|
|
257
|
-
|
|
258
|
+
{isDeleting && (
|
|
259
|
+
<span className='w-3 h-3 border-2 border-red-200 border-t-red-500 rounded-full animate-spin' aria-hidden='true'></span>
|
|
260
|
+
)}
|
|
261
|
+
{isDeleting
|
|
262
|
+
? formatMessage({ id: 'messages.detail.deleting', defaultMessage: 'Deleting...' })
|
|
263
|
+
: formatMessage({ id: 'messages.detail.delete', defaultMessage: 'Delete Message' })}
|
|
258
264
|
</button>
|
|
259
|
-
|
|
260
|
-
</form>
|
|
261
|
-
)}
|
|
262
|
-
{!isComposing && sortedThreads.length > 0 && (
|
|
263
|
-
<div className='flex items-center justify-between mb-2'>
|
|
264
|
-
<div>
|
|
265
|
-
<div className='text-[14px] font-semibold'>{selectedThread?.subject || formatMessage({ id: 'messages.noSubject', defaultMessage: 'No Subject' })}</div>
|
|
266
|
-
{/* <div className='text-[12px] text-gray-600 flex items-center gap-2'>
|
|
267
|
-
<span>{formatMessage({ id: 'messages.detail.status', defaultMessage: 'Status:' })}</span>
|
|
268
|
-
{selectedThread && renderStatusBadge(selectedThread.status)}
|
|
269
|
-
</div> */}
|
|
270
|
-
</div>
|
|
271
|
-
{selectedThread && (
|
|
272
|
-
<button
|
|
273
|
-
onClick={handleDeleteThread}
|
|
274
|
-
disabled={isDeleting}
|
|
275
|
-
className='text-[12px] px-3 py-1 rounded border text-red-600 border-red-300 hover:bg-red-50 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2'
|
|
276
|
-
>
|
|
277
|
-
{isDeleting && (
|
|
278
|
-
<span className='w-3 h-3 border-2 border-red-200 border-t-red-500 rounded-full animate-spin' aria-hidden='true'></span>
|
|
279
|
-
)}
|
|
280
|
-
{isDeleting
|
|
281
|
-
? formatMessage({ id: 'messages.detail.deleting', defaultMessage: 'Deleting...' })
|
|
282
|
-
: formatMessage({ id: 'messages.detail.delete', defaultMessage: 'Delete Message' })}
|
|
283
|
-
</button>
|
|
265
|
+
</>
|
|
284
266
|
)}
|
|
285
267
|
</div>
|
|
286
268
|
)}
|
|
287
|
-
{
|
|
288
|
-
|
|
289
|
-
{selectedThread ? (
|
|
269
|
+
{sortedThreads.length > 0 && selectedThread ? (
|
|
270
|
+
<div className='!border-gray-100 border rounded-[6px] p-3 flex-1 min-h-[320px]'>
|
|
290
271
|
<ChatContent chatData={chatData} />
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
272
|
+
</div>
|
|
273
|
+
) : (
|
|
274
|
+
<div className={classes.noConversationContainer}>
|
|
275
|
+
<img src="https://cdni.iconscout.com/illustration/premium/thumb/empty-message-illustration-svg-download-png-8593289.png" style={{"width":"20%"}}/>
|
|
276
|
+
<div className='text-sm text-gray-500'>{formatMessage({ id: 'messages.detail.noConversation', defaultMessage: 'Select a message to view the conversation.' })}</div>
|
|
277
|
+
</div>
|
|
295
278
|
)}
|
|
296
|
-
{
|
|
279
|
+
{sortedThreads.length > 0 && selectedThread && (
|
|
297
280
|
<form onSubmit={handleSubmitReply} className='mt-3 flex gap-2'>
|
|
298
281
|
{(() => {
|
|
299
282
|
const statusNum = Number(selectedThread.status);
|
|
@@ -29,6 +29,21 @@
|
|
|
29
29
|
text-align: left;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
.noConversationContainer {
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-wrap: wrap;
|
|
36
|
+
align-content: center;
|
|
37
|
+
justify-content: flex-start;
|
|
38
|
+
align-items: center;
|
|
39
|
+
row-gap: 15px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.noMessages {
|
|
43
|
+
text-align: center;
|
|
44
|
+
padding: 60px;
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
.rootContainer {
|
|
33
48
|
justify-content: space-around;
|
|
34
49
|
display: flex;
|
|
@@ -30,15 +30,42 @@ const RFQModalForm = (props) => {
|
|
|
30
30
|
|
|
31
31
|
// Upload File with remove capability
|
|
32
32
|
const [uploadedFiles, setUploadedFiles] = useState([]);
|
|
33
|
+
// Calculate safe raw-size limit to keep base64 payload under 2MB
|
|
34
|
+
const GRAPHQL_BASE64_LIMIT = 2 * 1024 * 1024;
|
|
35
|
+
const SAFE_RAW_LIMIT = Math.floor((GRAPHQL_BASE64_LIMIT * 3) / 4) - 8 * 1024;
|
|
36
|
+
const estimateBase64Size = size => Math.ceil((4 * size) / 3);
|
|
37
|
+
|
|
33
38
|
const onDrop = useCallback((accepted) => {
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
const safe = [];
|
|
40
|
+
const rejected = [];
|
|
41
|
+
accepted.forEach(f => {
|
|
42
|
+
const est = estimateBase64Size(f.size || 0);
|
|
43
|
+
if (est <= GRAPHQL_BASE64_LIMIT) {
|
|
44
|
+
safe.push(f);
|
|
45
|
+
} else {
|
|
46
|
+
rejected.push({ name: f.name || f.path, size: f.size });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
if (safe.length) {
|
|
50
|
+
setUploadedFiles(prev => [...prev, ...safe]);
|
|
51
|
+
}
|
|
52
|
+
if (rejected.length) {
|
|
53
|
+
addToast({
|
|
54
|
+
type: 'error',
|
|
55
|
+
message: formatMessage({
|
|
56
|
+
id: 'productFullDetail.rfqFileTooLarge',
|
|
57
|
+
defaultMessage: 'One or more files are too large. Max ~1.5MB each.'
|
|
58
|
+
}),
|
|
59
|
+
timeout: 4000
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}, [addToast, formatMessage]);
|
|
36
63
|
|
|
37
64
|
const {
|
|
38
65
|
getRootProps,
|
|
39
66
|
getInputProps,
|
|
40
67
|
isDragActive
|
|
41
|
-
} = useDropzone({ onDrop });
|
|
68
|
+
} = useDropzone({ onDrop, maxSize: SAFE_RAW_LIMIT, multiple: true });
|
|
42
69
|
|
|
43
70
|
const handleRemoveFile = useCallback((index) => {
|
|
44
71
|
setUploadedFiles(prev => prev.filter((_, i) => i !== index));
|
|
@@ -59,8 +86,60 @@ const RFQModalForm = (props) => {
|
|
|
59
86
|
</div>
|
|
60
87
|
));
|
|
61
88
|
|
|
89
|
+
// Helper: convert File to base64 string (without data URL prefix)
|
|
90
|
+
const toBase64 = useCallback(
|
|
91
|
+
file =>
|
|
92
|
+
new Promise((resolve, reject) => {
|
|
93
|
+
try {
|
|
94
|
+
const reader = new FileReader();
|
|
95
|
+
reader.onload = () => {
|
|
96
|
+
const result = reader.result;
|
|
97
|
+
// reader.result is a Data URL: "data:<mime>;base64,<payload>"
|
|
98
|
+
const base64 = typeof result === 'string' && result.includes(',')
|
|
99
|
+
? result.split(',')[1]
|
|
100
|
+
: '';
|
|
101
|
+
resolve(base64);
|
|
102
|
+
};
|
|
103
|
+
reader.onerror = reject;
|
|
104
|
+
reader.readAsDataURL(file);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
reject(e);
|
|
107
|
+
}
|
|
108
|
+
}),
|
|
109
|
+
[]
|
|
110
|
+
);
|
|
111
|
+
|
|
62
112
|
const handleSubmit = useCallback(async values => {
|
|
63
|
-
console.log('[RFQ] submit values', values);
|
|
113
|
+
// console.log('[RFQ] submit values', values);
|
|
114
|
+
// Prepare base64 conversions if any files were uploaded
|
|
115
|
+
let filesPayload = undefined;
|
|
116
|
+
if (uploadedFiles && uploadedFiles.length) {
|
|
117
|
+
// Hard guard: abort if any file would exceed limit after base64 encoding
|
|
118
|
+
const oversize = uploadedFiles.find(f => estimateBase64Size(f.size || 0) > GRAPHQL_BASE64_LIMIT);
|
|
119
|
+
if (oversize) {
|
|
120
|
+
addToast({
|
|
121
|
+
type: 'error',
|
|
122
|
+
message: formatMessage({
|
|
123
|
+
id: 'productFullDetail.rfqFileTooLargeSubmit',
|
|
124
|
+
defaultMessage: 'A selected file is too large (limit ~1.5MB).'
|
|
125
|
+
}),
|
|
126
|
+
timeout: 4000
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const base64List = await Promise.all(
|
|
132
|
+
uploadedFiles.map(file => toBase64(file))
|
|
133
|
+
);
|
|
134
|
+
filesPayload = uploadedFiles.map((file, i) => ({
|
|
135
|
+
file_name: (file && (file.name || file.path)) || '',
|
|
136
|
+
file_type: (file && file.type) ? file.type : 'application/octet-stream',
|
|
137
|
+
base64: base64List[i] || ''
|
|
138
|
+
}));
|
|
139
|
+
} catch (e) {
|
|
140
|
+
console.error('[RFQ] file base64 conversion failed', e);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
64
143
|
const input = {
|
|
65
144
|
product_id: productId,
|
|
66
145
|
quantity: values.quote_qty ? Number(values.quote_qty) : undefined,
|
|
@@ -70,24 +149,16 @@ const RFQModalForm = (props) => {
|
|
|
70
149
|
? Number(values.quote_price_per_unit)
|
|
71
150
|
: undefined,
|
|
72
151
|
date_need_quote: startDate ? startDate.toISOString() : undefined,
|
|
73
|
-
files:
|
|
74
|
-
? uploadedFiles.map(file => ({
|
|
75
|
-
file_name: (file && (file.name || file.path)) || '',
|
|
76
|
-
// Browsers do not expose real file system paths; use name as a stable placeholder
|
|
77
|
-
file_path: (file && (file.path || file.name)) || '',
|
|
78
|
-
// Ensure a valid MIME string; default if missing
|
|
79
|
-
file_type: (file && file.type) ? file.type : 'application/octet-stream'
|
|
80
|
-
}))
|
|
81
|
-
: undefined
|
|
152
|
+
files: filesPayload
|
|
82
153
|
};
|
|
83
154
|
|
|
84
|
-
console.log('[RFQ] input payload', input);
|
|
155
|
+
// console.log('[RFQ] input payload', input);
|
|
85
156
|
|
|
86
157
|
Object.keys(input).forEach(key => input[key] === undefined && delete input[key]);
|
|
87
158
|
|
|
88
159
|
try {
|
|
89
160
|
const result = await handleCreateQuickRfq(input);
|
|
90
|
-
console.log('[RFQ] mutation result', result);
|
|
161
|
+
// console.log('[RFQ] mutation result', result);
|
|
91
162
|
addToast({
|
|
92
163
|
type: 'success',
|
|
93
164
|
message: formatMessage({
|
|
@@ -108,7 +179,7 @@ const RFQModalForm = (props) => {
|
|
|
108
179
|
timeout: 4000
|
|
109
180
|
});
|
|
110
181
|
}
|
|
111
|
-
}, [uploadedFiles, addToast, formatMessage, handleCreateQuickRfq, setOpen, startDate]);
|
|
182
|
+
}, [uploadedFiles, addToast, formatMessage, handleCreateQuickRfq, setOpen, startDate, toBase64]);
|
|
112
183
|
|
|
113
184
|
return (
|
|
114
185
|
<>
|
|
@@ -271,6 +342,7 @@ const RFQModalForm = (props) => {
|
|
|
271
342
|
color="#f76b1c"
|
|
272
343
|
/>
|
|
273
344
|
<div className='mt-2 text-blue-600'>Drop files or click here to upload</div>
|
|
345
|
+
<div className='mt-1 text-gray-500 text-xs'>Max size per file ~1.5MB (transport limit 2MB)</div>
|
|
274
346
|
</div>
|
|
275
347
|
{files && files.length > 0 && (
|
|
276
348
|
<div className="wfp--dropzone__file-list mt-4">
|
|
@@ -288,7 +360,7 @@ const RFQModalForm = (props) => {
|
|
|
288
360
|
}}
|
|
289
361
|
type="button"
|
|
290
362
|
onClick={() => {
|
|
291
|
-
console.log('[RFQ] Cancel clicked, closing modal');
|
|
363
|
+
// console.log('[RFQ] Cancel clicked, closing modal');
|
|
292
364
|
setOpen(false);
|
|
293
365
|
}}
|
|
294
366
|
>
|
|
@@ -307,7 +379,7 @@ const RFQModalForm = (props) => {
|
|
|
307
379
|
}}
|
|
308
380
|
type="button"
|
|
309
381
|
onPress={() => {
|
|
310
|
-
console.log('[RFQ] Submit pressed, calling formApi.submitForm()');
|
|
382
|
+
// console.log('[RFQ] Submit pressed, calling formApi.submitForm()');
|
|
311
383
|
formApi.submitForm();
|
|
312
384
|
}}
|
|
313
385
|
disabled={createState?.loading}
|
|
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
|
|
2
2
|
import { useIntl } from 'react-intl';
|
|
3
3
|
import { StoreTitle } from '@magento/venia-ui/lib/components/Head';
|
|
4
4
|
import Image from '@magento/venia-ui/lib/components/Image';
|
|
5
|
-
import {
|
|
5
|
+
import { useParams, useLocation, useHistory } from 'react-router-dom';
|
|
6
6
|
import Button from '@magento/venia-ui/lib/components/Button';
|
|
7
7
|
import ChatContent from '../LiveChat/chatContent';
|
|
8
8
|
import cn from 'classnames';
|
|
@@ -10,16 +10,52 @@ import { Send } from 'iconsax-react';
|
|
|
10
10
|
import Price from '@magento/venia-ui/lib/components/Price';
|
|
11
11
|
import { useRFQ } from '@riosst100/pwa-marketplace/src/talons/RFQ/useRFQ';
|
|
12
12
|
import LoadingIndicator from '@magento/venia-ui/lib/components/LoadingIndicator';
|
|
13
|
+
import { useToasts } from '@magento/peregrine/lib';
|
|
14
|
+
import { useCartContext } from '@magento/peregrine/lib/context/cart';
|
|
15
|
+
import {
|
|
16
|
+
useApolloClient
|
|
17
|
+
} from '@apollo/client';
|
|
13
18
|
|
|
14
19
|
const quoteDetail = () => {
|
|
20
|
+
const history = useHistory();
|
|
21
|
+
|
|
15
22
|
const { formatMessage } = useIntl();
|
|
16
23
|
const { urlKey } = useParams();
|
|
17
24
|
const location = useLocation();
|
|
18
25
|
const locationRfq = location && location.state && location.state.rfq ? location.state.rfq : null;
|
|
19
|
-
const { loadRfqDetail, rfqDetailState, handleSendRfqMessage, startDetailPolling, stopDetailPolling, handleConvertQuickrfqToCart } = useRFQ();
|
|
26
|
+
const { fetchCartId, addToCartResponseData, isAddProductLoading, errorAddingProductToCart, loadRfqDetail, rfqDetailState, handleSendRfqMessage, startDetailPolling, stopDetailPolling, handleConvertQuickrfqToCart } = useRFQ();
|
|
27
|
+
|
|
28
|
+
const apolloClient = useApolloClient();
|
|
29
|
+
const [{ createCart, removeCart }] = useCartContext();
|
|
30
|
+
|
|
31
|
+
useEffect(async() => {
|
|
32
|
+
if (
|
|
33
|
+
addToCartResponseData
|
|
34
|
+
&& !addToCartResponseData?.addProductsToCart?.user_errors.length
|
|
35
|
+
&& !isAddProductLoading
|
|
36
|
+
&& !errorAddingProductToCart
|
|
37
|
+
) {
|
|
38
|
+
try {
|
|
39
|
+
await removeCart();
|
|
40
|
+
await apolloClient.clearCacheData(apolloClient, 'cart');
|
|
41
|
+
|
|
42
|
+
// masked_cart_id
|
|
43
|
+
await createCart({ fetchCartId });
|
|
44
|
+
|
|
45
|
+
history.push('/checkout');
|
|
46
|
+
} catch (cleanupErr) {
|
|
47
|
+
// eslint-disable-next-line no-console
|
|
48
|
+
console.log(cleanupErr);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
}, [history, fetchCartId, apolloClient, removeCart, createCart, addToCartResponseData, errorAddingProductToCart, isAddProductLoading]);
|
|
54
|
+
|
|
20
55
|
const [messageText, setMessageText] = useState('');
|
|
21
56
|
const [isSending, setIsSending] = useState(false);
|
|
22
57
|
const [sendError, setSendError] = useState(null);
|
|
58
|
+
|
|
23
59
|
const detailData = rfqDetailState && rfqDetailState.data ? rfqDetailState.data.quickrfqDetail : null;
|
|
24
60
|
const detailLoading = rfqDetailState ? rfqDetailState.loading : false;
|
|
25
61
|
const detailError = rfqDetailState ? rfqDetailState.error : null;
|
|
@@ -27,10 +63,9 @@ const quoteDetail = () => {
|
|
|
27
63
|
id: 'Quotes.pageTitleTextQuoteDetail',
|
|
28
64
|
defaultMessage: 'Quote Detail'
|
|
29
65
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
66
|
+
|
|
67
|
+
const placeholderImage = 'https://pwa-tcgcollective.local:8255/media/catalog/product/s/-/s-l1600_6__1.jpg?auto=webp&format=pjpg&width=495&height=618.75&fit=cover';
|
|
68
|
+
|
|
34
69
|
useEffect(() => {
|
|
35
70
|
if (!locationRfq) {
|
|
36
71
|
const fromParam = urlKey ? parseInt(urlKey, 10) : NaN;
|
|
@@ -311,8 +346,8 @@ const quoteDetail = () => {
|
|
|
311
346
|
<div className='relative flex w-full md_w-6/12 justify-center flex-col mb-10 md_mb-0'>
|
|
312
347
|
<Image
|
|
313
348
|
alt='product image'
|
|
314
|
-
className="relative max-w-[
|
|
315
|
-
src={
|
|
349
|
+
className="relative max-w-[200px]"
|
|
350
|
+
src={rfq && rfq.image_url ? rfq.image_url : placeholderImage}
|
|
316
351
|
classes={{
|
|
317
352
|
root: ' relative self-center mb-5'
|
|
318
353
|
}}
|
|
@@ -334,32 +369,15 @@ const quoteDetail = () => {
|
|
|
334
369
|
classes={{
|
|
335
370
|
content: 'capitalize text-[16px] font-medium'
|
|
336
371
|
}}
|
|
337
|
-
disabled={isConverting || !quickrfqIdValue || isNaN(quickrfqIdValue)}
|
|
372
|
+
disabled={isConverting || !quickrfqIdValue || isNaN(quickrfqIdValue) || status === 'Done'}
|
|
338
373
|
aria-busy={isConverting}
|
|
339
|
-
|
|
340
|
-
const qid = quickrfqIdValue;
|
|
341
|
-
if (!qid || isNaN(qid)) return;
|
|
342
|
-
try {
|
|
343
|
-
setConvertError(null);
|
|
344
|
-
setConvertSuccess(null);
|
|
345
|
-
setIsConverting(true);
|
|
346
|
-
const res = await handleConvertQuickrfqToCart(qid);
|
|
347
|
-
const payload = res && res.data && res.data.convertQuickrfqToCart ? res.data.convertQuickrfqToCart : null;
|
|
348
|
-
if (payload && payload.status) {
|
|
349
|
-
setConvertSuccess(payload.message || 'Converted to cart.');
|
|
350
|
-
} else {
|
|
351
|
-
setConvertError('Failed to convert quote to cart.');
|
|
352
|
-
}
|
|
353
|
-
} catch (e) {
|
|
354
|
-
setConvertError('Failed to convert quote to cart.');
|
|
355
|
-
} finally {
|
|
356
|
-
setIsConverting(false);
|
|
357
|
-
}
|
|
358
|
-
}}
|
|
374
|
+
onClick={handleConvertQuickrfqToCart}
|
|
359
375
|
>
|
|
360
376
|
{isConverting
|
|
361
377
|
? formatMessage({ id: 'Quotes.converting', defaultMessage: 'Add To Cart…' })
|
|
362
|
-
:
|
|
378
|
+
: status === 'Done'
|
|
379
|
+
? formatMessage({ id: 'Quotes.completed', defaultMessage: 'Completed' })
|
|
380
|
+
: formatMessage({ id: 'Quotes.addToCart', defaultMessage: 'Add To Cart' })}
|
|
363
381
|
</Button>
|
|
364
382
|
{convertError ? (
|
|
365
383
|
<div className='text-red-600 text-[12px] mt-2 text-center'>{convertError}</div>
|
|
@@ -41,12 +41,13 @@ import ProductLabel from '@riosst100/pwa-marketplace/src/components/ProductLabel
|
|
|
41
41
|
import RFQ from '@riosst100/pwa-marketplace/src/components/RFQ';
|
|
42
42
|
import LinkToOtherStores from '@riosst100/pwa-marketplace/src/components/LinkToOtherStores';
|
|
43
43
|
import Collapsible from '@riosst100/pwa-marketplace/src/components/commons/Collapsible';
|
|
44
|
-
import { useLocation } from 'react-router-dom';
|
|
45
44
|
import Icon from '@magento/venia-ui/lib/components/Icon';
|
|
46
45
|
import { ShoppingCart } from 'react-feather';
|
|
47
46
|
import MessagesModal from '@riosst100/pwa-marketplace/src/components/LiveChat/MessagesModal';
|
|
48
47
|
import SellerOperations from '@riosst100/pwa-marketplace/src/talons/Seller/seller.gql';
|
|
49
48
|
|
|
49
|
+
import { useLocation, useHistory } from 'react-router-dom';
|
|
50
|
+
|
|
50
51
|
import AgeVerificationModal from '@riosst100/pwa-marketplace/src/components/AgeVerification/ageVerificationModal';
|
|
51
52
|
|
|
52
53
|
import { totalListings, lowestPrice } from '@riosst100/pwa-marketplace/src/components/CrossSeller/crossSellerBuy';
|
|
@@ -87,6 +88,8 @@ const ProductDetailsCollapsible = (props) => {
|
|
|
87
88
|
const ProductFullDetail = props => {
|
|
88
89
|
const { product } = props;
|
|
89
90
|
|
|
91
|
+
const history = useHistory();
|
|
92
|
+
|
|
90
93
|
const { search } = useLocation();
|
|
91
94
|
|
|
92
95
|
const params = new URLSearchParams(search);
|
|
@@ -174,7 +177,10 @@ const ProductFullDetail = props => {
|
|
|
174
177
|
message: formatMessage({ id: 'messages.messageSent', defaultMessage: 'Message sent successfully.' }),
|
|
175
178
|
timeout: 3000
|
|
176
179
|
});
|
|
177
|
-
|
|
180
|
+
if (result) {
|
|
181
|
+
var messageId = result?.data?.customerSendMessage.message_id;
|
|
182
|
+
history.push('/messages?id=' + messageId);
|
|
183
|
+
}
|
|
178
184
|
return result?.data?.customerSendMessage;
|
|
179
185
|
};
|
|
180
186
|
|
|
@@ -94,6 +94,7 @@ export const GET_QUICK_RFQ_DETAIL_QUERY = gql`
|
|
|
94
94
|
date_need_quote
|
|
95
95
|
email
|
|
96
96
|
expiry
|
|
97
|
+
image_url
|
|
97
98
|
info_buy_request
|
|
98
99
|
messages {
|
|
99
100
|
created_at
|
|
@@ -166,7 +167,14 @@ export const CONVERT_QUICK_RFQ_TO_CART_MUTATION = gql`
|
|
|
166
167
|
}
|
|
167
168
|
`;
|
|
168
169
|
|
|
170
|
+
export const CREATE_CART = gql`
|
|
171
|
+
mutation createCart {
|
|
172
|
+
cartId: createEmptyCart
|
|
173
|
+
}
|
|
174
|
+
`;
|
|
175
|
+
|
|
169
176
|
export default {
|
|
177
|
+
createCartMutation: CREATE_CART,
|
|
170
178
|
convertQuickrfqToCartMutation: CONVERT_QUICK_RFQ_TO_CART_MUTATION,
|
|
171
179
|
sendRfqMessageMutation: SEND_RFQ_MESSAGE_MUTATION,
|
|
172
180
|
getQuickRfqDetailQuery: GET_QUICK_RFQ_DETAIL_QUERY,
|
package/src/talons/RFQ/useRFQ.js
CHANGED
|
@@ -4,14 +4,23 @@ import { useCallback } from 'react';
|
|
|
4
4
|
import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
|
|
5
5
|
import DEFAULT_OPERATIONS from './rfq.gql';
|
|
6
6
|
|
|
7
|
+
import { useLocation } from 'react-router-dom';
|
|
8
|
+
|
|
7
9
|
export const useRFQ = props => {
|
|
10
|
+
const { pathname } = useLocation();
|
|
11
|
+
const rfqQuoteId = pathname.split('/')[2];
|
|
12
|
+
|
|
13
|
+
console.log('rfqQuoteId',rfqQuoteId)
|
|
14
|
+
|
|
8
15
|
const operations = mergeOperations(DEFAULT_OPERATIONS, props?.operations);
|
|
16
|
+
|
|
9
17
|
const {
|
|
10
18
|
getQuickRfqListQuery,
|
|
11
19
|
getQuickRfqDetailQuery,
|
|
12
20
|
createQuickRfqMutation,
|
|
13
21
|
sendRfqMessageMutation,
|
|
14
|
-
convertQuickrfqToCartMutation
|
|
22
|
+
convertQuickrfqToCartMutation,
|
|
23
|
+
createCartMutation
|
|
15
24
|
} = operations;
|
|
16
25
|
|
|
17
26
|
const [fetchRfqList, rfqListState] = useLazyQuery(getQuickRfqListQuery, {
|
|
@@ -28,7 +37,20 @@ export const useRFQ = props => {
|
|
|
28
37
|
|
|
29
38
|
const [createQuickRfq, createState] = useMutation(createQuickRfqMutation);
|
|
30
39
|
const [sendRfqMessage, sendMessageState] = useMutation(sendRfqMessageMutation);
|
|
31
|
-
|
|
40
|
+
|
|
41
|
+
const [fetchCartId] = useMutation(createCartMutation);
|
|
42
|
+
// const [convertQuickrfqToCart, convertState] = useMutation(convertQuickrfqToCartMutation);
|
|
43
|
+
|
|
44
|
+
const convertState = null;
|
|
45
|
+
|
|
46
|
+
const [
|
|
47
|
+
convertQuickrfqToCart,
|
|
48
|
+
{
|
|
49
|
+
data: addToCartResponseData,
|
|
50
|
+
error: errorAddingProductToCart,
|
|
51
|
+
loading: isAddProductLoading
|
|
52
|
+
}
|
|
53
|
+
] = useMutation(convertQuickrfqToCartMutation);
|
|
32
54
|
|
|
33
55
|
const loadRfqList = useCallback(
|
|
34
56
|
variables => fetchRfqList({ variables }),
|
|
@@ -57,12 +79,15 @@ export const useRFQ = props => {
|
|
|
57
79
|
[sendRfqMessage]
|
|
58
80
|
);
|
|
59
81
|
|
|
60
|
-
const handleConvertQuickrfqToCart = useCallback(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
);
|
|
82
|
+
const handleConvertQuickrfqToCart = useCallback(() => {
|
|
83
|
+
convertQuickrfqToCart({ variables: { quickrfqId: rfqQuoteId } })
|
|
84
|
+
}, [convertQuickrfqToCart, rfqQuoteId]);
|
|
64
85
|
|
|
65
86
|
return {
|
|
87
|
+
fetchCartId,
|
|
88
|
+
addToCartResponseData,
|
|
89
|
+
isAddProductLoading,
|
|
90
|
+
errorAddingProductToCart,
|
|
66
91
|
loadRfqList,
|
|
67
92
|
rfqListState,
|
|
68
93
|
loadRfqDetail,
|
|
@@ -154,7 +154,10 @@ export const useSeller = props => {
|
|
|
154
154
|
}),
|
|
155
155
|
timeout: 3000
|
|
156
156
|
});
|
|
157
|
-
|
|
157
|
+
if (result) {
|
|
158
|
+
var messageId = result?.data?.customerSendMessage.message_id;
|
|
159
|
+
history.push('/messages?id=' + messageId);
|
|
160
|
+
}
|
|
158
161
|
return result?.data?.customerSendMessage;
|
|
159
162
|
}, [sendMessage, refetchMessages, addToast, formatMessage]);
|
|
160
163
|
|