@riosst100/pwa-marketplace 3.2.7 → 3.2.9

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.
@@ -0,0 +1,345 @@
1
+ import { shape, string } from 'prop-types';
2
+ import { FormattedMessage, useIntl } from 'react-intl';
3
+
4
+ // import { useMessagesPage } from '@riosst100/pwa-marketplace/src/talons/MessagesPage/useMessagesPage';
5
+ import { useStyle } from '@magento/venia-ui/lib/classify';
6
+ import { StoreTitle } from '@magento/venia-ui/lib/components/Head';
7
+ import defaultClasses from '@riosst100/pwa-marketplace/src/components/Messages/messagesPage.module.css';
8
+ import cn from 'classnames';
9
+ import React, { useMemo, useState, useEffect } from 'react';
10
+ import ChatContent from '@riosst100/pwa-marketplace/src/components/LiveChat/chatContent';
11
+
12
+ const MessagesPage = props => {
13
+ const classes = useStyle(defaultClasses, props.classes);
14
+ // const { becomeSellerProps } = useMessagesPage(props);
15
+ const { formatMessage } = useIntl();
16
+
17
+ const [selectedThread, setSelectedThread] = useState(null);
18
+ const [replyText, setReplyText] = useState('');
19
+ const [newSubject, setNewSubject] = useState('');
20
+ const [newContent, setNewContent] = useState('');
21
+ const [isComposing, setIsComposing] = useState(false);
22
+ const [hasLoadedOnce, setHasLoadedOnce] = useState(false);
23
+ const [selectLatestAfterSend, setSelectLatestAfterSend] = useState(false);
24
+ const [isSendingNew, setIsSendingNew] = useState(false);
25
+ const [isReplying, setIsReplying] = useState(false);
26
+ const [isDeleting, setIsDeleting] = useState(false);
27
+
28
+ const renderStatusBadge = (statusValue) => {
29
+ const s = Number(statusValue);
30
+ const map = {
31
+ 0: { label: formatMessage({ id: 'messages.status.closed', defaultMessage: 'Closed' }), bg: 'bg-red-100', text: 'text-red-700', dot: 'bg-red-500' },
32
+ 1: { label: formatMessage({ id: 'messages.status.open', defaultMessage: 'Open' }), bg: 'bg-green-100', text: 'text-green-700', dot: 'bg-green-500' },
33
+ 2: { label: formatMessage({ id: 'messages.status.processing', defaultMessage: 'Processing' }), bg: 'bg-yellow-300/20', text: 'text-yellow-300', dot: 'bg-yellow-300' },
34
+ 3: { label: formatMessage({ id: 'messages.status.done', defaultMessage: 'Done' }), bg: 'bg-[#3d84ff1a]', text: 'text-[#3d84ff]', dot: 'bg-[#3d84ff]' }
35
+ };
36
+ const conf = map[s] || { label: 'Unknown', bg: 'bg-gray-200', text: 'text-gray-700', dot: 'bg-gray-500' };
37
+ return (
38
+ <span className={`inline-flex items-center justify-center gap-1 px-2 h-[24px] rounded-full text-[12px] font-medium ${conf.bg} ${conf.text} whitespace-nowrap`}>
39
+ <span className={`inline-block w-[8px] h-[8px] rounded-full ${conf.dot}`}></span>
40
+ {conf.label}
41
+ </span>
42
+ );
43
+ };
44
+
45
+ // Filter pesan yang terkait seller ini (perkiraan berdasarkan id terkait)
46
+ const threads = useMemo(() => {
47
+ const items = messages?.items || [];
48
+ if (!seller?.seller_id) return items;
49
+ return items.filter(t =>
50
+ t?.owner_id === seller.seller_id ||
51
+ t?.receiver_id === seller.seller_id ||
52
+ t?.sender_id === seller.seller_id
53
+ );
54
+ }, [messages, seller]);
55
+
56
+ const sortedThreads = useMemo(() => {
57
+ return [...threads].sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
58
+ }, [threads]);
59
+
60
+ useEffect(() => {
61
+ if (!isComposing && sortedThreads?.length) {
62
+ const latest = sortedThreads[sortedThreads.length - 1];
63
+ if (!selectedThread) {
64
+ setSelectedThread(latest);
65
+ } else {
66
+ const stillThere = sortedThreads.find(t => t.message_id === selectedThread.message_id);
67
+ if (!stillThere) {
68
+ setSelectedThread(latest);
69
+ } else {
70
+ // Replace with the fresh thread data so new details from polling appear
71
+ setSelectedThread(stillThere);
72
+ if (selectLatestAfterSend) {
73
+ setSelectedThread(latest);
74
+ setSelectLatestAfterSend(false);
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }, [sortedThreads, isComposing, selectedThread, selectLatestAfterSend]);
80
+
81
+ // Tandai sudah pernah load minimal sekali untuk mencegah flicker
82
+ useEffect(() => {
83
+ if (threads.length > 0 || (!loading && messages)) {
84
+ setHasLoadedOnce(true);
85
+ }
86
+ }, [threads.length, loading, messages]);
87
+
88
+ const chatData = useMemo(() => {
89
+ const details = selectedThread?.details?.items || [];
90
+ const sorted = [...details].sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
91
+ const opening = selectedThread ? [{
92
+ message: selectedThread.description,
93
+ senderName: selectedThread.sender_name,
94
+ timeStamp: selectedThread.created_at ? new Date(selectedThread.created_at).toLocaleString() : '',
95
+ type: selectedThread.sender_name === seller?.name ? 'seller' : 'customer'
96
+ }] : [];
97
+ const mapped = sorted.map(d => ({
98
+ message: d.content,
99
+ senderName: d.sender_name,
100
+ timeStamp: new Date(d.created_at).toLocaleString(),
101
+ type: d.sender_name === seller?.name ? 'seller' : 'customer'
102
+ }));
103
+ return [...opening, ...mapped];
104
+ }, [selectedThread, seller]);
105
+
106
+ const handleSubmitReply = async (e) => {
107
+ e.preventDefault();
108
+ if (!selectedThread?.message_id || !replyText.trim()) return;
109
+ try {
110
+ setIsReplying(true);
111
+ const res = await onReply({ message_id: selectedThread.message_id, content: replyText.trim() });
112
+ // Optimistic local append so the reply appears instantly
113
+ const newDetail = {
114
+ content: res?.content || replyText.trim(),
115
+ sender_name: res?.sender_name || selectedThread?.sender_name || 'You',
116
+ sender_email: res?.sender_email || '',
117
+ receiver_name: res?.receiver_name || '',
118
+ is_read: true,
119
+ created_at: res?.created_at || new Date().toISOString()
120
+ };
121
+ const nextThread = {
122
+ ...selectedThread,
123
+ details: {
124
+ ...selectedThread.details,
125
+ items: [...(selectedThread.details?.items || []), newDetail],
126
+ total_count: (selectedThread.details?.total_count || 0) + 1
127
+ }
128
+ };
129
+ setSelectedThread(nextThread);
130
+ } finally {
131
+ setIsReplying(false);
132
+ }
133
+ setReplyText('');
134
+ };
135
+
136
+ const handleDeleteThread = async () => {
137
+ if (!selectedThread?.message_id) return;
138
+ try {
139
+ setIsDeleting(true);
140
+ await onDelete({ id: selectedThread.message_id });
141
+ } finally {
142
+ setIsDeleting(false);
143
+ }
144
+ };
145
+
146
+ const handleSubmitNewMessage = async (e) => {
147
+ e.preventDefault();
148
+ if (!newSubject.trim() || !newContent.trim()) return;
149
+ try {
150
+ setIsSendingNew(true);
151
+ await onSend({ subject: newSubject.trim(), content: newContent.trim(), seller_url: seller?.url_key });
152
+ } finally {
153
+ setIsSendingNew(false);
154
+ }
155
+ setNewSubject('');
156
+ setNewContent('');
157
+ setIsComposing(false);
158
+ setSelectLatestAfterSend(true);
159
+ };
160
+
161
+ return (
162
+ <div className={classes.rootContainer}>
163
+ <div className={cn(classes.leftContent, '!py-[60px]')}>
164
+ <StoreTitle>
165
+ {formatMessage({
166
+ id: 'messagesPage.title',
167
+ defaultMessage: 'Messages'
168
+ })}
169
+ </StoreTitle>
170
+ <div className={cn(classes.leftContentContainer, '')}>
171
+ <div class="auth-left">
172
+ <div className='px-3 py-2 border-b font-medium flex items-center justify-between'>
173
+ <span>{formatMessage({ id: 'messages.sidebar.title', defaultMessage: 'Messages' })}</span>
174
+ <button
175
+ type='button'
176
+ onClick={() => {
177
+ setIsComposing(true);
178
+ setSelectedThread(null);
179
+ }}
180
+ className='text-[12px] px-2 py-1 rounded border border-[#FF6E26] text-[#FF6E26] hover:bg-[#FF6E26] hover:text-white'
181
+ >
182
+ {formatMessage({ id: 'messages.compose.newMessage', defaultMessage: 'Send New Message' })}
183
+ </button>
184
+ </div>
185
+ <div className='max-h-[420px] overflow-y-auto'>
186
+ {(!isComposing && !hasLoadedOnce && loading && sortedThreads.length === 0) ? (
187
+ <div className='p-3 text-sm text-gray-500'>{formatMessage({ id: 'messages.loading', defaultMessage: 'Loading…' })}</div>
188
+ ) : (
189
+ sortedThreads.map(t => (
190
+ <button
191
+ key={t.message_id}
192
+ onClick={() => setSelectedThread(t)}
193
+ className={cn('w-full text-left px-3 py-2 border-b hover:bg-gray-50',
194
+ selectedThread?.message_id === t.message_id && 'bg-gray-100')}
195
+ >
196
+ <div className='flex items-center justify-between gap-2'>
197
+ <div className='text-[13px] font-semibold line-clamp-1'>{t.subject || formatMessage({ id: 'messages.noSubject', defaultMessage: 'No Subject' })}</div>
198
+ <div className='text-[12px] flex-shrink-0'>
199
+ {renderStatusBadge(t.status)}
200
+ </div>
201
+ </div>
202
+ <div className='text-[11px] text-gray-500 mt-1'>{formatMessage({ id: 'messages.createdAt', defaultMessage: 'Created:' })} {new Date(t.created_at).toLocaleString()}</div>
203
+ </button>
204
+ ))
205
+ )}
206
+ </div>
207
+ </div>
208
+ </div>
209
+ </div>
210
+ <div className={classes.root}>
211
+ <div className="lg_border-2 lg_border-solid lg_border-subtle lg_rounded-md !border !border-gray-100 !rounded-lg">
212
+ {/* Compose new message (shown only when composing OR when no thread exists) */}
213
+ {(isComposing || sortedThreads.length === 0) && (
214
+ <form onSubmit={handleSubmitNewMessage} className='mb-3 rounded-[6px] p-1 flex flex-col gap-2'>
215
+ <div className='text-[13px] font-semibold flex items-center justify-between'>
216
+ <span>{formatMessage({ id: 'messages.compose.header', defaultMessage: 'Create New Message' })}</span>
217
+ {(sortedThreads.length > 0) && (
218
+ <button
219
+ type='button'
220
+ onClick={() => setIsComposing(false)}
221
+ className='text-[12px] px-2 py-1 rounded border text-gray-600 hover:bg-gray-50'
222
+ >
223
+ {formatMessage({ id: 'messages.compose.cancel', defaultMessage: 'Cancel' })}
224
+ </button>
225
+ )}
226
+ </div>
227
+ <input
228
+ type='text'
229
+ className='border border-gray-100 rounded px-3 py-2 outline-none focus:ring-2 focus:ring-[#FF6E26]'
230
+ placeholder={formatMessage({ id: 'messages.compose.subjectPlaceholder', defaultMessage: 'Subject' })}
231
+ value={newSubject}
232
+ onChange={(e) => setNewSubject(e.target.value)}
233
+ />
234
+ <textarea
235
+ rows={3}
236
+ className='border border-gray-100 rounded px-3 py-2 outline-none focus:ring-2 focus:ring-[#FF6E26]'
237
+ placeholder={formatMessage({ id: 'messages.compose.contentPlaceholder', defaultMessage: 'Write a message to the seller…' })}
238
+ value={newContent}
239
+ onChange={(e) => setNewContent(e.target.value)}
240
+ />
241
+ <div className='flex justify-end'>
242
+ <button
243
+ type='submit'
244
+ className={cn('px-4 py-2 rounded-[6px] text-white disabled:opacity-50 disabled:cursor-not-allowed', isSendingNew ? 'bg-gray-500 text-gray-700' : 'bg-[#FF6E26]')}
245
+ disabled={isSendingNew}
246
+ aria-busy={isSendingNew}
247
+ >
248
+ {isSendingNew
249
+ ? formatMessage({ id: 'messages.compose.sending', defaultMessage: 'Send Message...' })
250
+ : formatMessage({ id: 'messages.compose.send', defaultMessage: 'Send Message' })}
251
+ </button>
252
+ </div>
253
+ </form>
254
+ )}
255
+ {!isComposing && sortedThreads.length > 0 && (
256
+ <div className='flex items-center justify-between mb-2'>
257
+ <div>
258
+ <div className='text-[14px] font-semibold'>{selectedThread?.subject || formatMessage({ id: 'messages.noSubject', defaultMessage: 'No Subject' })}</div>
259
+ <div className='text-[12px] text-gray-600 flex items-center gap-2'>
260
+ <span>{formatMessage({ id: 'messages.detail.status', defaultMessage: 'Status:' })}</span>
261
+ {selectedThread && renderStatusBadge(selectedThread.status)}
262
+ </div>
263
+ </div>
264
+ {selectedThread && (
265
+ <button
266
+ onClick={handleDeleteThread}
267
+ disabled={isDeleting}
268
+ 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'
269
+ >
270
+ {isDeleting && (
271
+ <span className='w-3 h-3 border-2 border-red-200 border-t-red-500 rounded-full animate-spin' aria-hidden='true'></span>
272
+ )}
273
+ {isDeleting
274
+ ? formatMessage({ id: 'messages.detail.deleting', defaultMessage: 'Deleting...' })
275
+ : formatMessage({ id: 'messages.detail.delete', defaultMessage: 'Delete Message' })}
276
+ </button>
277
+ )}
278
+ </div>
279
+ )}
280
+ {!isComposing && sortedThreads.length > 0 && (
281
+ <div className='border rounded-[6px] p-3 flex-1 min-h-[320px]'>
282
+ {selectedThread ? (
283
+ <ChatContent chatData={chatData} />
284
+ ) : (
285
+ <div className='text-sm text-gray-500'>{formatMessage({ id: 'messages.detail.selectPrompt', defaultMessage: 'Select a message to view the conversation.' })}</div>
286
+ )}
287
+ </div>
288
+ )}
289
+ {!isComposing && sortedThreads.length > 0 && selectedThread && (
290
+ <form onSubmit={handleSubmitReply} className='mt-3 flex gap-2'>
291
+ {(() => {
292
+ const statusNum = Number(selectedThread.status);
293
+ const isReplyEnabled = statusNum === 1 || statusNum === 2;
294
+ const disabledReason = statusNum === 0
295
+ ? formatMessage({ id: 'messages.reply.disabled.closed', defaultMessage: 'Conversation closed' })
296
+ : statusNum === 3
297
+ ? formatMessage({ id: 'messages.reply.disabled.done', defaultMessage: 'Conversation finished' })
298
+ : formatMessage({ id: 'messages.reply.disabled.generic', defaultMessage: 'Cannot send a reply' });
299
+ return (
300
+ <>
301
+ <input
302
+ type='text'
303
+ className='flex-1 border rounded px-3 py-2 outline-none focus:ring-2 focus:ring-[#FF6E26] disabled:bg-gray-100 disabled:text-gray-500'
304
+ placeholder={isReplyEnabled ? formatMessage({ id: 'messages.reply.placeholder', defaultMessage: 'Write a reply…' }) : disabledReason}
305
+ value={replyText}
306
+ onChange={(e) => setReplyText(e.target.value)}
307
+ disabled={!isReplyEnabled}
308
+ title={!isReplyEnabled ? disabledReason : undefined}
309
+ aria-disabled={!isReplyEnabled}
310
+ />
311
+ <button
312
+ type='submit'
313
+ disabled={!isReplyEnabled || isReplying}
314
+ title={!isReplyEnabled ? disabledReason : undefined}
315
+ className={cn('px-4 py-2 rounded-[6px] text-white disabled:opacity-50 disabled:cursor-not-allowed', isReplying ? 'bg-gray-500 text-gray-700' : 'bg-[#FF6E26]')}
316
+ aria-busy={isReplying}
317
+ >
318
+ {isReplying
319
+ ? formatMessage({ id: 'messages.reply.sending', defaultMessage: 'Sending...' })
320
+ : formatMessage({ id: 'messages.reply.send', defaultMessage: 'Send' })}
321
+ </button>
322
+ </>
323
+ );
324
+ })()}
325
+ </form>
326
+ )}
327
+ </div>
328
+ </div>
329
+ </div>
330
+ )
331
+ }
332
+
333
+ export default MessagesPage;
334
+
335
+ MessagesPage.defaultProps = {
336
+ signedInRedirectUrl: '/messages',
337
+ signInPageUrl: '/sign-in'
338
+ };
339
+
340
+ MessagesPage.propTypes = {
341
+ classes: shape({
342
+ root: string
343
+ }),
344
+ signedInRedirectUrl: string
345
+ };
@@ -0,0 +1 @@
1
+ export {default} from './messagesPage';
@@ -0,0 +1,50 @@
1
+ import { shape, string } from 'prop-types';
2
+ import { FormattedMessage, useIntl } from 'react-intl';
3
+
4
+ import { useMessagesPage } from '@riosst100/pwa-marketplace/src/talons/MessagesPage/useMessagesPage';
5
+ import { useStyle } from '@magento/venia-ui/lib/classify';
6
+ import { StoreTitle } from '@magento/venia-ui/lib/components/Head';
7
+ import defaultClasses from './messagesPage.module.css';
8
+ import cn from 'classnames';
9
+ import React, { useMemo, useState, useEffect } from 'react';
10
+ import Messages from '@riosst100/pwa-marketplace/src/components/Messages';
11
+
12
+ const MessagesPage = props => {
13
+ const classes = useStyle(defaultClasses, props.classes);
14
+ const { messagesData, messagesLoading, handleReplyMessage, handleDeleteMessage, handleSendMessage } = useMessagesPage(props);
15
+ const { formatMessage } = useIntl();
16
+
17
+ return (
18
+ <>
19
+ <StoreTitle>
20
+ {formatMessage({
21
+ id: 'messagesPage.title',
22
+ defaultMessage: 'Messages'
23
+ })}
24
+ </StoreTitle>
25
+ <div className={classes.rootContainer}>
26
+ <Messages
27
+ messages={messagesData?.customer?.sellerMessages}
28
+ onReply={handleReplyMessage}
29
+ onDelete={handleDeleteMessage}
30
+ onSend={handleSendMessage}
31
+ loading={messagesLoading}
32
+ />
33
+ </div>
34
+ </>
35
+ )
36
+ }
37
+
38
+ export default MessagesPage;
39
+
40
+ MessagesPage.defaultProps = {
41
+ signedInRedirectUrl: '/messages',
42
+ signInPageUrl: '/sign-in'
43
+ };
44
+
45
+ MessagesPage.propTypes = {
46
+ classes: shape({
47
+ root: string
48
+ }),
49
+ signedInRedirectUrl: string
50
+ };
@@ -0,0 +1,35 @@
1
+ .root {
2
+ composes: gap-y-md from global;
3
+ composes: grid from global;
4
+ /* composes: justify-center from global; */
5
+ composes: px-0 from global;
6
+ composes: py-md from global;
7
+ composes: text-center from global;
8
+ grid-template-columns: minmax(auto, 95%);
9
+ justify-content: right;
10
+ }
11
+
12
+ .leftContent {
13
+ composes: gap-y-md from global;
14
+ composes: grid from global;
15
+ composes: justify-center from global;
16
+ composes: px-0 from global;
17
+ composes: py-md from global;
18
+ composes: text-center from global;
19
+ grid-template-columns: minmax(auto, 50%);
20
+ }
21
+
22
+ .header {
23
+ composes: font-serif from global;
24
+ }
25
+
26
+ .leftContentContainer {
27
+ text-align: left;
28
+ width: 30%;
29
+ }
30
+
31
+ .rootContainer {
32
+ justify-content: center;
33
+ display: flex;
34
+ align-items: normal;
35
+ }
@@ -13,7 +13,7 @@ import LoadingIndicator from '@magento/venia-ui/lib/components/LoadingIndicator'
13
13
 
14
14
  const quoteDetail = () => {
15
15
  const { formatMessage } = useIntl();
16
- const { id } = useParams();
16
+ const { urlKey } = useParams();
17
17
  const location = useLocation();
18
18
  const locationRfq = location && location.state && location.state.rfq ? location.state.rfq : null;
19
19
  const { loadRfqDetail, rfqDetailState, handleSendRfqMessage, startDetailPolling, stopDetailPolling, handleConvertQuickrfqToCart } = useRFQ();
@@ -33,13 +33,16 @@ const quoteDetail = () => {
33
33
 
34
34
  useEffect(() => {
35
35
  if (!locationRfq) {
36
- const parsedId = id ? parseInt(id, 10) : NaN;
37
- if (!isNaN(parsedId)) {
38
- const variables = { quickrfqId: parsedId };
39
- loadRfqDetail(variables);
36
+ const fromParam = urlKey ? parseInt(urlKey, 10) : NaN;
37
+ const fromData = detailData && detailData.quickrfq_id
38
+ ? (typeof detailData.quickrfq_id === 'number' ? detailData.quickrfq_id : parseInt(detailData.quickrfq_id, 10))
39
+ : NaN;
40
+ const finalId = !isNaN(fromParam) ? fromParam : fromData;
41
+ if (!isNaN(finalId)) {
42
+ loadRfqDetail({ quickrfqId: finalId });
40
43
  }
41
44
  }
42
- }, [id, locationRfq, loadRfqDetail]);
45
+ }, [urlKey, detailData, locationRfq, loadRfqDetail]);
43
46
 
44
47
  // start polling messages every 5s while on detail page; stop on unmount
45
48
  useEffect(() => {
@@ -50,7 +53,9 @@ const quoteDetail = () => {
50
53
  }, [startDetailPolling, stopDetailPolling]);
51
54
 
52
55
  const rfq = locationRfq || detailData || {};
53
- const quickrfqIdValue = rfq && rfq.quickrfq_id ? (typeof rfq.quickrfq_id === 'number' ? rfq.quickrfq_id : parseInt(rfq.quickrfq_id, 10)) : (id ? parseInt(id, 10) : null);
56
+ const quickrfqIdValue = rfq && rfq.quickrfq_id
57
+ ? (typeof rfq.quickrfq_id === 'number' ? rfq.quickrfq_id : parseInt(rfq.quickrfq_id, 10))
58
+ : (urlKey ? parseInt(urlKey, 10) : null);
54
59
  const [isConverting, setIsConverting] = useState(false);
55
60
  const [convertError, setConvertError] = useState(null);
56
61
  const [convertSuccess, setConvertSuccess] = useState(null);
@@ -62,10 +67,20 @@ const quoteDetail = () => {
62
67
  const unitValue = pricePerProduct && typeof pricePerProduct.value === 'number' ? pricePerProduct.value : null;
63
68
  const qtyNumber = typeof quantity === 'number' ? quantity : (typeof quantity === 'string' ? parseFloat(quantity) : 0);
64
69
  const expectedTotal = unitValue !== null && !isNaN(qtyNumber) ? unitValue * qtyNumber : null;
70
+ // admin suggested fields
71
+ const adminQtyRaw = rfq && rfq.admin_quantity != null ? rfq.admin_quantity : null;
72
+ const adminQtyNumber = typeof adminQtyRaw === 'number' ? adminQtyRaw : (typeof adminQtyRaw === 'string' ? parseFloat(adminQtyRaw) : null);
73
+ const adminPrice = rfq && rfq.admin_price ? rfq.admin_price : null;
74
+ const adminCurrency = adminPrice && adminPrice.currency ? adminPrice.currency : currency;
75
+ const adminUnitValue = adminPrice && typeof adminPrice.value === 'number' ? adminPrice.value : null;
76
+ const adminTotal = adminUnitValue !== null && adminQtyNumber !== null && !isNaN(adminQtyNumber) ? adminUnitValue * adminQtyNumber : null;
65
77
  const contactName = rfq && rfq.contact_name ? rfq.contact_name : '-';
66
78
  const email = rfq && rfq.email ? rfq.email : '-';
67
79
  const phone = rfq && rfq.phone ? rfq.phone : '-';
68
80
  const dateNeedQuote = rfq && rfq.date_need_quote ? rfq.date_need_quote : '-';
81
+ const couponCode = rfq && rfq.coupon_code ? rfq.coupon_code : '-';
82
+ const sellerName = rfq && rfq.seller_name ? rfq.seller_name : '-';
83
+ const expiryDate = rfq && rfq.expiry ? rfq.expiry : '-';
69
84
 
70
85
  const isInitialLoading = detailLoading && !detailData;
71
86
  if (isInitialLoading && !locationRfq) {
@@ -95,7 +110,7 @@ const quoteDetail = () => {
95
110
  <div className='relative grid gap-y-md'>
96
111
  <StoreTitle>{PAGE_TITLE}</StoreTitle>
97
112
  <div aria-live="polite" className="text-xl font-medium text-left">
98
- {PAGE_TITLE} - #{id}
113
+ {PAGE_TITLE} - #{quickrfqIdValue}
99
114
  </div>
100
115
  <div className='rounded-md border border-gray-100 px-4 py-6 flex flex-col-reverse md_flex-row justify-between gap-x-10'>
101
116
  <div className='flex flex-col gap-y-4 w-full md_w-6/12'>
@@ -140,7 +155,7 @@ const quoteDetail = () => {
140
155
  }
141
156
  </span>
142
157
  <span className='font-normal block'>
143
- -
158
+ {adminQtyNumber !== null && !isNaN(adminQtyNumber) ? adminQtyNumber : '-'} Units
144
159
  </span>
145
160
  </p>
146
161
  </div>
@@ -176,7 +191,13 @@ const quoteDetail = () => {
176
191
  }
177
192
  </span>
178
193
  <span className='font-normal block'>
179
- -
194
+ {adminUnitValue !== null ? (
195
+ <>
196
+ <Price currencyCode={adminCurrency} value={adminUnitValue} /> {' '} Per Unit
197
+ </>
198
+ ) : (
199
+ '-'
200
+ )}
180
201
  </span>
181
202
  </p>
182
203
  </div>
@@ -210,7 +231,11 @@ const quoteDetail = () => {
210
231
  }
211
232
  </span>
212
233
  <span className='font-normal block'>
213
- -
234
+ {adminTotal !== null ? (
235
+ <Price currencyCode={adminCurrency} value={adminTotal} />
236
+ ) : (
237
+ '-'
238
+ )} {' '} Units
214
239
  </span>
215
240
  </p>
216
241
  </div>
@@ -225,7 +250,7 @@ const quoteDetail = () => {
225
250
  }
226
251
  </span>
227
252
  <span className='font-normal block'>
228
- -
253
+ {couponCode}
229
254
  </span>
230
255
  </p>
231
256
  <small className='text-[10px] text-gray-600'>
@@ -248,7 +273,7 @@ const quoteDetail = () => {
248
273
  }
249
274
  </span>
250
275
  <span className='font-normal block'>
251
- -
276
+ {sellerName}
252
277
  </span>
253
278
  </p>
254
279
  </div>
@@ -278,7 +303,7 @@ const quoteDetail = () => {
278
303
  }
279
304
  </span>
280
305
  <span className='font-normal block'>
281
- -
306
+ {expiryDate}
282
307
  </span>
283
308
  </p>
284
309
  </div>
package/src/intercept.js CHANGED
@@ -234,6 +234,13 @@ module.exports = targets => {
234
234
  pattern: "/order-history/view/:urlKey",
235
235
  path: require.resolve("./components/OrderDetail/orderDetailPage.js"),
236
236
  authed: true,
237
+ },
238
+ {
239
+ exact: true,
240
+ name: "Messages",
241
+ pattern: "/messages",
242
+ path: require.resolve("./components/MessagesPage/index.js"),
243
+ authed: true,
237
244
  }
238
245
  ];
239
246
 
@@ -1,11 +1,11 @@
1
- import React, { Fragment } from 'react';
1
+ import React, { useState, Fragment } from 'react';
2
2
  import { Facebook, Instagram, Twitter, Linkedin } from 'react-feather';
3
3
  import { FormattedMessage, useIntl } from 'react-intl';
4
4
  import { Link } from 'react-router-dom';
5
5
  import { shape, string } from 'prop-types';
6
6
  import { useFooter } from '@magento/peregrine/lib/talons/Footer/useFooter';
7
7
  import { useLocation } from 'react-router-dom';
8
-
8
+ import LiveChat from '@riosst100/pwa-marketplace/src/components/LiveChat';
9
9
  import Logo from '@magento/venia-ui/lib/components/Logo';
10
10
  import Newsletter from '@magento/venia-ui/lib/components/Newsletter';
11
11
  import { useStyle } from '@magento/venia-ui/lib/classify';
@@ -14,12 +14,17 @@ import { DEFAULT_LINKS, LOREM_IPSUM } from './sampleData';
14
14
  import resourceUrl from '@magento/peregrine/lib/util/makeUrl';
15
15
  import cn from 'classnames';
16
16
  import Divider from '@riosst100/pwa-marketplace/src/components/Divider';
17
-
17
+ import Button from '@magento/venia-ui/lib/components/Button';
18
18
  const Footer = props => {
19
19
  const { links } = props;
20
20
  const classes = useStyle(defaultClasses, props.classes);
21
21
  const talonProps = useFooter();
22
22
 
23
+ const [openChat, setOpenChat] = useState(false);
24
+ const toggleChat = () => {
25
+ setOpenChat(!openChat);
26
+ }
27
+
23
28
  const location = useLocation();
24
29
  const isCheckoutPage = location.pathname === '/checkout';
25
30
 
@@ -86,6 +91,10 @@ const Footer = props => {
86
91
  }
87
92
  }
88
93
  >
94
+ <LiveChat
95
+ open={openChat}
96
+ setOpen={setOpenChat}
97
+ />
89
98
  {!isCheckoutPage && <div className={classes.links} style={{"display":"flex"}}>
90
99
  {linkGroups}
91
100
  {/* <div>
File without changes