ordering-ui-react-native 0.17.23 → 0.17.25

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ordering-ui-react-native",
3
- "version": "0.17.23",
3
+ "version": "0.17.25",
4
4
  "description": "Reusable components made in react native",
5
5
  "main": "src/index.tsx",
6
6
  "author": "ordering.inc",
@@ -1,6 +1,6 @@
1
1
 
2
2
  import React, { useState, useEffect } from 'react'
3
- import { useOrder, useSession, useLanguage } from 'ordering-components/native';
3
+ import { useOrder, useSession, useLanguage, useConfig } from 'ordering-components/native';
4
4
 
5
5
  import { useTheme } from 'styled-components/native'
6
6
  import { BusinessesListing as OriginalBusinessListing } from './Layout/Original'
@@ -16,6 +16,8 @@ export const BusinessesListing = (props: any) => {
16
16
  const [, t] = useLanguage();
17
17
  const [{ auth }] = useSession()
18
18
  const [, { getLastOrderHasNoReview }] = useOrder();
19
+ const [{ configs }] = useConfig()
20
+ const isShowReviewsPopupEnabled = configs?.show_reviews_popups_enabled?.value === '1'
19
21
 
20
22
  const [, setIsReviewed] = useState()
21
23
  const [checkNotificationStatus, setCheckNotificationStatus] = useState({ open: false, checked: false })
@@ -74,8 +76,9 @@ export const BusinessesListing = (props: any) => {
74
76
  }
75
77
 
76
78
  useEffect(() => {
79
+ if (!isShowReviewsPopupEnabled) return
77
80
  (checkNotificationStatus?.checked && auth) && _getLastOrderHasNoReview()
78
- }, [checkNotificationStatus, auth])
81
+ }, [checkNotificationStatus, auth, isShowReviewsPopupEnabled])
79
82
 
80
83
  return (
81
84
  <>
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from 'react';
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
2
  import { View, StyleSheet, BackHandler, Platform, Linking, RefreshControl } from 'react-native';
3
3
  import LinearGradient from 'react-native-linear-gradient';
4
4
  import { _setStoreData } from '../../providers/StoreUtil';
@@ -10,6 +10,7 @@ import {
10
10
  useConfig
11
11
  } from 'ordering-components/native';
12
12
  import { useTheme } from 'styled-components/native';
13
+ import { showLocation } from 'react-native-map-link';
13
14
  import {
14
15
  OrderDetailsContainer,
15
16
  Header,
@@ -95,6 +96,12 @@ export const OrderDetailsUI = (props: OrderDetailsParams) => {
95
96
  display: 'flex',
96
97
  alignItems: 'center',
97
98
  flexDirection: 'row'
99
+ },
100
+ professionalBlock: {
101
+ borderBottomColor: theme.colors.border,
102
+ borderBottomWidth: 1,
103
+ marginVertical: 10,
104
+ paddingVertical: 5
98
105
  }
99
106
  });
100
107
 
@@ -110,6 +117,7 @@ export const OrderDetailsUI = (props: OrderDetailsParams) => {
110
117
  const { order, businessData } = props.order;
111
118
  const mapValidStatuses = [9, 19, 23]
112
119
  const placeSpotTypes = [3, 4, 5]
120
+ const directionTypes = [2, 3, 4, 5]
113
121
 
114
122
  const walletName: any = {
115
123
  cash: {
@@ -449,6 +457,59 @@ export const OrderDetailsUI = (props: OrderDetailsParams) => {
449
457
  lng: parseFloat(location?.location?.split(',')[1].replace(/[^-.0-9]/g, ''))
450
458
  } : location)
451
459
 
460
+ const getProductList = () => {
461
+ const professionalList = order?.products.reduce((prev: any, current: any) => {
462
+ const found = prev.find((item: any) => item.id === current?.calendar_event?.professional?.id)
463
+ if (found || !current?.calendar_event) {
464
+ return prev
465
+ }
466
+ return [...prev, current?.calendar_event?.professional]
467
+ }, [])
468
+
469
+ return (
470
+ <>
471
+ {professionalList?.length > 0 && professionalList.map((professional: any, i: number) => (
472
+ <View key={i} style={styles.professionalBlock}>
473
+ <View style={{ flexDirection: 'row', alignItems: 'center', width: '100%' }}>
474
+ {professional?.photo ? (
475
+ <ProfessionalPhoto
476
+ source={{
477
+ uri: professional?.photo
478
+ }}
479
+ imageStyle={{ borderRadius: 8 }}
480
+ />
481
+ ) : (
482
+ <OIcon
483
+ src={theme.images.general.user}
484
+ cover={false}
485
+ width={80}
486
+ height={80}
487
+ />
488
+ )}
489
+ <OText size={12} lineHeight={18} weight={'500'} numberOfLines={1}>{professional?.name} {professional?.lastname}</OText>
490
+ </View>
491
+ {order?.products.filter((product: any) => product?.calendar_event?.professional?.id === professional?.id).map((product: any, i: number) => (
492
+ <ProductItemAccordion
493
+ key={product?.id || i}
494
+ product={product}
495
+ isFromCheckout
496
+ />
497
+ ))}
498
+ </View>
499
+ ))}
500
+ {order?.products.filter((product: any) => !product?.calendar_event).map((product: any, i: number) => (
501
+ <ProductItemAccordion
502
+ key={product?.id || i}
503
+ product={product}
504
+ isFromCheckout
505
+ />
506
+ ))}
507
+ </>
508
+ )
509
+ }
510
+
511
+ const sortedProductList = useMemo(() => getProductList(), [order?.products])
512
+
452
513
  useEffect(() => {
453
514
  if (driverLocation) {
454
515
  parsedLocations[0] = {
@@ -684,6 +745,26 @@ export const OrderDetailsUI = (props: OrderDetailsParams) => {
684
745
  {order?.business?.address}
685
746
  </OText>
686
747
  </View>
748
+ {directionTypes.includes(order?.delivery_type) && (
749
+ <OButton
750
+ text={t('GET_DIRECTIONS', 'Get Directions')}
751
+ imgRightSrc=''
752
+ textStyle={{ color: theme.colors.white }}
753
+ style={{
754
+ alignSelf: 'center',
755
+ borderRadius: 10,
756
+ marginTop: 30
757
+ }}
758
+ onClick={() => showLocation({
759
+ latitude: order?.business?.location?.lat,
760
+ longitude: order?.business?.location?.lng,
761
+ naverCallerName: 'com.reactnativeappstemplate5',
762
+ dialogTitle: t('GET_DIRECTIONS', 'Get Directions'),
763
+ dialogMessage: t('WHAT_APP_WOULD_YOU_USE', 'What app would you like to use?'),
764
+ cancelText: t('CANCEL', 'Cancel'),
765
+ })}
766
+ />
767
+ )}
687
768
  </OrderBusiness>
688
769
 
689
770
  {placeSpotTypes.includes(order?.delivery_type) && (
@@ -877,34 +958,7 @@ export const OrderDetailsUI = (props: OrderDetailsParams) => {
877
958
  </OrderAction>
878
959
  </HeaderInfo>
879
960
  <OrderProducts>
880
- {!!order?.products[0]?.calendar_event?.professional && (
881
- <View style={{ flexDirection: 'row', alignItems: 'center', width: '100%' }}>
882
- {!!order?.products[0]?.calendar_event?.professional?.photo ? (
883
- <ProfessionalPhoto
884
- source={{
885
- uri: order?.products[0]?.calendar_event?.professional?.photo
886
- }}
887
- imageStyle={{ borderRadius: 8 }}
888
- />
889
- ) : (
890
- <OIcon
891
- src={theme.images.general.user}
892
- cover={false}
893
- width={82}
894
- height={82}
895
- />
896
- )}
897
- <OText size={12} lineHeight={18} weight={'500'} numberOfLines={1}>{order?.products[0]?.calendar_event?.professional?.name} {order?.products[0]?.calendar_event?.professional?.lastname}</OText>
898
- </View>
899
- )}
900
- {order?.products?.length &&
901
- order?.products.map((product: any, i: number) => (
902
- <ProductItemAccordion
903
- key={product?.id || i}
904
- product={product}
905
- isFromCheckout
906
- />
907
- ))}
961
+ {sortedProductList}
908
962
  </OrderProducts>
909
963
  <OrderBill>
910
964
  <View style={{ height: 1, backgroundColor: theme.colors.border, marginBottom: 17 }} />
@@ -136,9 +136,9 @@ export const PlaceSpotWrapper = styled.View`
136
136
  export const ProfessionalPhoto = styled.ImageBackground`
137
137
  width: 100%;
138
138
  position: relative;
139
- max-height: 82px;
140
- height: 82px;
141
- width: 82px;
139
+ max-height: 80px;
140
+ height: 80px;
141
+ width: 80px;
142
142
  resize-mode: cover;
143
143
  margin-right: 10px;
144
144
  `;
@@ -4,6 +4,7 @@ import { useUtils, useLanguage, useOrder } from 'ordering-components/native'
4
4
  import { useTheme } from 'styled-components/native';
5
5
  import MaterialCommunityIcon from 'react-native-vector-icons/MaterialCommunityIcons'
6
6
  import RNPickerSelect from 'react-native-picker-select'
7
+ import { ServiceForm } from '../ServiceForm';
7
8
 
8
9
  import {
9
10
  Accordion,
@@ -18,7 +19,7 @@ import {
18
19
  ProductSubOption,
19
20
  ProductComment
20
21
  } from './styles'
21
- import { OIcon, OText, OAlert } from '../shared'
22
+ import { OIcon, OText, OAlert, OModal } from '../shared'
22
23
 
23
24
  import { ProductItemAccordionParams } from '../../types'
24
25
 
@@ -71,6 +72,7 @@ export const ProductItemAccordion = (props: ProductItemAccordionParams) => {
71
72
  const [{ parsePrice, optimizeImage, parseDate }] = useUtils()
72
73
 
73
74
  const [isActive, setActiveState] = useState(false)
75
+ const [isServiceOpen, setIsServiceOpen] = useState(false)
74
76
  // const [setHeight, setHeightState] = useState({ height: new Animated.Value(0) })
75
77
  // const [setRotate, setRotateState] = useState({ angle: new Animated.Value(0) })
76
78
 
@@ -92,6 +94,14 @@ export const ProductItemAccordion = (props: ProductItemAccordionParams) => {
92
94
  return product
93
95
  }
94
96
 
97
+ const handleEditProduct = (curProduct: any) => {
98
+ if (!curProduct?.calendar_event) {
99
+ onEditProduct && onEditProduct(curProduct)
100
+ return
101
+ }
102
+ setIsServiceOpen(true)
103
+ }
104
+
95
105
  /* const toggleAccordion = () => {
96
106
  if ((!product?.valid_menu && isCartProduct)) return
97
107
  if (isActive) {
@@ -133,148 +143,167 @@ export const ProductItemAccordion = (props: ProductItemAccordionParams) => {
133
143
  })
134
144
 
135
145
  return (
136
- <AccordionSection>
137
- <Accordion
138
- isValid={product?.valid ?? true}
139
- onPress={() => (!product?.valid_menu && isCartProduct)
140
- ? {}
141
- : setActiveState(!isActive)}
142
- activeOpacity={1}
143
- >
144
- <View style={{ flexDirection: 'row', alignItems: 'flex-start' }}>
145
- <ContentInfo>
146
- {(product?.images || theme?.images?.dummies?.product) && (
147
- <ProductImage>
148
- {isFromCheckout ? (
149
- <OIcon url={optimizeImage(product?.images || theme?.images?.dummies?.product, 'h_100,c_limit')} style={{ ...styles.productImage, ...{ width: 82, height: 82 } }} />
150
- ) : (
151
- <OIcon url={optimizeImage(product?.images || theme?.images?.dummies?.product, 'h_100,c_limit')} style={styles.productImage} />
152
- )}
153
- </ProductImage>
154
- )}
155
- {!!product?.calendar_event ? (
156
- <View style={{ flex: 1, marginLeft: 10, flexDirection: 'column' }}>
157
- <View>
158
- <OText size={12} lineHeight={18} weight={'400'} numberOfLines={1}>{product?.name}</OText>
146
+ <>
147
+ <AccordionSection>
148
+ <Accordion
149
+ isValid={product?.valid ?? true}
150
+ onPress={() => (!product?.valid_menu && isCartProduct)
151
+ ? {}
152
+ : setActiveState(!isActive)}
153
+ activeOpacity={1}
154
+ >
155
+ <View style={{ flexDirection: 'row', alignItems: 'flex-start' }}>
156
+ <ContentInfo>
157
+ {(product?.images || theme?.images?.dummies?.product) && (
158
+ <ProductImage>
159
+ {isFromCheckout ? (
160
+ <OIcon url={optimizeImage(product?.images || theme?.images?.dummies?.product, 'h_100,c_limit')} style={{ ...styles.productImage, ...{ width: 82, height: 82 } }} />
161
+ ) : (
162
+ <OIcon url={optimizeImage(product?.images || theme?.images?.dummies?.product, 'h_100,c_limit')} style={styles.productImage} />
163
+ )}
164
+ </ProductImage>
165
+ )}
166
+ {!!product?.calendar_event ? (
167
+ <View style={{ flex: 1, marginLeft: 10, flexDirection: 'column' }}>
168
+ <View>
169
+ <OText size={12} lineHeight={18} weight={'400'} numberOfLines={1}>{product?.name}</OText>
170
+ </View>
171
+ <OText size={10} color={theme.colors.textSecondary} style={{ marginTop: 3 }}>
172
+ {parseDate(product?.calendar_event?.start, { outputFormat: 'hh:mm a' })} - {parseDate(product?.calendar_event?.end, { outputFormat: 'hh:mm a' })}
173
+ </OText>
159
174
  </View>
160
- <OText size={10} color={theme.colors.textSecondary} style={{ marginTop: 3 }}>
161
- {parseDate(product?.calendar_event?.start, { outputFormat: 'hh:mm a' })} - {parseDate(product?.calendar_event?.end, { outputFormat: 'hh:mm a' })}
162
- </OText>
163
- </View>
164
- ): (
165
- <>
166
- {isCartProduct && !isCartPending && getProductMax && (
167
- <ProductInfo>
168
- <RNPickerSelect
169
- items={productOptions}
170
- onValueChange={handleChangeQuantity}
171
- value={product.quantity.toString()}
172
- style={pickerStyle}
173
- useNativeAndroidPickerStyle={false}
174
- placeholder={{}}
175
- Icon={() => <View style={pickerStyle.icon}><OIcon src={theme.images.general.arrow_down} color={theme.colors.textNormal} width={8} /></View>}
176
- disabled={orderState.loading}
177
- />
178
- </ProductInfo>
179
- )}
180
- {isFromCheckout && (
181
- <ProductQuantity>
182
- <OText size={12} lineHeight={18}>
183
- {product?.quantity}
184
- </OText>
185
- </ProductQuantity>
186
- )}
187
- <View style={{ flex: 1 }}>
188
- <OText size={12} lineHeight={18} weight={'400'}>{product.name}</OText>
175
+ ): (
176
+ <>
177
+ {isCartProduct && !isCartPending && getProductMax && (
178
+ <ProductInfo>
179
+ <RNPickerSelect
180
+ items={productOptions}
181
+ onValueChange={handleChangeQuantity}
182
+ value={product.quantity.toString()}
183
+ style={pickerStyle}
184
+ useNativeAndroidPickerStyle={false}
185
+ placeholder={{}}
186
+ Icon={() => <View style={pickerStyle.icon}><OIcon src={theme.images.general.arrow_down} color={theme.colors.textNormal} width={8} /></View>}
187
+ disabled={orderState.loading}
188
+ />
189
+ </ProductInfo>
190
+ )}
191
+ {isFromCheckout && (
192
+ <ProductQuantity>
193
+ <OText size={12} lineHeight={18}>
194
+ {product?.quantity}
195
+ </OText>
196
+ </ProductQuantity>
197
+ )}
198
+ <View style={{ flex: 1 }}>
199
+ <OText size={12} lineHeight={18} weight={'400'}>{product.name}</OText>
200
+ </View>
201
+ </>
202
+ )}
203
+ <View style={{ display: 'flex', flexDirection: 'column', flex: 1, alignItems: 'flex-end', maxWidth: 100 }}>
204
+ <View style={{ flexDirection: 'row' }}>
205
+ <OText size={12} lineHeight={18} weight={'400'}>{parsePrice(product.total || product.price)}</OText>
206
+ {(productInfo().ingredients.length > 0 || productInfo().options.length > 0 || product.comment) && (
207
+ <MaterialCommunityIcon name='chevron-down' size={18} />
208
+ )}
209
+ </View>
210
+ <View style={{ display: 'flex', flexDirection: 'row', justifyContent: 'flex-start' }}>
211
+ {onEditProduct && isCartProduct && !isCartPending && product?.valid_menu && (
212
+ <TouchableOpacity onPress={() => handleEditProduct(product)} style={{ marginEnd: 7 }}>
213
+ <OIcon
214
+ src={theme.images.general.pencil}
215
+ width={16}
216
+ color={theme.colors.textSecondary}
217
+ />
218
+ </TouchableOpacity>
219
+ )}
220
+ {onDeleteProduct && isCartProduct && !isCartPending && (
221
+ <OAlert
222
+ title={t('DELETE_PRODUCT', 'Delete Product')}
223
+ message={t('QUESTION_DELETE_PRODUCT', 'Are you sure that you want to delete the product?')}
224
+ onAccept={() => onDeleteProduct(product)}
225
+ >
226
+ <OIcon
227
+ src={theme.images.general.trash}
228
+ width={17}
229
+ color={theme.colors.textSecondary}
230
+ />
231
+ </OAlert>
232
+ )}
189
233
  </View>
190
- </>
191
- )}
192
- <View style={{ display: 'flex', flexDirection: 'column', flex: 1, alignItems: 'flex-end', maxWidth: 100 }}>
193
- <View style={{ flexDirection: 'row' }}>
194
- <OText size={12} lineHeight={18} weight={'400'}>{parsePrice(product.total || product.price)}</OText>
195
- {(productInfo().ingredients.length > 0 || productInfo().options.length > 0 || product.comment) && (
196
- <MaterialCommunityIcon name='chevron-down' size={18} />
197
- )}
198
234
  </View>
199
- <View style={{ display: 'flex', flexDirection: 'row', justifyContent: 'flex-start' }}>
200
- {onEditProduct && isCartProduct && !isCartPending && product?.valid_menu && (
201
- <TouchableOpacity onPress={() => onEditProduct(product)} style={{ marginEnd: 7 }}>
202
- <OIcon
203
- src={theme.images.general.pencil}
204
- width={16}
205
- color={theme.colors.textSecondary}
206
- />
207
- </TouchableOpacity>
208
- )}
209
- {onDeleteProduct && isCartProduct && !isCartPending && (
210
- <OAlert
211
- title={t('DELETE_PRODUCT', 'Delete Product')}
212
- message={t('QUESTION_DELETE_PRODUCT', 'Are you sure that you want to delete the product?')}
213
- onAccept={() => onDeleteProduct(product)}
214
- >
215
- <OIcon
216
- src={theme.images.general.trash}
217
- width={17}
218
- color={theme.colors.textSecondary}
219
- />
220
- </OAlert>
221
- )}
235
+ </ContentInfo>
236
+ </View>
237
+ {((isCartProduct && !isCartPending && product?.valid_menu && !product?.valid_quantity) ||
238
+ (!product?.valid_menu && isCartProduct && !isCartPending)) && (
239
+ <View style={{ alignItems: 'flex-end', width: '100%', }}>
240
+ <OText size={14} color={theme.colors.red} style={{ textAlign: 'right', marginTop: 5 }}>
241
+ {t('NOT_AVAILABLE', 'Not available')}
242
+ </OText>
222
243
  </View>
223
- </View>
224
- </ContentInfo>
225
- </View>
226
- {((isCartProduct && !isCartPending && product?.valid_menu && !product?.valid_quantity) ||
227
- (!product?.valid_menu && isCartProduct && !isCartPending)) && (
228
- <View style={{ alignItems: 'flex-end', width: '100%', }}>
229
- <OText size={14} color={theme.colors.red} style={{ textAlign: 'right', marginTop: 5 }}>
230
- {t('NOT_AVAILABLE', 'Not available')}
231
- </OText>
232
- </View>
233
- )}
234
- </Accordion>
235
-
236
- <View style={{ display: isActive ? 'flex' : 'none', paddingStart: 40 }}>
237
- <Animated.View>
238
- <AccordionContent>
239
- {productInfo().ingredients.length > 0 && productInfo().ingredients.some((ingredient: any) => !ingredient.selected) && (
240
- <ProductOptionsList>
241
- <OText size={10} color={theme.colors.textSecondary}>{t('INGREDIENTS', 'Ingredients')}</OText>
242
- {productInfo().ingredients.map((ingredient: any) => !ingredient.selected && (
243
- <OText size={10} color={theme.colors.textThird} key={ingredient.id} style={{ marginLeft: 10 }}>{t('NO', 'No')} {ingredient.name}</OText>
244
- ))}
245
- </ProductOptionsList>
246
- )}
247
- {productInfo().options.length > 0 && (
248
- <ProductOptionsList>
249
- {productInfo().options.map((option: any, i: number) => (
250
- <ProductOption key={option.id + i}>
251
- <OText size={10} color={theme.colors.textSecondary}>{option.name}</OText>
252
- {option.suboptions.map((suboption: any) => (
253
- <ProductSubOption key={suboption.id}>
254
- <OText size={10} color={theme.colors.textThird}>
255
- {getFormattedSubOptionName({
256
- quantity: suboption.quantity,
257
- name: suboption.name,
258
- position: (suboption.position !== 'whole') ? t(suboption.position.toUpperCase(), suboption.position) : '',
259
- price: parsePrice(suboption.price)
260
- })}
261
- </OText>
262
- </ProductSubOption>
263
- ))}
264
- </ProductOption>
265
- ))}
266
- </ProductOptionsList>
267
244
  )}
268
- {product.comment && (
269
- <ProductComment>
270
- <OText size={10} color={theme.colors.textSecondary}>{t('SPECIAL_COMMENT', 'Special Comment')}</OText>
271
- <OText size={10} color={theme.colors.textThird}>{product.comment}</OText>
272
- </ProductComment>
273
- )}
274
- </AccordionContent>
275
- </Animated.View>
276
- </View>
277
- </AccordionSection>
245
+ </Accordion>
246
+
247
+ <View style={{ display: isActive ? 'flex' : 'none', paddingStart: 40 }}>
248
+ <Animated.View>
249
+ <AccordionContent>
250
+ {productInfo().ingredients.length > 0 && productInfo().ingredients.some((ingredient: any) => !ingredient.selected) && (
251
+ <ProductOptionsList>
252
+ <OText size={10} color={theme.colors.textSecondary}>{t('INGREDIENTS', 'Ingredients')}</OText>
253
+ {productInfo().ingredients.map((ingredient: any) => !ingredient.selected && (
254
+ <OText size={10} color={theme.colors.textThird} key={ingredient.id} style={{ marginLeft: 10 }}>{t('NO', 'No')} {ingredient.name}</OText>
255
+ ))}
256
+ </ProductOptionsList>
257
+ )}
258
+ {productInfo().options.length > 0 && (
259
+ <ProductOptionsList>
260
+ {productInfo().options.map((option: any, i: number) => (
261
+ <ProductOption key={option.id + i}>
262
+ <OText size={10} color={theme.colors.textSecondary}>{option.name}</OText>
263
+ {option.suboptions.map((suboption: any) => (
264
+ <ProductSubOption key={suboption.id}>
265
+ <OText size={10} color={theme.colors.textThird}>
266
+ {getFormattedSubOptionName({
267
+ quantity: suboption.quantity,
268
+ name: suboption.name,
269
+ position: (suboption.position !== 'whole') ? t(suboption.position.toUpperCase(), suboption.position) : '',
270
+ price: parsePrice(suboption.price)
271
+ })}
272
+ </OText>
273
+ </ProductSubOption>
274
+ ))}
275
+ </ProductOption>
276
+ ))}
277
+ </ProductOptionsList>
278
+ )}
279
+ {product.comment && (
280
+ <ProductComment>
281
+ <OText size={10} color={theme.colors.textSecondary}>{t('SPECIAL_COMMENT', 'Special Comment')}</OText>
282
+ <OText size={10} color={theme.colors.textThird}>{product.comment}</OText>
283
+ </ProductComment>
284
+ )}
285
+ </AccordionContent>
286
+ </Animated.View>
287
+ </View>
288
+ </AccordionSection>
289
+ <OModal
290
+ open={isServiceOpen}
291
+ onClose={() => setIsServiceOpen(false)}
292
+ entireModal
293
+ >
294
+ <ServiceForm
295
+ isCartProduct
296
+ isService
297
+ businessId={product?.business_id}
298
+ categoryId={product?.category_id}
299
+ productId={product?.id}
300
+ productCart={product}
301
+ onSave={() => setIsServiceOpen(false)}
302
+ onClose={() => setIsServiceOpen(false)}
303
+ professionalSelected={product?.calendar_event?.professional}
304
+ />
305
+ </OModal>
306
+ </>
278
307
  )
279
308
  }
280
309
 
@@ -10,6 +10,8 @@ import CalendarPicker from 'react-native-calendar-picker'
10
10
  import FeatherIcon from 'react-native-vector-icons/Feather';
11
11
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
12
12
  import { ServiceFormParams } from '../../types'
13
+ import { Placeholder, PlaceholderLine, Fade } from 'rn-placeholder';
14
+
13
15
  import {
14
16
  ProductForm as ProductFormController,
15
17
  useUtils,
@@ -35,14 +37,15 @@ const screenWidth = Dimensions.get('window').width
35
37
  const ServiceFormUI = (props: ServiceFormParams) => {
36
38
  const {
37
39
  professionalSelected,
38
- product,
40
+ productObject,
39
41
  handleSave,
40
42
  productCart,
41
43
  navigation,
42
44
  isSoldOut,
43
45
  maxProductQuantity,
44
46
  onClose,
45
- professionalList
47
+ professionalListState,
48
+ isCartProduct
46
49
  } = props
47
50
 
48
51
  const theme = useTheme()
@@ -52,6 +55,7 @@ const ServiceFormUI = (props: ServiceFormParams) => {
52
55
  const [{ configs }] = useConfig()
53
56
  const [orderState] = useOrder()
54
57
  const [{ auth }] = useSession()
58
+ const { product, loading, error } = productObject;
55
59
 
56
60
  const [selectDate, setSelectedDate] = useState<any>(new Date())
57
61
  const [timeList, setTimeList] = useState<any>([])
@@ -153,7 +157,7 @@ const ServiceFormUI = (props: ServiceFormParams) => {
153
157
  }
154
158
 
155
159
  const handleRedirectLogin = () => {
156
- navigation.navigate('Login', {
160
+ navigation && navigation.navigate('Login', {
157
161
  store_slug: props.businessSlug
158
162
  });
159
163
  onClose && onClose()
@@ -209,7 +213,7 @@ const ServiceFormUI = (props: ServiceFormParams) => {
209
213
  }
210
214
 
211
215
  const addressRedirect = () => {
212
- navigation.navigate('AddressList')
216
+ navigation && navigation.navigate('AddressList')
213
217
  onClose && onClose()
214
218
  }
215
219
 
@@ -234,304 +238,323 @@ const ServiceFormUI = (props: ServiceFormParams) => {
234
238
  }, [selectDate, timeSelected])
235
239
 
236
240
  useEffect(() => {
237
- if (!professionalSelected) return
241
+ if (!professionalSelected?.schedule) return
238
242
  setCurrentProfessional(professionalSelected)
239
243
  }, [professionalSelected])
244
+
245
+ useEffect(() => {
246
+ if (isCartProduct && professionalListState?.professionals?.length > 0) {
247
+ const professional = professionalListState?.professionals?.find((item: any) => item.id === professionalSelected?.id)
248
+ setCurrentProfessional(professional)
249
+ }
250
+ }, [isCartProduct, professionalListState?.professionals])
240
251
 
241
252
  return (
242
253
  <>
243
- <Container>
244
- {!!product?.images ? (
245
- <ProfessionalPhoto
246
- source={{
247
- uri: product?.images
248
- }}
249
- />
250
- ) : (
251
- <OIcon
252
- src={theme?.images?.dummies?.product}
253
- cover={false}
254
- style={{ alignSelf: 'center' }}
255
- width={200}
256
- height={200}
254
+ {loading && !error && (
255
+ <Placeholder Animation={Fade}>
256
+ <PlaceholderLine
257
+ height={258}
258
+ style={{ borderRadius: 0 }}
259
+ width={screenWidth}
257
260
  />
258
- )}
259
- <InfoWrapper>
260
- <OText
261
- size={20}
262
- style={{ marginBottom: 4 }}
263
- weight={Platform.OS === 'ios' ? '600' : 'bold'}
264
- >
265
- {product?.name}
266
- </OText>
267
- <OText
268
- size={16}
269
- style={{ marginBottom: 10 }}
270
- weight={'400'}
271
- >
272
- {parsePrice(product?.price)} {product?.duration}min
273
- </OText>
274
- <OText
275
- size={14}
276
- weight={'400'}
277
- color={theme?.colors?.disabled}
278
- >
279
- {product?.description}
280
- </OText>
281
- </InfoWrapper>
282
- <Divider />
283
- <ProfessionalWrapper>
284
- <View
285
- style={{
286
- flexDirection: 'row',
287
- justifyContent: 'space-between',
288
- alignItems: 'center',
289
- marginBottom: 23
290
- }}
291
- >
261
+ </Placeholder>
262
+ )}
263
+ {!loading && !error && (
264
+ <Container>
265
+ {!!product?.images ? (
266
+ <ProfessionalPhoto
267
+ source={{
268
+ uri: product?.images
269
+ }}
270
+ />
271
+ ) : (
272
+ <OIcon
273
+ src={theme?.images?.dummies?.product}
274
+ cover={false}
275
+ style={{ alignSelf: 'center' }}
276
+ width={200}
277
+ height={200}
278
+ />
279
+ )}
280
+ <InfoWrapper>
292
281
  <OText
293
- size={16}
282
+ size={20}
283
+ style={{ marginBottom: 4 }}
294
284
  weight={Platform.OS === 'ios' ? '600' : 'bold'}
295
285
  >
296
- {t('PROFESSIONAL', 'Professional')}
286
+ {product?.name}
297
287
  </OText>
298
- <OText
299
- size={10}
300
- weight={'400'}
301
- color={theme.colors?.danger5}
302
- >
303
- {t('REQUIRED', 'Required')}
304
- </OText>
305
- </View>
306
- <TouchableOpacity
307
- style={styles.professionalSelect}
308
- onPress={() => setIsOpen(true)}
309
- >
310
- {!!currentProfessional ? (
311
- <>
312
- <View style={{ flexDirection: 'row' }}>
313
- {!!currentProfessional?.photo ? (
314
- <FastImage
315
- style={styles.photoStyle}
316
- source={{
317
- uri: optimizeImage(currentProfessional?.photo, 'h_250,c_limit'),
318
- priority: FastImage.priority.normal,
319
- }}
320
- resizeMode={FastImage.resizeMode.cover}
321
- />
322
- ) : (
323
- <OIcon
324
- src={theme?.images?.general?.user}
325
- cover={false}
326
- style={styles.photoStyle}
327
- />
328
- )}
329
- <View style={{ marginLeft: 14 }}>
330
- <OText
331
- size={14}
332
- weight={'400'}
333
- lineHeight={22}
334
- >
335
- {currentProfessional?.name} {currentProfessional?.lastname}
336
- </OText>
337
- <OText
338
- size={12}
339
- weight={'400'}
340
- lineHeight={17}
341
- color={isBusyTime(currentProfessional) ? theme.colors.danger5 : theme.colors.success500}
342
- >
343
- {isBusyTime(currentProfessional)
344
- ? t('BUSY_ON_SELECTED_TIME', 'Busy on selected time')
345
- : t('AVAILABLE', 'Available')
346
- }
347
- </OText>
348
- </View>
349
- </View>
350
- </>
351
- ) : (
352
- <OText size={12}>{t('SELECT_PROFESSIONAL', 'Select professional')}</OText>
353
- )}
354
- <View style={{ marginLeft: 5 }}>
355
- <IconAntDesign
356
- name='down'
357
- color={theme.colors.textThird}
358
- size={12}
359
- />
360
- </View>
361
- </TouchableOpacity>
362
- </ProfessionalWrapper>
363
- <ScheduleWrapper>
364
- <View
365
- style={{
366
- flexDirection: 'row',
367
- justifyContent: 'space-between',
368
- alignItems: 'center',
369
- marginBottom: 23
370
- }}
371
- >
372
288
  <OText
373
289
  size={16}
374
- weight={Platform.OS === 'ios' ? '600' : 'bold'}
290
+ style={{ marginBottom: 10 }}
291
+ weight={'400'}
375
292
  >
376
- {t('SCHEDULE', 'Schedule')}
293
+ {parsePrice(product?.price)} • {product?.duration}min
377
294
  </OText>
378
295
  <OText
379
- size={10}
296
+ size={14}
380
297
  weight={'400'}
381
- color={theme.colors?.danger5}
298
+ color={theme?.colors?.disabled}
382
299
  >
383
- {t('REQUIRED', 'Required')}
300
+ {product?.description}
384
301
  </OText>
385
- </View>
386
- {!!currentProfessional?.schedule ? (
387
- <CalendarWrapper>
388
- {(timeList?.length > 0 && isEnabled) ? (
389
- <SelectDropdown
390
- ref={dropdownRef}
391
- defaultValue={timeSelected}
392
- data={timeList}
393
- onSelect={(selectedItem, index) => {
394
- setTimeSelected(selectedItem?.value)
395
- }}
396
- buttonTextAfterSelection={(selectedItem, index) => {
397
- return selectedItem?.text
398
- }}
399
- rowTextForSelection={(item, index) => {
400
- return item.text
401
- }}
402
- buttonStyle={{borderRadius: 7.6, ...styles.selectOption}}
403
- buttonTextStyle={{
404
- color: theme.colors.disabled,
405
- fontSize: 14,
406
- textAlign: 'left',
407
- marginHorizontal: 0
408
- }}
409
- dropdownStyle={{
410
- borderRadius: 8,
411
- borderColor: theme.colors.lightGray,
412
- marginTop: Platform.OS === 'ios' ? 12 : -top
413
- }}
414
- rowStyle={{
415
- borderBottomColor: theme.colors.backgroundGray100,
416
- backgroundColor: theme.colors.backgroundGray100,
417
- height: 30,
418
- flexDirection: 'column',
419
- alignItems: 'flex-start',
420
- paddingTop: 8,
421
- paddingHorizontal: 12
422
- }}
423
- rowTextStyle={{
424
- color: theme.colors.disabled,
425
- fontSize: 14,
426
- marginHorizontal: 0
427
- }}
428
- renderDropdownIcon={() => dropDownIcon()}
429
- dropdownOverlayColor='transparent'
430
- />
302
+ </InfoWrapper>
303
+ <Divider />
304
+ <ProfessionalWrapper>
305
+ <View
306
+ style={{
307
+ flexDirection: 'row',
308
+ justifyContent: 'space-between',
309
+ alignItems: 'center',
310
+ marginBottom: 23
311
+ }}
312
+ >
313
+ <OText
314
+ size={16}
315
+ weight={Platform.OS === 'ios' ? '600' : 'bold'}
316
+ >
317
+ {t('PROFESSIONAL', 'Professional')}
318
+ </OText>
319
+ <OText
320
+ size={10}
321
+ weight={'400'}
322
+ color={theme.colors?.danger5}
323
+ >
324
+ {t('REQUIRED', 'Required')}
325
+ </OText>
326
+ </View>
327
+ <TouchableOpacity
328
+ style={styles.professionalSelect}
329
+ onPress={() => setIsOpen(true)}
330
+ >
331
+ {!!currentProfessional ? (
332
+ <>
333
+ <View style={{ flexDirection: 'row' }}>
334
+ {!!currentProfessional?.photo ? (
335
+ <FastImage
336
+ style={styles.photoStyle}
337
+ source={{
338
+ uri: optimizeImage(currentProfessional?.photo, 'h_250,c_limit'),
339
+ priority: FastImage.priority.normal,
340
+ }}
341
+ resizeMode={FastImage.resizeMode.cover}
342
+ />
343
+ ) : (
344
+ <OIcon
345
+ src={theme?.images?.general?.user}
346
+ cover={false}
347
+ style={styles.photoStyle}
348
+ />
349
+ )}
350
+ <View style={{ marginLeft: 14 }}>
351
+ <OText
352
+ size={14}
353
+ weight={'400'}
354
+ lineHeight={22}
355
+ >
356
+ {currentProfessional?.name} {currentProfessional?.lastname}
357
+ </OText>
358
+ <OText
359
+ size={12}
360
+ weight={'400'}
361
+ lineHeight={17}
362
+ color={isBusyTime(currentProfessional) ? theme.colors.danger5 : theme.colors.success500}
363
+ >
364
+ {isBusyTime(currentProfessional)
365
+ ? t('BUSY_ON_SELECTED_TIME', 'Busy on selected time')
366
+ : t('AVAILABLE', 'Available')
367
+ }
368
+ </OText>
369
+ </View>
370
+ </View>
371
+ </>
431
372
  ) : (
432
- <OText
433
- size={12}
434
- style={{ marginBottom: 30 }}
435
- weight={'400'}
436
- color={theme.colors?.danger5}
437
- >
438
- {t('PROFESSIONAL_NOT_AVAILABLE', 'Professional is not available at the moment')}
439
- </OText>
373
+ <OText size={12}>{t('SELECT_PROFESSIONAL', 'Select professional')}</OText>
440
374
  )}
441
- <CalendarPicker
442
- previousComponent={
443
- <FeatherIcon
444
- name='chevron-left'
445
- color={theme.colors.disabled}
446
- size={24}
447
- style={{ marginHorizontal: 4 }}
448
- />
449
- }
450
- nextComponent={
451
- <FeatherIcon
452
- name='chevron-right'
453
- color={theme.colors.disabled}
454
- size={24}
455
- style={{ marginHorizontal: 4 }}
375
+ <View style={{ marginLeft: 5 }}>
376
+ <IconAntDesign
377
+ name='down'
378
+ color={theme.colors.textThird}
379
+ size={12}
380
+ />
381
+ </View>
382
+ </TouchableOpacity>
383
+ </ProfessionalWrapper>
384
+ <ScheduleWrapper>
385
+ <View
386
+ style={{
387
+ flexDirection: 'row',
388
+ justifyContent: 'space-between',
389
+ alignItems: 'center',
390
+ marginBottom: 23
391
+ }}
392
+ >
393
+ <OText
394
+ size={16}
395
+ weight={Platform.OS === 'ios' ? '600' : 'bold'}
396
+ >
397
+ {t('SCHEDULE', 'Schedule')}
398
+ </OText>
399
+ <OText
400
+ size={10}
401
+ weight={'400'}
402
+ color={theme.colors?.danger5}
403
+ >
404
+ {t('REQUIRED', 'Required')}
405
+ </OText>
406
+ </View>
407
+ {!!currentProfessional?.schedule ? (
408
+ <CalendarWrapper>
409
+ {(timeList?.length > 0 && isEnabled) ? (
410
+ <SelectDropdown
411
+ ref={dropdownRef}
412
+ defaultValue={timeSelected}
413
+ data={timeList}
414
+ onSelect={(selectedItem, index) => {
415
+ setTimeSelected(selectedItem?.value)
416
+ }}
417
+ buttonTextAfterSelection={(selectedItem, index) => {
418
+ return selectedItem?.text
419
+ }}
420
+ rowTextForSelection={(item, index) => {
421
+ return item.text
422
+ }}
423
+ buttonStyle={{borderRadius: 7.6, ...styles.selectOption}}
424
+ buttonTextStyle={{
425
+ color: theme.colors.disabled,
426
+ fontSize: 14,
427
+ textAlign: 'left',
428
+ marginHorizontal: 0
429
+ }}
430
+ dropdownStyle={{
431
+ borderRadius: 8,
432
+ borderColor: theme.colors.lightGray,
433
+ marginTop: Platform.OS === 'ios' ? 12 : -top
434
+ }}
435
+ rowStyle={{
436
+ borderBottomColor: theme.colors.backgroundGray100,
437
+ backgroundColor: theme.colors.backgroundGray100,
438
+ height: 30,
439
+ flexDirection: 'column',
440
+ alignItems: 'flex-start',
441
+ paddingTop: 8,
442
+ paddingHorizontal: 12
443
+ }}
444
+ rowTextStyle={{
445
+ color: theme.colors.disabled,
446
+ fontSize: 14,
447
+ marginHorizontal: 0
448
+ }}
449
+ renderDropdownIcon={() => dropDownIcon()}
450
+ dropdownOverlayColor='transparent'
456
451
  />
457
- }
458
- width={screenWidth - 110}
459
- selectedDayTextColor={theme.colors.white}
460
- selectedDayColor={theme.colors.primary}
461
- todayBackgroundColor={theme.colors.border}
462
- dayLabelsWrapper={{ borderColor: theme.colors.clear }}
463
- onDateChange={onDateChange}
464
- minDate={new Date()}
465
- customDayHeaderStyles={customDayHeaderStylesCallback}
466
- selectedStartDate={selectDate}
467
- />
468
- </CalendarWrapper>
469
- ) : (
452
+ ) : (
453
+ <OText
454
+ size={12}
455
+ style={{ marginBottom: 30 }}
456
+ weight={'400'}
457
+ color={theme.colors?.danger5}
458
+ >
459
+ {t('PROFESSIONAL_NOT_AVAILABLE', 'Professional is not available at the moment')}
460
+ </OText>
461
+ )}
462
+ <CalendarPicker
463
+ previousComponent={
464
+ <FeatherIcon
465
+ name='chevron-left'
466
+ color={theme.colors.disabled}
467
+ size={24}
468
+ style={{ marginHorizontal: 4 }}
469
+ />
470
+ }
471
+ nextComponent={
472
+ <FeatherIcon
473
+ name='chevron-right'
474
+ color={theme.colors.disabled}
475
+ size={24}
476
+ style={{ marginHorizontal: 4 }}
477
+ />
478
+ }
479
+ width={screenWidth - 110}
480
+ selectedDayTextColor={theme.colors.white}
481
+ selectedDayColor={theme.colors.primary}
482
+ todayBackgroundColor={theme.colors.border}
483
+ dayLabelsWrapper={{ borderColor: theme.colors.clear }}
484
+ onDateChange={onDateChange}
485
+ minDate={new Date()}
486
+ customDayHeaderStyles={customDayHeaderStylesCallback}
487
+ selectedStartDate={selectDate}
488
+ />
489
+ </CalendarWrapper>
490
+ ) : (
491
+ <OText
492
+ size={16}
493
+ style={{ marginBottom: 30, textAlign: 'center' }}
494
+ color={theme?.colors?.disabled}
495
+ weight={Platform.OS === 'ios' ? '600' : 'bold'}
496
+ >
497
+ {t('NO_SCHEDULE', 'No schedule')}
498
+ </OText>
499
+ )}
500
+ </ScheduleWrapper>
501
+ <ButtonWrapper>
470
502
  <OText
471
- size={16}
472
- style={{ marginBottom: 30, textAlign: 'center' }}
473
- color={theme?.colors?.disabled}
503
+ size={14}
474
504
  weight={Platform.OS === 'ios' ? '600' : 'bold'}
475
505
  >
476
- {t('NO_SCHEDULE', 'No schedule')}
506
+ {dateSelected && moment(dateSelected).format('hh:mm A')}
477
507
  </OText>
478
- )}
479
- </ScheduleWrapper>
480
- <ButtonWrapper>
481
- <OText
482
- size={14}
483
- weight={Platform.OS === 'ios' ? '600' : 'bold'}
484
- >
485
- {dateSelected && moment(dateSelected).format('hh:mm A')}
486
- </OText>
487
- {((productCart &&
488
- auth &&
489
- orderState.options?.address_id)) && (
490
- <OButton
491
- bgColor={theme.colors.primary}
492
- onClick={() => handleSaveService()}
493
- text={orderState.loading
494
- ? t('LOADING', 'Loading')
495
- : ((isSoldOut || maxProductQuantity <= 0)
496
- ? t('SOLD_OUT', 'Sold out')
497
- : t('BOOK', 'Book'))}
498
- style={styles.buttonStyle}
499
- isDisabled={isSoldOut || maxProductQuantity <= 0 || !currentProfessional?.id || !dateSelected}
500
- textStyle={{ fontSize: 14, color: theme.colors.white }}
501
- />
502
- )}
503
- {auth &&
504
- !orderState.options?.address_id &&
505
- (orderState.loading ? (
506
- <OButton
507
- isDisabled
508
- text={t('LOADING', 'Loading')}
509
- imgRightSrc=""
510
- textStyle={{ fontSize: 10 }}
511
- />
512
- ) : (
513
- <OButton onClick={() => addressRedirect()} />
514
- ))}
515
- {!auth && (
516
- <OButton
517
- isDisabled={isSoldOut || maxProductQuantity <= 0}
518
- onClick={() => handleRedirectLogin()}
519
- text={
520
- isSoldOut || maxProductQuantity <= 0
521
- ? t('SOLD_OUT', 'Sold out')
522
- : t('LOGIN_SIGNUP', 'Login / Sign Up')
523
- }
524
- imgRightSrc=""
525
- textStyle={{ color: theme.colors.primary, fontSize: 14 }}
526
- style={{
527
- height: 44,
528
- borderColor: theme.colors.primary,
529
- backgroundColor: theme.colors.white,
530
- }}
531
- />
532
- )}
533
- </ButtonWrapper>
534
- </Container>
508
+ {((productCart &&
509
+ auth &&
510
+ orderState.options?.address_id)) && (
511
+ <OButton
512
+ bgColor={theme.colors.primary}
513
+ onClick={() => handleSaveService()}
514
+ text={orderState.loading
515
+ ? t('LOADING', 'Loading')
516
+ : ((isSoldOut || maxProductQuantity <= 0)
517
+ ? t('SOLD_OUT', 'Sold out')
518
+ : t('BOOK', 'Book'))}
519
+ style={styles.buttonStyle}
520
+ isDisabled={isSoldOut || maxProductQuantity <= 0 || !currentProfessional?.id || !dateSelected}
521
+ textStyle={{ fontSize: 14, color: theme.colors.white }}
522
+ />
523
+ )}
524
+ {auth &&
525
+ !orderState.options?.address_id &&
526
+ (orderState.loading ? (
527
+ <OButton
528
+ isDisabled
529
+ text={t('LOADING', 'Loading')}
530
+ imgRightSrc=""
531
+ textStyle={{ fontSize: 10 }}
532
+ />
533
+ ) : (
534
+ <OButton onClick={() => addressRedirect()} />
535
+ ))}
536
+ {!auth && (
537
+ <OButton
538
+ isDisabled={isSoldOut || maxProductQuantity <= 0}
539
+ onClick={() => handleRedirectLogin()}
540
+ text={
541
+ isSoldOut || maxProductQuantity <= 0
542
+ ? t('SOLD_OUT', 'Sold out')
543
+ : t('LOGIN_SIGNUP', 'Login / Sign Up')
544
+ }
545
+ imgRightSrc=""
546
+ textStyle={{ color: theme.colors.primary, fontSize: 14 }}
547
+ style={{
548
+ height: 44,
549
+ borderColor: theme.colors.primary,
550
+ backgroundColor: theme.colors.white,
551
+ }}
552
+ />
553
+ )}
554
+ </ButtonWrapper>
555
+ </Container>
556
+ )}
557
+
535
558
  <OModal
536
559
  open={isOpen}
537
560
  onClose={() => setIsOpen(false)}
@@ -546,7 +569,7 @@ const ServiceFormUI = (props: ServiceFormParams) => {
546
569
  {t('ANY_OROFESSIONAL_MEMBER', 'Any professional member')}
547
570
  </OText>
548
571
  </View>
549
- {professionalList?.map((professional: any) => (
572
+ {professionalListState?.professionals?.map((professional: any) => (
550
573
  <TouchableOpacity
551
574
  key={professional?.id}
552
575
  style={styles.professionalItem}
@@ -743,7 +743,10 @@ export interface ServiceFormParams {
743
743
  maxProductQuantity: any,
744
744
  businessSlug?: string,
745
745
  onClose: any,
746
- professionalList: any
746
+ professionalList: any,
747
+ productObject?: any,
748
+ professionalListState?: any,
749
+ isCartProduct?: any
747
750
  }
748
751
 
749
752
  export interface ProfessionalFilterParams {