@lancom/shared 0.0.423 → 0.0.424

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/assets/js/api/admin.js +33 -7
  2. package/assets/js/api/index.js +17 -3
  3. package/assets/js/utils/share-promises/index.js +76 -0
  4. package/components/common/coupon_select/coupon-select.vue +12 -2
  5. package/components/common/phone_input/phone-input.vue +15 -3
  6. package/components/common/postcode_select/postcode-select.vue +3 -1
  7. package/components/customer/customer_navigation_menu/customer-navigation-menu.vue +11 -7
  8. package/components/customer/customer_preferences_form/customer-preferences-form.scss +12 -0
  9. package/components/customer/customer_preferences_form/customer-preferences-form.vue +87 -0
  10. package/components/pages/customer/preferences/preferences.vue +79 -0
  11. package/components/product/product.vue +0 -12
  12. package/components/product/product_reviews/product-reviews.vue +6 -6
  13. package/components/product/related_products/related-products.vue +4 -4
  14. package/components/resource/resource_view/resource-view.mixin.js +74 -0
  15. package/components/resource/resource_view/resource-view.scss +13 -0
  16. package/components/resource/resource_view/resource-view.vue +97 -0
  17. package/components/resource/resource_view/resource_view_prints/resource-view-prints.scss +7 -0
  18. package/components/resource/resource_view/resource_view_prints/resource-view-prints.vue +42 -0
  19. package/components/resource/resource_view/resource_view_prints/resource_view_print/resource-view-print.scss +5 -0
  20. package/components/resource/resource_view/resource_view_prints/resource_view_print/resource-view-print.vue +54 -0
  21. package/components/resource/resource_view/resource_view_prints/resource_view_print/resource_view_print_products/resource-view-print-products.scss +0 -0
  22. package/components/resource/resource_view/resource_view_prints/resource_view_print/resource_view_print_products/resource-view-print-products.vue +45 -0
  23. package/components/resource/resource_view/resource_view_prints/resource_view_print/resource_view_print_products/resource_view_print_product/resource-view-print-product.scss +0 -0
  24. package/components/resource/resource_view/resource_view_prints/resource_view_print/resource_view_print_products/resource_view_print_product/resource-view-print-product.vue +44 -0
  25. package/package.json +1 -1
  26. package/pages/customer/preferences.vue +33 -0
  27. package/pages/order/_token/resource/_resource.vue +53 -0
  28. package/routes/index.js +10 -0
  29. package/store/auth.js +1 -0
  30. package/store/index.js +12 -3
  31. package/store/product.js +1 -1
@@ -3,6 +3,12 @@ import { _get, _post, _delete, _put } from './helpers';
3
3
  import { sortSizes } from './../utils/sizes';
4
4
  import { sortByName } from './../utils/filters';
5
5
 
6
+ const CACHE = {
7
+ PRINT_AREAS: 'print-areas',
8
+ PACKAGES: 'packages'
9
+ };
10
+ const promiseCache = new Map();
11
+
6
12
  export default {
7
13
  updateInventoryFromLink(link, warehouse) {
8
14
  const url = `admin/products/inventory/link${warehouse ? `?warehouse=${warehouse}` : ''}`;
@@ -110,6 +116,9 @@ export default {
110
116
  saveOrder(order) {
111
117
  return order._id ? _put(`admin/order/${order._id}`, order) : _post('admin/order', order);
112
118
  },
119
+ validateOrderDate(order, field) {
120
+ return _post(`admin/order/${order._id}/validate-date`, { [field]: order[field] });
121
+ },
113
122
  fetchPurchaseOrders(params) {
114
123
  return _get(`admin/purchase-orders`, params);
115
124
  },
@@ -215,8 +224,13 @@ export default {
215
224
  removeCategory(id) {
216
225
  return _delete(`admin/categories/${id}`);
217
226
  },
227
+ dropPackagesCache() {
228
+ promiseCache.delete(CACHE.PACKAGES);
229
+ },
218
230
  fetchPackages() {
219
- return _get('admin/packages');
231
+ const promise = promiseCache.get(CACHE.PACKAGES) || _get('admin/packages');
232
+ promiseCache.set(CACHE.PACKAGES, promise);
233
+ return promise;
220
234
  },
221
235
  fetchPackageById(id) {
222
236
  return _get(`admin/packages/${id}`);
@@ -660,8 +674,16 @@ export default {
660
674
  removePrintType(id) {
661
675
  return _delete(`admin/print-types/${id}`);
662
676
  },
663
- fetchPrintAreas() {
664
- return _get('admin/print-areas');
677
+ dropPrintAreasCache() {
678
+ promiseCache.delete(CACHE.PRINT_AREAS);
679
+ },
680
+ fetchPrintAreas(params) {
681
+ if (params) {
682
+ return _get('admin/print-areas', params);
683
+ }
684
+ const promise = promiseCache.get(CACHE.PRINT_AREAS) || _get('admin/print-areas');
685
+ promiseCache.set(CACHE.PRINT_AREAS, promise);
686
+ return promise;
665
687
  },
666
688
  fetchPrintAreaById(id) {
667
689
  return _get(`admin/print-areas/${id}`);
@@ -672,8 +694,8 @@ export default {
672
694
  removePrintArea(id) {
673
695
  return _delete(`admin/print-areas/${id}`);
674
696
  },
675
- fetchPrintSizes() {
676
- return _get('admin/print-sizes');
697
+ fetchPrintSizes(params) {
698
+ return _get('admin/print-sizes', params);
677
699
  },
678
700
  fetchPrintSizeById(id) {
679
701
  return _get(`admin/print-sizes/${id}`);
@@ -804,7 +826,11 @@ export default {
804
826
  findResources(query) {
805
827
  return _get(`admin/order/resources?search=${query || ''}`);
806
828
  },
807
- getReOrderReport(params) {
808
- return _get(`admin/reports/re-order`, params);
829
+ approveMockupRequest(order, resource) {
830
+ const shopId = order.shop?._id || order.shop;
831
+ return _post(`admin/shop/${shopId}/order/${order._id}/resources/${resource._id}/approve-request`, {});
809
832
  },
833
+ getReOrderReport(params) {
834
+ return _get('admin/reports/re-order', params);
835
+ }
810
836
  };
@@ -1,10 +1,15 @@
1
-
2
1
  import { _get, _post, _put, _delete, _patch } from './helpers';
3
2
  import adminApi from './admin';
4
3
  import { unminifySimpleProducts } from './utils/simple-products';
5
4
  import { unminifyProduct } from './utils/product';
5
+ import { getSharePromise } from './../utils/share-promises';
6
+
7
+ const shareRequestsPromises = getSharePromise(5 * 60 * 1000);
6
8
 
7
9
  const api = {
10
+ clearAllSharePromises() {
11
+ return _post('clear-share-promises');
12
+ },
8
13
  fetchClientSettings(shop, params) {
9
14
  return _get(`shop/${shop}/client-settings`, params);
10
15
  },
@@ -20,6 +25,9 @@ const api = {
20
25
  saveCustomer(customer, shop) {
21
26
  return customer._id ? _patch(`shop/${shop}/customer/${customer.accessToken}`, customer) : _post(`shop/${shop}/customer`, customer);
22
27
  },
28
+ fetchCustomer(accessToken, shop) {
29
+ return _get(`shop/${shop}/customer/${accessToken}`);
30
+ },
23
31
  authCustomer(customer, shop) {
24
32
  return _post(`shop/${shop}/customer/login`, customer);
25
33
  },
@@ -69,7 +77,8 @@ const api = {
69
77
  return _get(`shop/${shop}/menus`);
70
78
  },
71
79
  fetchBanners(shop, params) {
72
- return _get(`shop/${shop}/banners`, params);
80
+ const KEY = `shop/${shop}/banners_${JSON.stringify(params || {})}`;
81
+ return shareRequestsPromises.share(KEY, () => _get(`shop/${shop}/banners`, params));
73
82
  },
74
83
  fetchProductsKits(shop, params) {
75
84
  return _get(`shop/${shop}/products-kit`, params);
@@ -84,7 +93,8 @@ const api = {
84
93
  return _get(`shop/${shop}/news/${alias}?preview=${preview}`);
85
94
  },
86
95
  fetchNewsTags(shop, params) {
87
- return _get(`shop/${shop}/news-tag`, params);
96
+ const KEY = `shop/${shop}/news-tag_${JSON.stringify(params || {})}`
97
+ return shareRequestsPromises.share(KEY, () => _get(`shop/${shop}/news-tag`, params));
88
98
  },
89
99
  fetchNewsTag(shop, alias) {
90
100
  return _get(`shop/${shop}/news-tag/${alias}`);
@@ -122,6 +132,10 @@ const api = {
122
132
  addOrderTimeline(id, item) {
123
133
  return _post(`order/${id}/timeline`, item);
124
134
  },
135
+ approveOrderMockup(order, resource) {
136
+ const shopId = order.shop?._id || order.shop;
137
+ return _post(`shop/${shopId}/order/${order._id}/resources/${resource._id}/approve`, resource);
138
+ },
125
139
  generateApprovalPDFs(token) {
126
140
  return _post(`order/${token}/approval-pdfs`);
127
141
  },
@@ -0,0 +1,76 @@
1
+ const MAX_PROMISES_COUNT = 1000;
2
+ const DEFAULT_SHARE_LIFETIME = 10 * 1000;
3
+
4
+ export class SharePromises {
5
+ constructor(clearSharesTimeoutDelay) {
6
+ this.promises = new Map();
7
+ this.clearSharesTimeout = null;
8
+ this.clearSharesTimeoutDelay = clearSharesTimeoutDelay || DEFAULT_SHARE_LIFETIME;
9
+ }
10
+
11
+ startClearTimeout(key) {
12
+ const promise = this.promises.get(key);
13
+ if (promise) {
14
+ clearTimeout(promise.timeout);
15
+ promise.timeout = setTimeout(() => {
16
+ this.promises.delete(key);
17
+ }, this.clearSharesTimeoutDelay);
18
+ }
19
+ }
20
+
21
+ share(keyObj, promiseFactory) {
22
+ const key = `${keyObj}`;
23
+ let promise = this.promises.get(key);
24
+ if (!promise) {
25
+ this.checkMaxPromisesCount();
26
+
27
+ promise = {
28
+ promise: promiseFactory(),
29
+ timeout: null
30
+ };
31
+
32
+ this.promises.set(key, promise);
33
+
34
+ this.startClearTimeout(key);
35
+ }
36
+
37
+ promise.promise = promise.promise.then((result) => result);
38
+
39
+ return promise.promise;
40
+ }
41
+
42
+ checkMaxPromisesCount() {
43
+ if (this.promises.size >= MAX_PROMISES_COUNT) {
44
+ this.clear();
45
+ }
46
+ }
47
+
48
+ clear() {
49
+ this.promises.forEach(promise => {
50
+ clearTimeout(promise.timeout);
51
+ });
52
+ this.promises.clear();
53
+ }
54
+
55
+ clearPromise(keyObj) {
56
+ const key = `${keyObj}`;
57
+ let promise = this.promises.get(key);
58
+ if (promise) {
59
+ clearTimeout(promise.timeout);
60
+ this.promises.delete(key);
61
+ }
62
+ }
63
+ }
64
+
65
+ const allSharePromises = [];
66
+ export function getSharePromise(timeout) {
67
+ const sharePromise = new SharePromises(timeout);
68
+ allSharePromises.push(sharePromise);
69
+ return sharePromise;
70
+ }
71
+
72
+ export function clearAllSharePromises() {
73
+ for (const sharePromise of allSharePromises) {
74
+ sharePromise.clear();
75
+ }
76
+ }
@@ -74,7 +74,7 @@
74
74
  <div
75
75
  v-else
76
76
  class="lc_caption form-help is-danger">
77
- Invalid coupon: Min Order for Coupon: {{ value.minOrderValue | price(currency) }}
77
+ Invalid coupon: Min Order for Coupon: {{ minOrderValue | price(currency) }}
78
78
  </div>
79
79
  <div v-if="hasQualifyingProducts" class="lc_caption mb-10">
80
80
  <div v-if="noQualifyingProductsInCart">
@@ -129,8 +129,18 @@ export default {
129
129
  hasQualifyingProductsInCart() {
130
130
  return this.sumQualifyingProductsInCart > 0;
131
131
  },
132
+ minOrderValue() {
133
+ const value = [
134
+ this.value,
135
+ ...(this.value?.values || [])
136
+ ]
137
+ .filter(v => v?.minOrderValue > 0)
138
+ .sort((a, b) => a.minOrderValue - b.minOrderValue)[0];
139
+
140
+ return value?.minOrderValue || 0;
141
+ },
132
142
  isValidPricing() {
133
- return !this.value?.minOrderValue || this.pricing.coupon;
143
+ return !this.minOrderValue || this.pricing.coupon;
134
144
  },
135
145
  model: {
136
146
  get() {
@@ -1,9 +1,9 @@
1
1
  <template>
2
2
  <div>
3
- <div class="form-row">
3
+ <div :class="containerClassname">
4
4
  <label
5
5
  v-if="labelless"
6
- class="form-label"
6
+ :class="labelClassname"
7
7
  @click="$refs.phone.$el.focus()">
8
8
  {{ displayLabelText }}
9
9
  </label>
@@ -31,8 +31,8 @@
31
31
  :disabled="disabled"
32
32
  :name="'phone' + uniqueKey"
33
33
  type="text"
34
- class="form-field"
35
34
  :class="{
35
+ [inputClassname]: true,
36
36
  'is-danger': errors.length,
37
37
  filled: model,
38
38
  labelless
@@ -115,6 +115,18 @@ export default {
115
115
  namePrefix: {
116
116
  type: String,
117
117
  default: ''
118
+ },
119
+ containerClassname: {
120
+ type: String,
121
+ default: 'form-row'
122
+ },
123
+ labelClassname: {
124
+ type: String,
125
+ default: 'form-label'
126
+ },
127
+ inputClassname: {
128
+ type: String,
129
+ default: 'form-field'
118
130
  }
119
131
  },
120
132
  data() {
@@ -177,7 +177,9 @@ export default {
177
177
  if (this.suburb) {
178
178
  const option = this.createOptionFromSuburb(this.suburb);
179
179
  this.selected = option;
180
- await this.handleSearch(this.selected.value.trim())
180
+ if (this.selected.value) {
181
+ await this.handleSearch(this.selected.value.trim());
182
+ }
181
183
  }
182
184
  },
183
185
  methods: {
@@ -15,6 +15,12 @@
15
15
  <script>
16
16
  export default {
17
17
  name: 'CustomerMenu',
18
+ props: {
19
+ activeMenuItem: {
20
+ type: String,
21
+ required: true
22
+ }
23
+ },
18
24
  data() {
19
25
  return {
20
26
  menuItems: [{
@@ -29,14 +35,12 @@ export default {
29
35
  label: 'Coupons',
30
36
  key: 'coupons',
31
37
  url: '/customer/coupons'
38
+ }, {
39
+ label: 'Preferences',
40
+ key: 'preferences',
41
+ url: '/customer/preferences'
32
42
  }]
33
43
  };
34
- },
35
- props: {
36
- activeMenuItem: {
37
- type: String,
38
- required: true
39
- }
40
44
  }
41
45
  };
42
46
  </script>
@@ -68,5 +72,5 @@ export default {
68
72
  background-color: $green;
69
73
  }
70
74
  }
71
- }
75
+ }
72
76
  </style>
@@ -0,0 +1,12 @@
1
+ @import "@/assets/scss/variables";
2
+
3
+ .CustomerPreferencesForm {
4
+ &__error {
5
+ font-weight: bold;
6
+ font-size: 14px;
7
+ padding: 18px 10px;
8
+ text-align: center;
9
+ color: $white;
10
+ background: #EA3434;
11
+ }
12
+ }
@@ -0,0 +1,87 @@
1
+ <template>
2
+ <div class="form__wrapper CustomerFrom__wrapper">
3
+ <validation-observer ref="form">
4
+ <div class="mt-10">
5
+ <div
6
+ class="row"
7
+ style="margin-top: -20px">
8
+ <div class="col-12">
9
+ <div class="form-row">
10
+ <label class="form-label">
11
+ <checkbox v-model="customer.signUpForNewsletter" />
12
+ <span class="lc_regular12 lc__grey1">
13
+ Sign up for newsletter
14
+ </span>
15
+ </label>
16
+ </div>
17
+ </div>
18
+ </div>
19
+ </div>
20
+ <div class="form__footer mt-5">
21
+ <div
22
+ v-if="errorMessage"
23
+ class="CustomerForm__error">
24
+ {{ errorMessage }}
25
+ </div>
26
+ <div class="form-actions full">
27
+ <btn
28
+ class="btn btn-black"
29
+ :btn-processing="processing"
30
+ :btn-disabled="processing"
31
+ :btn-label="customer._id ? 'Save' : 'Create Account'"
32
+ @onclick="submit()">
33
+ </btn>
34
+ </div>
35
+ </div>
36
+ </validation-observer>
37
+ </div>
38
+ </template>
39
+
40
+ <script>
41
+ import { mapGetters } from 'vuex';
42
+ import api from '@lancom/shared/assets/js/api';
43
+
44
+ export default {
45
+ name: 'CustomerPreferencesForm',
46
+ props: {
47
+ customer: {
48
+ type: Object,
49
+ required: true
50
+ }
51
+ },
52
+ data() {
53
+ return {
54
+ errorMessage: null,
55
+ processing: false
56
+ };
57
+ },
58
+ computed: {
59
+ ...mapGetters(['shop'])
60
+ },
61
+ methods: {
62
+ async submit() {
63
+ this.errorMessage = null;
64
+
65
+ const isValid = await this.$refs.form.validate();
66
+ if (!isValid) {
67
+ return;
68
+ }
69
+
70
+ try {
71
+ this.processing = true;
72
+ const result = await api.saveCustomer(this.customer, this.shop._id);
73
+ this.$emit('save', result);
74
+ } catch ({ response }) {
75
+ const { data } = response || {};
76
+ this.errorMessage = data && data.error;
77
+ } finally {
78
+ this.processing = false;
79
+ }
80
+ }
81
+ }
82
+ };
83
+ </script>
84
+
85
+ <style lang="scss" scoped>
86
+ @import 'customer-preferences-form.scss';
87
+ </style>
@@ -0,0 +1,79 @@
1
+ <template>
2
+ <div class="CustomerPreferences__wrapper">
3
+ <customer-preferences-form
4
+ v-if="customer"
5
+ ref="customerForm"
6
+ :customer="customer"
7
+ @save="onSave"
8
+ @customer-loaded="onCustomerLoaded" />
9
+ </div>
10
+ </template>
11
+
12
+ <script>
13
+ import api from '@lancom/shared/assets/js/api';
14
+ import { mapGetters, mapActions } from 'vuex';
15
+ import CustomerPreferencesForm from '@lancom/shared/components/customer/customer_preferences_form/customer-preferences-form';
16
+
17
+ export default {
18
+ name: 'CustomerPreferences',
19
+ components: {
20
+ CustomerPreferencesForm
21
+ },
22
+ data() {
23
+ return {
24
+ customer: null,
25
+ loading: false,
26
+ error: null
27
+ };
28
+ },
29
+ computed: {
30
+ ...mapGetters(['shop']),
31
+ ...mapGetters('auth', ['user'])
32
+ },
33
+ async created() {
34
+ await this.loadCustomerData();
35
+ },
36
+ methods: {
37
+ ...mapActions('auth', ['update_user']),
38
+ async loadCustomerData() {
39
+ try {
40
+ this.processing = true;
41
+ const customer = await api.fetchCustomer(this.user.accessToken, this.shop._id);
42
+ console.log('customer: ', customer);
43
+ this.customer = customer;
44
+ } catch (e) {
45
+ console.log(e);
46
+ const errorMessage = e?.response?.data?.error;
47
+ this.$toastr.e(errorMessage || 'Customer failed to load');
48
+ } finally {
49
+ this.processing = false;
50
+ }
51
+ },
52
+ onSave({ customer: user, token }) {
53
+ this.$toastr.s('Customer successfully saved');
54
+ this.update_user({ user, token });
55
+ },
56
+ onCustomerLoaded(customer) {
57
+ this.customer = customer;
58
+ }
59
+ }
60
+ };
61
+ </script>
62
+
63
+ <style lang="scss" scoped>
64
+ .loading-message {
65
+ text-align: center;
66
+ padding: 2rem;
67
+ font-size: 1.1rem;
68
+ color: #666;
69
+ }
70
+
71
+ .error-message {
72
+ text-align: center;
73
+ padding: 2rem;
74
+ color: #d32f2f;
75
+ background-color: #ffebee;
76
+ border-radius: 4px;
77
+ margin: 1rem 0;
78
+ }
79
+ </style>
@@ -32,12 +32,6 @@
32
32
  <section class="Product__section">
33
33
  <wizard-editor />
34
34
  </section>
35
- <!-- <section class="Product__section">
36
- <product-colors-selector v-if="productDetails" />
37
- </section> -->
38
- <!-- <section class="Product__section">
39
- <product-tier-prices :product="product" />
40
- </section> -->
41
35
  <section class="Product__section">
42
36
  <product-fabric-and-size-info :product="product" />
43
37
  </section>
@@ -59,23 +53,17 @@ import RelatedProducts from '@lancom/shared/components/product/related_products/
59
53
  import Gallery from '@lancom/shared/components/product/gallery/gallery';
60
54
  import { generateProductLink } from '@lancom/shared/assets/js/utils/product';
61
55
  import ProductMainInfo from './layouts/product_main_info/product-main-info';
62
- import ProductColorsSelector from './product_colors_selector/product-colors-selector';
63
- import ProductTierPrices from './layouts/product_tier_prices/product-tier-prices';
64
56
  import ProductFabricAndSizeInfo from './layouts/product_fabric_and_size_info/product-fabric-and-size-info';
65
57
  import ProductPackaging from './layouts/product_packaging/product-packaging';
66
58
  import ProductModelMeasurements from './layouts/product_model_measurements/product-model-measurements';
67
59
  import ProductSizeTable from './layouts/product_size_table/product-size-table';
68
60
  import WizardEditor from './wizard-editor/wizard-editor.vue';
69
61
 
70
-
71
- // import ProductLabelsCustomization from './layouts/product_labels_customization/product-labels-customization';
72
62
 
73
63
  export default {
74
64
  name: 'Product',
75
65
  components: {
76
66
  ProductMainInfo,
77
- ProductColorsSelector,
78
- ProductTierPrices,
79
67
  ProductFabricAndSizeInfo,
80
68
  ProductPackaging,
81
69
  ProductModelMeasurements,
@@ -41,6 +41,12 @@ export default {
41
41
  ProductReview
42
42
  },
43
43
  mixins: [],
44
+ props: {
45
+ product: {
46
+ type: Object,
47
+ required: true
48
+ }
49
+ },
44
50
  data() {
45
51
  const reviews = this.product.reviews || [];
46
52
  const [, mainReviewId] = (this.$route.hash || '').match(/review-([a-z0-9]+)/i) || [];
@@ -56,12 +62,6 @@ export default {
56
62
  ].filter(review => !!review)
57
63
  };
58
64
  },
59
- props: {
60
- product: {
61
- type: Object,
62
- required: true
63
- }
64
- },
65
65
  computed: {
66
66
  currentReview() {
67
67
  return this.reviews[this.activeIndex];
@@ -36,15 +36,15 @@ export default {
36
36
  products: []
37
37
  };
38
38
  },
39
+ async fetch() {
40
+ const { products } = await api.fetchRelatedProducts(this.shop._id, this.product.alias, { needShuffle: true });
41
+ this.products = products.splice(0, 6);
42
+ },
39
43
  computed: {
40
44
  ...mapGetters(['shop']),
41
45
  hasProducts() {
42
46
  return this.products.length > 0;
43
47
  }
44
- },
45
- async fetch() {
46
- const { products } = await api.fetchRelatedProducts(this.shop._id, this.product.alias, { needShuffle: true });
47
- this.products = products.splice(0, 6);
48
48
  }
49
49
  };
50
50
  </script>
@@ -0,0 +1,74 @@
1
+ import api from '@lancom/shared/assets/js/api';
2
+
3
+ const ORDER_RESOURCE_STATUS = {
4
+ PENDING_APPROVAL: 'pending approval',
5
+ APPROVED: 'approved',
6
+ NOT_APPROVED: 'not approved'
7
+ };
8
+
9
+ export default {
10
+ props: {
11
+ order: {
12
+ type: Object,
13
+ required: true
14
+ },
15
+ resource: {
16
+ type: Object,
17
+ required: true
18
+ }
19
+ },
20
+ data() {
21
+ return {
22
+ processing: false
23
+ };
24
+ },
25
+ computed: {
26
+ file() {
27
+ return this.resource.file;
28
+ },
29
+ isNotApproved() {
30
+ return this.resource.status === ORDER_RESOURCE_STATUS.NOT_APPROVED;
31
+ },
32
+ isApproved() {
33
+ return this.resource.status === ORDER_RESOURCE_STATUS.APPROVED;
34
+ },
35
+ isAvailableForApprove() {
36
+ const statuses = [
37
+ ORDER_RESOURCE_STATUS.APPROVED,
38
+ ORDER_RESOURCE_STATUS.NOT_APPROVED,
39
+ ORDER_RESOURCE_STATUS.PENDING_APPROVAL
40
+ ];
41
+ return statuses.includes(this.resource.status);
42
+ },
43
+ isValidStatus() {
44
+ const statuses = [
45
+ ORDER_RESOURCE_STATUS.APPROVED,
46
+ ORDER_RESOURCE_STATUS.NOT_APPROVED
47
+ ];
48
+ return statuses.includes(this.resource.status);
49
+ }
50
+ },
51
+ mounted() {
52
+ console.log('this.resource: ', this.resource);
53
+ console.log('this.order: ', this.order);
54
+ },
55
+ methods: {
56
+ approveMockup() {
57
+ this.resource.status = 'approved';
58
+ },
59
+ notApproveMockup() {
60
+ this.resource.status = 'not approved';
61
+ },
62
+ async submitApproval() {
63
+ this.processing = true;
64
+ try {
65
+ await api.approveOrderMockup(this.order, this.resource);
66
+ this.processing = false;
67
+ this.$toastr.s('Approval submitted successfully.');
68
+ } catch (error) {
69
+ this.processing = false;
70
+ this.$toastr.e('Failed to submit approval.');
71
+ }
72
+ }
73
+ }
74
+ };
@@ -0,0 +1,13 @@
1
+ .ResourceView {
2
+ &__wrapper {
3
+ margin-top: 50px;
4
+ }
5
+ &__approve-btn {
6
+ height: 42px;
7
+ }
8
+ &__approve {
9
+ margin-top: 30px;
10
+ border: 1px solid #f1f1f1;
11
+ padding: 10px;
12
+ }
13
+ }
@@ -0,0 +1,97 @@
1
+ <template>
2
+ <div class="ResourceView__wrapper">
3
+ <div class="row">
4
+ <div class="col-6">
5
+ <div class="ResourceView__code lc_h3">
6
+ {{ order.code }}
7
+ </div>
8
+ <div>
9
+ <div>
10
+ <div class="ResourceView__code lc_h5">
11
+ Approve Mockup
12
+ </div>
13
+ <div
14
+ v-if="file"
15
+ class="lc_regular16">
16
+ <span>
17
+ Mockup:
18
+ </span>
19
+ <a
20
+ :href="file.origin"
21
+ target="_blank"
22
+ class="lc_black">
23
+ {{ file.fileName }}
24
+ </a>
25
+ </div>
26
+ </div>
27
+ <div class="ResourceView__approve">
28
+ <div>
29
+ <btn
30
+ :btn-class="isApproved ? 'purple' : 'white'"
31
+ :btn-disabled="!isAvailableForApprove || processing"
32
+ :btn-processing="processing"
33
+ btn-label="Approved"
34
+ class="ResourceView__approve-btn"
35
+ @onclick="approveMockup()">
36
+ </btn>
37
+ <btn
38
+ :btn-class="isNotApproved ? 'purple' : 'white'"
39
+ :btn-disabled="!isAvailableForApprove"
40
+ :btn-processing="processing"
41
+ btn-label="Not Approved"
42
+ class="ResourceView__approve-btn"
43
+ @onclick="notApproveMockup()">
44
+ </btn>
45
+ </div>
46
+ <div class="mt-10">
47
+ <validation-provider
48
+ tag="div"
49
+ name="Comment"
50
+ class="mb-5 mt-5">
51
+ <textarea
52
+ id="timeline_message"
53
+ v-model="resource.comment"
54
+ name="comment"
55
+ style="height: 140px"
56
+ placeholder="Comment"
57
+ class="form-field no-label"></textarea>
58
+ </validation-provider>
59
+ </div>
60
+ <div class="mt-10">
61
+ <btn
62
+ btn-class="green"
63
+ :btn-disabled="!isValidStatus || processing"
64
+ :btn-processing="processing"
65
+ btn-label="Save"
66
+ class="ResourceView__approve-btn"
67
+ @onclick="submitApproval()">
68
+ </btn>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ <div class="col-6">
74
+ <resource-view-prints
75
+ :order="order"
76
+ :resource="resource" />
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </template>
81
+
82
+ <script>
83
+ import ResourceViewMixin from './resource-view.mixin';
84
+ import ResourceViewPrints from './resource_view_prints/resource-view-prints';
85
+
86
+ export default {
87
+ name: 'LancomResourceView',
88
+ components: {
89
+ ResourceViewPrints
90
+ },
91
+ mixins: [ResourceViewMixin]
92
+ };
93
+ </script>
94
+
95
+ <style lang="scss" scoped>
96
+ @import 'resource-view.scss';
97
+ </style>
@@ -0,0 +1,7 @@
1
+ .ResourceViewPrints {
2
+ &__print {
3
+ margin-bottom: 10px;
4
+ padding-left: 10px;
5
+ border-left: 4px solid #d8d8d8;
6
+ }
7
+ }
@@ -0,0 +1,42 @@
1
+ <template>
2
+ <div class="ResourceViewPrints__wrapper">
3
+ <resource-view-print
4
+ v-for="print in prints"
5
+ :key="print._id"
6
+ :order="order"
7
+ :resource="resource"
8
+ :print="print"
9
+ class="ResourceViewPrints__print" />
10
+ </div>
11
+ </template>
12
+
13
+ <script>
14
+ import ResourceViewPrint from './resource_view_print/resource-view-print';
15
+
16
+ export default {
17
+ name: 'LancomResourceViewPrints',
18
+ components: {
19
+ ResourceViewPrint
20
+ },
21
+ props: {
22
+ order: {
23
+ type: Object,
24
+ required: true
25
+ },
26
+ resource: {
27
+ type: Object,
28
+ required: true
29
+ }
30
+ },
31
+ computed: {
32
+ prints() {
33
+ const productsPrints = this.order.products.reduce((prints, product) => [...prints, ...product.prints], []);
34
+ return [...this.order.prints, ...productsPrints];
35
+ }
36
+ }
37
+ };
38
+ </script>
39
+
40
+ <style lang="scss" scoped>
41
+ @import 'resource-view-prints.scss';
42
+ </style>
@@ -0,0 +1,5 @@
1
+ .ResourceViewPrint {
2
+ &__products {
3
+ margin-top: 5px;
4
+ }
5
+ }
@@ -0,0 +1,54 @@
1
+ <template>
2
+ <div class="ResourceViewPrint__wrapper">
3
+ <div class="lc_regular12">
4
+ <div v-if="print.printType">
5
+ <b class="lc_bold">Print Type:</b> {{ print.printType.name }}
6
+ </div>
7
+ <div v-if="print.printArea">
8
+ <b class="lc_bold">Print Area:</b> {{ print.printArea.name }}
9
+ </div>
10
+ <div v-if="print.printSize">
11
+ <b class="lc_bold">Print Size:</b> {{ print.printSize.name }}
12
+ </div>
13
+ </div>
14
+ <div class="ResourceViewPrint__products">
15
+ <resource-view-print-products
16
+ :order="order"
17
+ :resource="resource"
18
+ :print="print" />
19
+ </div>
20
+ </div>
21
+ </template>
22
+
23
+ <script>
24
+ import ResourceViewPrintProducts from './resource_view_print_products/resource-view-print-products';
25
+
26
+ export default {
27
+ name: 'LancomResourceViewPrint',
28
+ components: {
29
+ ResourceViewPrintProducts
30
+ },
31
+ props: {
32
+ order: {
33
+ type: Object,
34
+ required: true
35
+ },
36
+ resource: {
37
+ type: Object,
38
+ required: true
39
+ },
40
+ print: {
41
+ type: Object,
42
+ required: true
43
+ }
44
+ },
45
+ mounted() {
46
+ console.log('');
47
+ console.log('this.print: ', this.print);
48
+ }
49
+ };
50
+ </script>
51
+
52
+ <style lang="scss" scoped>
53
+ @import 'resource-view-print.scss';
54
+ </style>
@@ -0,0 +1,45 @@
1
+ <template>
2
+ <div class="ResourceViewPrintProducts__wrapper">
3
+ <resource-view-print-product
4
+ v-for="(product, index) in products"
5
+ :key="product.guid || index"
6
+ :order="order"
7
+ :resource="resource"
8
+ :print="print"
9
+ :product="product" />
10
+ </div>
11
+ </template>
12
+
13
+ <script>
14
+ import ResourceViewPrintProduct from './resource_view_print_product/resource-view-print-product';
15
+
16
+ export default {
17
+ name: 'LancomResourceViewPrintProducts',
18
+ components: {
19
+ ResourceViewPrintProduct
20
+ },
21
+ props: {
22
+ order: {
23
+ type: Object,
24
+ required: true
25
+ },
26
+ resource: {
27
+ type: Object,
28
+ required: true
29
+ },
30
+ print: {
31
+ type: Object,
32
+ required: true
33
+ }
34
+ },
35
+ computed: {
36
+ products() {
37
+ return this.order.products;
38
+ }
39
+ }
40
+ };
41
+ </script>
42
+
43
+ <style lang="scss" scoped>
44
+ @import 'resource-view-print-products.scss';
45
+ </style>
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <div class="ResourceViewPrintProduct__wrapper">
3
+ <div
4
+ v-for="simpleProduct in simpleProducts"
5
+ :key="simpleProduct.guid"
6
+ class="lc_regular14">
7
+ {{ simpleProduct.SKU }} x {{ simpleProduct.amount }}
8
+ </div>
9
+ </div>
10
+ </template>
11
+
12
+ <script>
13
+ export default {
14
+ name: 'LancomResourceViewPrintProduct',
15
+ props: {
16
+ order: {
17
+ type: Object,
18
+ required: true
19
+ },
20
+ resource: {
21
+ type: Object,
22
+ required: true
23
+ },
24
+ print: {
25
+ type: Object,
26
+ required: true
27
+ },
28
+ product: {
29
+ type: Object,
30
+ required: true
31
+ }
32
+ },
33
+ computed: {
34
+ simpleProducts() {
35
+ const simpleProducts = this.product.simpleProducts || [];
36
+ return simpleProducts.filter(sp => sp.amount > 0);
37
+ }
38
+ }
39
+ };
40
+ </script>
41
+
42
+ <style lang="scss" scoped>
43
+ @import 'resource-view-print-product.scss';
44
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lancom/shared",
3
- "version": "0.0.423",
3
+ "version": "0.0.424",
4
4
  "description": "lancom common scripts",
5
5
  "author": "e.tokovenko <e.tokovenko@gmail.com>",
6
6
  "repository": {
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <div
3
+ :data-aos="aosFadeLeft"
4
+ class="CustomerPreferencesPage__wrapper">
5
+ <customer-preferences />
6
+ </div>
7
+ </template>
8
+
9
+ <script>
10
+ import metaInfo from '@lancom/shared/mixins/meta-info';
11
+
12
+ export default {
13
+ name: 'CustomerPreferencesPage',
14
+ components: {
15
+ CustomerPreferences: () => import('@lancom/shared/components/pages/customer/preferences/preferences')
16
+ },
17
+ mixins: [metaInfo]
18
+ };
19
+ </script>
20
+
21
+ <style lang="scss" scoped>
22
+ @import "@/assets/scss/variables";
23
+
24
+ .CustomerPreferencesPage {
25
+ &__wrapper {
26
+ margin: 100px auto;
27
+ width: 500px;
28
+ @media (max-width: $bp-extra-small-max) {
29
+ width: auto;
30
+ }
31
+ }
32
+ }
33
+ </style>
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <div class="ResourceViewPage__wrapper">
3
+ <div :class="isFullScreen ? '' : 'content-inner'">
4
+ <resource-view
5
+ :order="order"
6
+ :resource="resource"
7
+ :responsive="!isFullScreen" />
8
+ </div>
9
+ </div>
10
+ </template>
11
+
12
+ <script>
13
+ import metaInfo from '@lancom/shared/mixins/meta-info';
14
+ import api from '@lancom/shared/assets/js/api';
15
+
16
+ export default {
17
+ name: 'OrderResourceViewPage',
18
+ components: {
19
+ ResourceView: () => import('@lancom/shared/components/resource/resource_view/resource-view')
20
+ },
21
+ mixins: [metaInfo],
22
+ async asyncData({ params, error }) {
23
+ try {
24
+ const order = await api.fetchOrderByToken(params.token, { invoiceProducts: true });
25
+ const resource = order.resources.find(i => [i._id, i.guid].includes(params.resource));
26
+ if (!resource) {
27
+ return error({
28
+ statusCode: 404,
29
+ error: 'Resource not found'
30
+ });
31
+ }
32
+ return { order, resource };
33
+ } catch (e) {
34
+ const { status, data } = e?.response || {};
35
+ const statusCode = status || 500;
36
+ error({
37
+ statusCode,
38
+ error: data?.error || 'Resource not found'
39
+ });
40
+ }
41
+ },
42
+ data() {
43
+ return {
44
+ order: null,
45
+ invoice: null,
46
+ isFullScreen: this.$route.query.print
47
+ };
48
+ }
49
+ };
50
+ </script>
51
+
52
+ <style lang="scss">
53
+ </style>
package/routes/index.js CHANGED
@@ -87,6 +87,11 @@ module.exports = function(routes, resolve, config) {
87
87
  path: '/order/:token/payment',
88
88
  component: resolve('@lancom/shared/pages/order/_token/payment/index.vue'),
89
89
  chunkName: 'pages/order/payment'
90
+ }, {
91
+ name: 'order-resource',
92
+ path: '/order/:token/resource/:resource',
93
+ component: resolve('@lancom/shared/pages/order/_token/resource/_resource.vue'),
94
+ chunkName: 'pages/order/resource'
90
95
  }, {
91
96
  name: 'order-payment-invoice',
92
97
  path: '/order/:token/payment/invoice/:invoice',
@@ -142,6 +147,11 @@ module.exports = function(routes, resolve, config) {
142
147
  path: '/customer/password/:token',
143
148
  component: resolve('@lancom/shared/pages/customer/password/_token.vue'),
144
149
  chunkName: 'pages/customer/password/reset'
150
+ }, {
151
+ name: 'customer-preferences',
152
+ path: '/customer/preferences',
153
+ component: resolve('@lancom/shared/pages/customer/preferences.vue'),
154
+ chunkName: 'pages/customer/preferences'
145
155
  }, {
146
156
  name: 'checkout-cart',
147
157
  path: '/checkout/cart',
package/store/auth.js CHANGED
@@ -18,6 +18,7 @@ export const getters = {
18
18
  isGuestUser: state => !state.token,
19
19
  isAdmin: state => checkRoles(state.user, ['admin']),
20
20
  isContentEditor: state => checkRoles(state.user, ['admin', 'content-editor']),
21
+ isDesigner: state => checkRoles(state.user, ['admin', 'designer']),
21
22
  isWarehouse: state => checkRoles(state.user, ['admin', 'warehouse']),
22
23
  user: state => state.user,
23
24
  userWarehouse: state => state.user?.warehouse,
package/store/index.js CHANGED
@@ -4,9 +4,13 @@ import { getAuthToken } from '@lancom/shared/assets/js/utils/auth';
4
4
  import { getShopCountrySettings } from '@lancom/shared/assets/js/utils/shop';
5
5
  import { MESSAGES, COUNTRIES_MESSAGES } from '@/messages';
6
6
  import { SETTINGS, COUNTRIES_SETTINGS } from '@/settings';
7
+ import { getSharePromise, clearAllSharePromises } from '@lancom/shared/assets/js/utils/share-promises';
8
+
7
9
  const cookieparser = process.server ? require('cookieparser') : undefined;
8
10
  const CLOSED_NOTIFICATION = 'lancom-closed-notification-1.0';
9
11
 
12
+ const shareShopInfo = getSharePromise(60 * 1000);
13
+
10
14
  export const state = () => ({
11
15
  googleClickId: null,
12
16
  stockCountry: null,
@@ -69,9 +73,14 @@ export const actions = {
69
73
  await commit('setShop', shop);
70
74
  }
71
75
  },
72
- async nuxtServerInit({ commit }, { req }) {
73
- const shop = await api.fetchShopByUrl(process.env.HOST_NAME);
74
- const menus = await api.fetchMenus(shop._id);
76
+ async nuxtServerInit({ commit }, { req, query }) {
77
+ if (query?.cache === 'false') {
78
+ clearAllSharePromises();
79
+ await api.clearAllSharePromises();
80
+ }
81
+
82
+ const shop = await shareShopInfo.share(process.env.HOST_NAME, () => api.fetchShopByUrl(process.env.HOST_NAME));
83
+ const menus = await shareShopInfo.share(`menus_${shop._id}`, () => api.fetchMenus(shop._id));
75
84
  commit('setMenus', menus);
76
85
  commit('setShop', shop);
77
86
  // if (req.headers.cookie) {
package/store/product.js CHANGED
@@ -77,7 +77,7 @@ export const getters = {
77
77
  usedBigSizeSimpleProductsQuantity: (state, { usedSimpleProducts }) => filterBigSize(usedSimpleProducts).reduce((sum, { amount }) => sum + amount, 0),
78
78
  defaultSimpleProduct: ({ productDetails, editableColor }) => {
79
79
  return editableColor && (
80
- productDetails.simpleProducts.find(({ color, size }) => color._id === editableColor._id && size.alias === 'small')
80
+ productDetails.simpleProducts.find(({ color, size }) => color._id === editableColor._id && size?.alias === 'small')
81
81
  || productDetails.simpleProducts.find(({ color }) => color._id === editableColor._id)
82
82
  );
83
83
  },