@oxyshop/admin 1.3.35 → 1.3.39

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/lib/index.js CHANGED
@@ -25,6 +25,9 @@ import 'semantic-ui-css/components/visibility';
25
25
  import 'semantic-ui-css/components/visit';
26
26
  import 'jquery.dirtyforms/jquery.dirtyforms';
27
27
  import 'chart.js/dist/Chart.min';
28
+ import Chart$1 from 'chart.js';
29
+ import { axios } from '@oxyshop/shop/lib/plugins/Axios';
30
+ import axios$1 from 'axios';
28
31
  import 'semantic-ui-css/semantic.css';
29
32
  import '@oxyshop/admin/scss/main.scss';
30
33
  import '@oxyshop/admin/lib/style.css';
@@ -1884,11 +1887,13 @@ $.fn.extend({
1884
1887
  },
1885
1888
  });
1886
1889
 
1887
- $.fn.extend({
1888
- feedCategorySelect() {
1889
- const matchedElements = this;
1890
+ class FeedCategorySelect {
1891
+ constructor(selector) {
1892
+ this.selectSelector = selector;
1893
+ }
1890
1894
 
1891
- Object.values(matchedElements).forEach((selectElement) => {
1895
+ init() {
1896
+ document.querySelectorAll(this.selectSelector).forEach((selectElement) => {
1892
1897
  const sourceCode = selectElement.getAttribute('data-source-code');
1893
1898
 
1894
1899
  $(selectElement).dropdown({
@@ -1899,8 +1904,154 @@ $.fn.extend({
1899
1904
  minCharacters: 2,
1900
1905
  });
1901
1906
  });
1902
- },
1903
- });
1907
+ }
1908
+ }
1909
+
1910
+ class ProductVariantPricingGraph {
1911
+ constructor(containerSelector) {
1912
+ this.containerSelector = containerSelector;
1913
+ }
1914
+
1915
+ init() {
1916
+ document.querySelectorAll(this.containerSelector).forEach((componentContainer) => {
1917
+ const graphsData = JSON.parse(componentContainer.getAttribute('data-graph'));
1918
+
1919
+ for (const currencyGraphIndex in graphsData) {
1920
+ const currencyGraph = graphsData[currencyGraphIndex];
1921
+
1922
+ const currencyCodeHeader = document.createElement('h4');
1923
+ currencyCodeHeader.classList.add('ui', 'header');
1924
+ currencyCodeHeader.textContent = currencyGraph.currency;
1925
+
1926
+ const graphCanvas = document.createElement('canvas');
1927
+ graphCanvas.width = 400;
1928
+ graphCanvas.height = 200;
1929
+
1930
+ const graphContainer = document.createElement('div');
1931
+ graphContainer.append(currencyCodeHeader);
1932
+ graphContainer.append(graphCanvas);
1933
+
1934
+ componentContainer.append(graphContainer);
1935
+
1936
+ const graphConfig = this.getGraphConfig(currencyGraph.graph);
1937
+ new Chart$1(graphCanvas.getContext('2d'), graphConfig);
1938
+ }
1939
+ });
1940
+ }
1941
+
1942
+ /** @private */
1943
+ getGraphConfig(rawGraphData) {
1944
+ return {
1945
+ type: 'line',
1946
+ data: {
1947
+ datasets: rawGraphData.datasets.map((rawDataset) => {
1948
+ return {
1949
+ label: rawDataset.label,
1950
+ data: rawDataset.data,
1951
+ borderColor: this.getRandomRgbColor(),
1952
+ fill: false,
1953
+ steppedLine: true,
1954
+ pointRadius: 8,
1955
+ pointHoverRadius: 10,
1956
+ }
1957
+ }),
1958
+ },
1959
+ options: {
1960
+ maintainAspectRatio: true,
1961
+ responsive: true,
1962
+ scales: {
1963
+ xAxes: [
1964
+ {
1965
+ type: 'linear',
1966
+ position: 'bottom',
1967
+ scaleLabel: {
1968
+ display: true,
1969
+ labelString: rawGraphData.xAxisLabel,
1970
+ },
1971
+ },
1972
+ ],
1973
+ yAxes: [
1974
+ {
1975
+ scaleLabel: {
1976
+ display: true,
1977
+ labelString: rawGraphData.yAxisLabel,
1978
+ },
1979
+ },
1980
+ ],
1981
+ },
1982
+ },
1983
+ }
1984
+ }
1985
+
1986
+ /** @private */
1987
+ getRandomRgbColor() {
1988
+ const r = Math.floor(Math.random() * 255);
1989
+ const g = Math.floor(Math.random() * 255);
1990
+ const b = Math.floor(Math.random() * 255);
1991
+ return `rgb(${r},${g},${b})`
1992
+ }
1993
+ }
1994
+
1995
+ class CustomerGroupClientAssigner {
1996
+ constructor(selector, searchSelector = '.ng-client-search-select', submitSelector = '.ng-client-assign-submit') {
1997
+ this.selector = selector;
1998
+ this.searchSelector = searchSelector;
1999
+ this.submitSelector = submitSelector;
2000
+
2001
+ this.containerElement = null;
2002
+ this.searchElement = null;
2003
+ this.submitElement = null;
2004
+ }
2005
+
2006
+ init() {
2007
+ this.containerElement = document.querySelector(this.selector);
2008
+ if (null === this.containerElement) {
2009
+ return
2010
+ }
2011
+
2012
+ const submitApiEndpoint = this.containerElement.getAttribute('data-assign-customers-url');
2013
+ const redirectAfterUrl = this.containerElement.getAttribute('data-redirect-after-url');
2014
+
2015
+ this.searchElement = this.containerElement.querySelector(this.searchSelector);
2016
+ const searchApiEndpoint = this.searchElement.getAttribute('data-customer-search-url');
2017
+
2018
+ $(this.searchElement).dropdown({
2019
+ apiSettings: {
2020
+ url: `${searchApiEndpoint}/{query}`,
2021
+ cache: false,
2022
+ },
2023
+
2024
+ minCharacters: 2,
2025
+ });
2026
+
2027
+ this.submitElement = this.containerElement.querySelector(this.submitSelector);
2028
+ this.submitElement.addEventListener('click', () => {
2029
+ const selectedCustomersIds = this.getSelectValues(this.searchElement);
2030
+ this.submitCustomers(submitApiEndpoint, selectedCustomersIds, redirectAfterUrl);
2031
+ });
2032
+ }
2033
+
2034
+ getSelectValues(selectElement) {
2035
+ const selected = selectElement.querySelectorAll('option:checked');
2036
+ return Array.from(selected).map((optionElement) => optionElement.value)
2037
+ }
2038
+
2039
+ submitCustomers(apiEndpoint, selectedIds, redirectTo) {
2040
+ this.submitElement.disabled = true;
2041
+
2042
+ axios
2043
+ .post(apiEndpoint, { customers: selectedIds })
2044
+ .then(() => {
2045
+ window.location = redirectTo;
2046
+ })
2047
+ .catch((error) => {
2048
+ console.error(error);
2049
+ })
2050
+ .finally(() => {
2051
+ this.submitElement.disabled = false;
2052
+ });
2053
+ }
2054
+ }
1904
2055
 
1905
2056
  class TooltipHelpers {
1906
2057
  static init() {
@@ -1990,6 +2141,322 @@ class AdminSidebarScroller {
1990
2141
  }
1991
2142
  }
1992
2143
 
2144
+ class CustomerGroupingRuleConfiguration {
2145
+ /**
2146
+ * @param {string} groupingRuleSelectSelector
2147
+ * @param {string} formGroupSelector
2148
+ */
2149
+ constructor(groupingRuleSelectSelector, formGroupSelector) {
2150
+ this.groupingRuleSelectSelector = groupingRuleSelectSelector;
2151
+ this.formGroupSelector = formGroupSelector;
2152
+ }
2153
+
2154
+ init() {
2155
+ // Event listener on dynamic elements
2156
+ $(document).on('change', this.groupingRuleSelectSelector, (selectEvent) => {
2157
+ this.toggleRuleConfigurationSection(selectEvent.target);
2158
+ });
2159
+
2160
+ // Show all sections with selected value
2161
+ document.querySelectorAll(this.groupingRuleSelectSelector).forEach((selectElement) => {
2162
+ this.toggleRuleConfigurationSection(selectElement);
2163
+ });
2164
+ }
2165
+
2166
+ toggleRuleConfigurationSection(selectElement) {
2167
+ const selectedRuleCode = selectElement.value;
2168
+ const configurationGroup = selectElement.parentElement.parentElement;
2169
+
2170
+ configurationGroup.querySelectorAll(this.formGroupSelector).forEach((formGroup) => {
2171
+ const groupRuleCode = formGroup.getAttribute('data-grouping-rule-code');
2172
+ const toggleFunction = groupRuleCode === selectedRuleCode ? this.showElement : this.hideElement;
2173
+
2174
+ toggleFunction(formGroup);
2175
+ });
2176
+ }
2177
+
2178
+ /** @private */
2179
+ hideElement(element) {
2180
+ element.classList.add('d-none');
2181
+ }
2182
+
2183
+ /** @private */
2184
+ showElement(element) {
2185
+ element.classList.remove('d-none');
2186
+ }
2187
+ }
2188
+
2189
+ class ProductVariantPricingSimulation {
2190
+ constructor(selector) {
2191
+ this.selector = selector;
2192
+ }
2193
+
2194
+ init() {
2195
+ document.querySelectorAll(this.selector).forEach((element) => {
2196
+ const productVariantId = element.getAttribute('data-product-variant-id');
2197
+ const channels = JSON.parse(element.getAttribute('data-channels'));
2198
+ const searchApiEndpoint = element.getAttribute('data-customer-search-url');
2199
+ const pricingApiEndpoint = element.getAttribute('data-pricing-url');
2200
+
2201
+ this.initElement(element, productVariantId, channels, searchApiEndpoint, pricingApiEndpoint);
2202
+ });
2203
+ }
2204
+
2205
+ initElement(parentElement, productVariantId, channels, searchApiEndpoint, pricingApiEndpoint) {
2206
+ parentElement.innerHTML = '';
2207
+
2208
+ const simulationData = {
2209
+ productVariantId: productVariantId,
2210
+ channelId: null,
2211
+ currencyId: null,
2212
+ customerId: null,
2213
+ };
2214
+
2215
+ const submitButton = document.createElement('button');
2216
+ const currencySelect = document.createElement('select');
2217
+
2218
+ const onChannelSelect = (event) => {
2219
+ const selectedChannelId = parseInt(event.target.value);
2220
+ simulationData.channelId = selectedChannelId;
2221
+ simulationData.currencyId = null;
2222
+ submitButton.disabled = this.isSubmitButtonDisabled(simulationData);
2223
+
2224
+ const selectedChannel = channels.find((channel) => {
2225
+ return channel.id === selectedChannelId
2226
+ });
2227
+
2228
+ currencySelect.innerHTML = '';
2229
+ this.addSelectOptions(currencySelect, selectedChannel.currencies, 'Select currency');
2230
+ };
2231
+
2232
+ const onCurrencySelect = (event) => {
2233
+ simulationData.currencyId = event.target.value;
2234
+ submitButton.disabled = this.isSubmitButtonDisabled(simulationData);
2235
+ };
2236
+
2237
+ const onCustomerSelect = (event) => {
2238
+ simulationData.customerId = event.target.value;
2239
+ submitButton.disabled = this.isSubmitButtonDisabled(simulationData);
2240
+ };
2241
+
2242
+ const graphWrapper = this.createGraphWrapper();
2243
+
2244
+ const onSubmit = () => {
2245
+ const pricingUrl = `${pricingApiEndpoint}?${this.serializeObjectToQuery(simulationData)}`;
2246
+
2247
+ axios$1
2248
+ .get(pricingUrl)
2249
+ .then((response) => {
2250
+ const chartData = response.data;
2251
+ graphWrapper.setAttribute('data-debug-chart', JSON.stringify(chartData)); // For e2e tests
2252
+ this.renderGraph(graphWrapper, chartData);
2253
+ })
2254
+ .catch((error) => {
2255
+ console.error(error);
2256
+ graphWrapper.innerHTML = 'Pricing simulation error';
2257
+ });
2258
+ };
2259
+
2260
+ const wrapper = document.createElement('div');
2261
+ wrapper.setAttribute('style', 'border: solid 1px #ddd; background-color: #fafafa;');
2262
+
2263
+ const filters = this.createFilters(
2264
+ channels,
2265
+ currencySelect,
2266
+ onChannelSelect,
2267
+ onCurrencySelect,
2268
+ onCustomerSelect,
2269
+ onSubmit,
2270
+ submitButton,
2271
+ searchApiEndpoint
2272
+ );
2273
+ filters.setAttribute(
2274
+ 'style',
2275
+ 'display: grid; grid-template-columns: 1fr 1fr 1fr 60px; grid-gap: 10px; ' +
2276
+ 'padding: 15px; border-bottom: solid 1px #ddd;'
2277
+ );
2278
+
2279
+ const graphFiller = this.createGraphFiller();
2280
+ graphWrapper.append(graphFiller);
2281
+
2282
+ wrapper.append(filters, graphWrapper);
2283
+ parentElement.append(wrapper);
2284
+ }
2285
+
2286
+ createFilters(
2287
+ channels,
2288
+ currencySelect,
2289
+ onChannelSelect,
2290
+ onCurrencySelect,
2291
+ onCustomerSelect,
2292
+ onSubmit,
2293
+ submitButton,
2294
+ searchApiEndpoint
2295
+ ) {
2296
+ const filtersWrapper = document.createElement('div');
2297
+
2298
+ const channelSelect = document.createElement('select');
2299
+ this.addSelectOptions(channelSelect, channels, 'Select channel');
2300
+ channelSelect.addEventListener('change', onChannelSelect);
2301
+
2302
+ currencySelect.addEventListener('change', onCurrencySelect);
2303
+
2304
+ const customerSelect = document.createElement('select');
2305
+ customerSelect.addEventListener('change', onCustomerSelect);
2306
+
2307
+ // this is delayed to avoid racing conditions
2308
+ setTimeout(() => this.hookClientSearchOnSelect(customerSelect, searchApiEndpoint), 600);
2309
+
2310
+ submitButton.disabled = true;
2311
+ submitButton.setAttribute('class', 'ui icon primary button');
2312
+ submitButton.type = 'button';
2313
+ submitButton.addEventListener('click', onSubmit);
2314
+
2315
+ const playIcon = document.createElement('i');
2316
+ playIcon.setAttribute('class', 'icon play');
2317
+ submitButton.append(playIcon);
2318
+
2319
+ filtersWrapper.append(channelSelect, currencySelect, customerSelect, submitButton);
2320
+
2321
+ return filtersWrapper
2322
+ }
2323
+
2324
+ createGraphWrapper() {
2325
+ const wrapper = document.createElement('div');
2326
+ wrapper.setAttribute('style', 'padding: 15px;');
2327
+
2328
+ return wrapper
2329
+ }
2330
+
2331
+ createGraphFiller() {
2332
+ const filler = document.createElement('div');
2333
+ filler.setAttribute(
2334
+ 'style',
2335
+ 'border-radius: 7px; background-color: #eee; height: 350px; display: flex; ' +
2336
+ 'justify-content: space-around; align-items: center; font-size: 4em;'
2337
+ );
2338
+
2339
+ const chartIcon = document.createElement('i');
2340
+ chartIcon.setAttribute('class', 'icon chart line');
2341
+ filler.append(chartIcon);
2342
+
2343
+ return filler
2344
+ }
2345
+
2346
+ addSelectOptions(select, choices, placeholder = null) {
2347
+ if (placeholder !== null) {
2348
+ const placeholderOption = document.createElement('option');
2349
+ placeholderOption.innerHTML = placeholder;
2350
+ placeholderOption.disabled = true;
2351
+ placeholderOption.selected = true;
2352
+ select.append(placeholderOption);
2353
+ }
2354
+
2355
+ for (const property in choices) {
2356
+ const choice = choices[property];
2357
+
2358
+ const channelOption = document.createElement('option');
2359
+ channelOption.innerHTML = choice.name;
2360
+ channelOption.setAttribute('value', choice.id);
2361
+ select.append(channelOption);
2362
+ }
2363
+
2364
+ return select
2365
+ }
2366
+
2367
+ hookClientSearchOnSelect(selectElement, searchApiEndpoint) {
2368
+ selectElement.setAttribute('class', `${selectElement.getAttribute('class')} search dropdown selection`);
2369
+
2370
+ $(selectElement).dropdown({
2371
+ apiSettings: {
2372
+ url: `${searchApiEndpoint}/{query}`,
2373
+ cache: false,
2374
+ },
2375
+
2376
+ minCharacters: 2,
2377
+ });
2378
+ }
2379
+
2380
+ isSubmitButtonDisabled(simulationData) {
2381
+ return (
2382
+ null === simulationData.productVariantId ||
2383
+ null === simulationData.channelId ||
2384
+ null === simulationData.currencyId ||
2385
+ null === simulationData.customerId
2386
+ )
2387
+ }
2388
+
2389
+ serializeObjectToQuery(object) {
2390
+ const query = [];
2391
+
2392
+ for (const part in object) {
2393
+ if (Object.prototype.hasOwnProperty.call(object, part)) {
2394
+ query.push(`${encodeURIComponent(part)}=${encodeURIComponent(object[part])}`);
2395
+ }
2396
+ }
2397
+
2398
+ return query.join('&')
2399
+ }
2400
+
2401
+ renderGraph(graphWrapper, graphData) {
2402
+ graphWrapper.innerHTML = '';
2403
+
2404
+ const graphCanvas = document.createElement('canvas');
2405
+ graphCanvas.width = 600;
2406
+ graphCanvas.height = 200;
2407
+
2408
+ graphWrapper.append(graphCanvas);
2409
+
2410
+ const graphConfig = this.getGraphConfig(graphData);
2411
+ console.log('graphConfig', graphConfig);
2412
+ new Chart$1(graphCanvas.getContext('2d'), graphConfig);
2413
+ }
2414
+
2415
+ /** @private */
2416
+ getGraphConfig(rawGraphData) {
2417
+ return {
2418
+ type: 'line',
2419
+ data: {
2420
+ datasets: rawGraphData.datasets.map((rawDataset) => {
2421
+ return {
2422
+ label: rawDataset.label,
2423
+ data: rawDataset.data,
2424
+ borderColor: '#0000ff',
2425
+ fill: false,
2426
+ steppedLine: true,
2427
+ pointRadius: 8,
2428
+ pointHoverRadius: 10,
2429
+ }
2430
+ }),
2431
+ },
2432
+ options: {
2433
+ maintainAspectRatio: true,
2434
+ responsive: true,
2435
+ scales: {
2436
+ xAxes: [
2437
+ {
2438
+ type: 'linear',
2439
+ position: 'bottom',
2440
+ scaleLabel: {
2441
+ display: true,
2442
+ labelString: rawGraphData.xAxisLabel,
2443
+ },
2444
+ },
2445
+ ],
2446
+ yAxes: [
2447
+ {
2448
+ scaleLabel: {
2449
+ display: true,
2450
+ labelString: rawGraphData.yAxisLabel,
2451
+ },
2452
+ },
2453
+ ],
2454
+ },
2455
+ },
2456
+ }
2457
+ }
2458
+ }
2459
+
1993
2460
  /**
1994
2461
  * CKEditor config
1995
2462
  */
@@ -2288,7 +2755,6 @@ vendorDescriptionElements.forEach((element) => {
2288
2755
  // Components initializations
2289
2756
  $(document).ready(() => {
2290
2757
  $('.ng-taxon-attr-dropdown').taxonAttributes();
2291
- $('.ng-feed-category-select').feedCategorySelect();
2292
2758
 
2293
2759
  $(document).previewUploadedImage('#sylius_payment_method_images');
2294
2760
  $(document).previewUploadedImage('#sylius_shipping_method_images');
@@ -2299,8 +2765,29 @@ $(document).ready(() => {
2299
2765
  */
2300
2766
  TooltipHelpers.init();
2301
2767
 
2768
+ const customerGroupingRuleConfiguration = new CustomerGroupingRuleConfiguration(
2769
+ 'select.ng-grouping-rule-select',
2770
+ '.ng-grouping-rule-configuration'
2771
+ );
2772
+ customerGroupingRuleConfiguration.init();
2773
+
2774
+ const feedCategorySelect = new FeedCategorySelect('.ng-feed-category-select');
2775
+ feedCategorySelect.init();
2776
+
2302
2777
  // Admin sidebar scroller
2303
2778
  const adminSidebarElement = document.getElementById('sidebar');
2304
2779
  const adminSidebarScroller = new AdminSidebarScroller(adminSidebarElement, 'a.item');
2305
2780
  adminSidebarScroller.scrollToActiveLink();
2781
+
2782
+ const productVariantPricingGraph = new ProductVariantPricingGraph('.ng-product-variant-pricing-graph');
2783
+ productVariantPricingGraph.init();
2784
+
2785
+ const productVariantPricingSimulation = new ProductVariantPricingSimulation(
2786
+ '.ng-product-variant-pricing-simulation'
2787
+ );
2788
+ productVariantPricingSimulation.init();
2789
+
2790
+ // Client search select
2791
+ const clientSearchSelect = new CustomerGroupClientAssigner('.ng-customer-group-client-assigner');
2792
+ clientSearchSelect.init();
2306
2793
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyshop/admin",
3
- "version": "1.3.35",
3
+ "version": "1.3.39",
4
4
  "author": "oXy Online s.r.o. <info@oxyshop.cz>",
5
5
  "main": "lib/index.js",
6
6
  "sass": "scss/main.scss",
package/scss/main.scss CHANGED
@@ -1,3 +1,7 @@
1
1
  @import 'Form/main';
2
2
  @import 'Logo/main';
3
3
  @import 'PickupPoint/main';
4
+
5
+ .d-none {
6
+ display: none !important;
7
+ }
@@ -0,0 +1,62 @@
1
+ import { axios } from '@oxyshop/shop/lib/plugins/Axios'
2
+
3
+ export default class CustomerGroupClientAssigner {
4
+ constructor(selector, searchSelector = '.ng-client-search-select', submitSelector = '.ng-client-assign-submit') {
5
+ this.selector = selector
6
+ this.searchSelector = searchSelector
7
+ this.submitSelector = submitSelector
8
+
9
+ this.containerElement = null
10
+ this.searchElement = null
11
+ this.submitElement = null
12
+ }
13
+
14
+ init() {
15
+ this.containerElement = document.querySelector(this.selector)
16
+ if (null === this.containerElement) {
17
+ return
18
+ }
19
+
20
+ const submitApiEndpoint = this.containerElement.getAttribute('data-assign-customers-url')
21
+ const redirectAfterUrl = this.containerElement.getAttribute('data-redirect-after-url')
22
+
23
+ this.searchElement = this.containerElement.querySelector(this.searchSelector)
24
+ const searchApiEndpoint = this.searchElement.getAttribute('data-customer-search-url')
25
+
26
+ $(this.searchElement).dropdown({
27
+ apiSettings: {
28
+ url: `${searchApiEndpoint}/{query}`,
29
+ cache: false,
30
+ },
31
+
32
+ minCharacters: 2,
33
+ })
34
+
35
+ this.submitElement = this.containerElement.querySelector(this.submitSelector)
36
+ this.submitElement.addEventListener('click', () => {
37
+ const selectedCustomersIds = this.getSelectValues(this.searchElement)
38
+ this.submitCustomers(submitApiEndpoint, selectedCustomersIds, redirectAfterUrl)
39
+ })
40
+ }
41
+
42
+ getSelectValues(selectElement) {
43
+ const selected = selectElement.querySelectorAll('option:checked')
44
+ return Array.from(selected).map((optionElement) => optionElement.value)
45
+ }
46
+
47
+ submitCustomers(apiEndpoint, selectedIds, redirectTo) {
48
+ this.submitElement.disabled = true
49
+
50
+ axios
51
+ .post(apiEndpoint, { customers: selectedIds })
52
+ .then(() => {
53
+ window.location = redirectTo
54
+ })
55
+ .catch((error) => {
56
+ console.error(error)
57
+ })
58
+ .finally(() => {
59
+ this.submitElement.disabled = false
60
+ })
61
+ }
62
+ }
@@ -0,0 +1,44 @@
1
+ export default class CustomerGroupingRuleConfiguration {
2
+ /**
3
+ * @param {string} groupingRuleSelectSelector
4
+ * @param {string} formGroupSelector
5
+ */
6
+ constructor(groupingRuleSelectSelector, formGroupSelector) {
7
+ this.groupingRuleSelectSelector = groupingRuleSelectSelector
8
+ this.formGroupSelector = formGroupSelector
9
+ }
10
+
11
+ init() {
12
+ // Event listener on dynamic elements
13
+ $(document).on('change', this.groupingRuleSelectSelector, (selectEvent) => {
14
+ this.toggleRuleConfigurationSection(selectEvent.target)
15
+ })
16
+
17
+ // Show all sections with selected value
18
+ document.querySelectorAll(this.groupingRuleSelectSelector).forEach((selectElement) => {
19
+ this.toggleRuleConfigurationSection(selectElement)
20
+ })
21
+ }
22
+
23
+ toggleRuleConfigurationSection(selectElement) {
24
+ const selectedRuleCode = selectElement.value
25
+ const configurationGroup = selectElement.parentElement.parentElement
26
+
27
+ configurationGroup.querySelectorAll(this.formGroupSelector).forEach((formGroup) => {
28
+ const groupRuleCode = formGroup.getAttribute('data-grouping-rule-code')
29
+ const toggleFunction = groupRuleCode === selectedRuleCode ? this.showElement : this.hideElement
30
+
31
+ toggleFunction(formGroup)
32
+ })
33
+ }
34
+
35
+ /** @private */
36
+ hideElement(element) {
37
+ element.classList.add('d-none')
38
+ }
39
+
40
+ /** @private */
41
+ showElement(element) {
42
+ element.classList.remove('d-none')
43
+ }
44
+ }
@@ -1,8 +1,10 @@
1
- $.fn.extend({
2
- feedCategorySelect() {
3
- const matchedElements = this
1
+ export default class FeedCategorySelect {
2
+ constructor(selector) {
3
+ this.selectSelector = selector
4
+ }
4
5
 
5
- Object.values(matchedElements).forEach((selectElement) => {
6
+ init() {
7
+ document.querySelectorAll(this.selectSelector).forEach((selectElement) => {
6
8
  const sourceCode = selectElement.getAttribute('data-source-code')
7
9
 
8
10
  $(selectElement).dropdown({
@@ -13,5 +15,5 @@ $.fn.extend({
13
15
  minCharacters: 2,
14
16
  })
15
17
  })
16
- },
17
- })
18
+ }
19
+ }
@@ -0,0 +1,86 @@
1
+ import Chart from 'chart.js'
2
+
3
+ export default class ProductVariantPricingGraph {
4
+ constructor(containerSelector) {
5
+ this.containerSelector = containerSelector
6
+ }
7
+
8
+ init() {
9
+ document.querySelectorAll(this.containerSelector).forEach((componentContainer) => {
10
+ const graphsData = JSON.parse(componentContainer.getAttribute('data-graph'))
11
+
12
+ for (const currencyGraphIndex in graphsData) {
13
+ const currencyGraph = graphsData[currencyGraphIndex]
14
+
15
+ const currencyCodeHeader = document.createElement('h4')
16
+ currencyCodeHeader.classList.add('ui', 'header')
17
+ currencyCodeHeader.textContent = currencyGraph.currency
18
+
19
+ const graphCanvas = document.createElement('canvas')
20
+ graphCanvas.width = 400
21
+ graphCanvas.height = 200
22
+
23
+ const graphContainer = document.createElement('div')
24
+ graphContainer.append(currencyCodeHeader)
25
+ graphContainer.append(graphCanvas)
26
+
27
+ componentContainer.append(graphContainer)
28
+
29
+ const graphConfig = this.getGraphConfig(currencyGraph.graph)
30
+ new Chart(graphCanvas.getContext('2d'), graphConfig)
31
+ }
32
+ })
33
+ }
34
+
35
+ /** @private */
36
+ getGraphConfig(rawGraphData) {
37
+ return {
38
+ type: 'line',
39
+ data: {
40
+ datasets: rawGraphData.datasets.map((rawDataset) => {
41
+ return {
42
+ label: rawDataset.label,
43
+ data: rawDataset.data,
44
+ borderColor: this.getRandomRgbColor(),
45
+ fill: false,
46
+ steppedLine: true,
47
+ pointRadius: 8,
48
+ pointHoverRadius: 10,
49
+ }
50
+ }),
51
+ },
52
+ options: {
53
+ maintainAspectRatio: true,
54
+ responsive: true,
55
+ scales: {
56
+ xAxes: [
57
+ {
58
+ type: 'linear',
59
+ position: 'bottom',
60
+ scaleLabel: {
61
+ display: true,
62
+ labelString: rawGraphData.xAxisLabel,
63
+ },
64
+ },
65
+ ],
66
+ yAxes: [
67
+ {
68
+ scaleLabel: {
69
+ display: true,
70
+ labelString: rawGraphData.yAxisLabel,
71
+ },
72
+ },
73
+ ],
74
+ },
75
+ },
76
+ }
77
+ }
78
+
79
+ /** @private */
80
+ getRandomRgbColor() {
81
+ const r = Math.floor(Math.random() * 255)
82
+ const g = Math.floor(Math.random() * 255)
83
+ const b = Math.floor(Math.random() * 255)
84
+ return `rgb(${r},${g},${b})`
85
+ }
86
+ }
@@ -0,0 +1,273 @@
1
+ import axios from 'axios'
2
+ import Chart from 'chart.js'
3
+
4
+ export default class ProductVariantPricingSimulation {
5
+ constructor(selector) {
6
+ this.selector = selector
7
+ }
8
+
9
+ init() {
10
+ document.querySelectorAll(this.selector).forEach((element) => {
11
+ const productVariantId = element.getAttribute('data-product-variant-id')
12
+ const channels = JSON.parse(element.getAttribute('data-channels'))
13
+ const searchApiEndpoint = element.getAttribute('data-customer-search-url')
14
+ const pricingApiEndpoint = element.getAttribute('data-pricing-url')
15
+
16
+ this.initElement(element, productVariantId, channels, searchApiEndpoint, pricingApiEndpoint)
17
+ })
18
+ }
19
+
20
+ initElement(parentElement, productVariantId, channels, searchApiEndpoint, pricingApiEndpoint) {
21
+ parentElement.innerHTML = ''
22
+
23
+ const simulationData = {
24
+ productVariantId: productVariantId,
25
+ channelId: null,
26
+ currencyId: null,
27
+ customerId: null,
28
+ }
29
+
30
+ const submitButton = document.createElement('button')
31
+ const currencySelect = document.createElement('select')
32
+
33
+ const onChannelSelect = (event) => {
34
+ const selectedChannelId = parseInt(event.target.value)
35
+ simulationData.channelId = selectedChannelId
36
+ simulationData.currencyId = null
37
+ submitButton.disabled = this.isSubmitButtonDisabled(simulationData)
38
+
39
+ const selectedChannel = channels.find((channel) => {
40
+ return channel.id === selectedChannelId
41
+ })
42
+
43
+ currencySelect.innerHTML = ''
44
+ this.addSelectOptions(currencySelect, selectedChannel.currencies, 'Select currency')
45
+ }
46
+
47
+ const onCurrencySelect = (event) => {
48
+ simulationData.currencyId = event.target.value
49
+ submitButton.disabled = this.isSubmitButtonDisabled(simulationData)
50
+ }
51
+
52
+ const onCustomerSelect = (event) => {
53
+ simulationData.customerId = event.target.value
54
+ submitButton.disabled = this.isSubmitButtonDisabled(simulationData)
55
+ }
56
+
57
+ const graphWrapper = this.createGraphWrapper()
58
+
59
+ const onSubmit = () => {
60
+ const pricingUrl = `${pricingApiEndpoint}?${this.serializeObjectToQuery(simulationData)}`
61
+
62
+ axios
63
+ .get(pricingUrl)
64
+ .then((response) => {
65
+ const chartData = response.data
66
+ graphWrapper.setAttribute('data-debug-chart', JSON.stringify(chartData)) // For e2e tests
67
+ this.renderGraph(graphWrapper, chartData)
68
+ })
69
+ .catch((error) => {
70
+ console.error(error)
71
+ graphWrapper.innerHTML = 'Pricing simulation error'
72
+ })
73
+ }
74
+
75
+ const wrapper = document.createElement('div')
76
+ wrapper.setAttribute('style', 'border: solid 1px #ddd; background-color: #fafafa;')
77
+
78
+ const filters = this.createFilters(
79
+ channels,
80
+ currencySelect,
81
+ onChannelSelect,
82
+ onCurrencySelect,
83
+ onCustomerSelect,
84
+ onSubmit,
85
+ submitButton,
86
+ searchApiEndpoint
87
+ )
88
+ filters.setAttribute(
89
+ 'style',
90
+ 'display: grid; grid-template-columns: 1fr 1fr 1fr 60px; grid-gap: 10px; ' +
91
+ 'padding: 15px; border-bottom: solid 1px #ddd;'
92
+ )
93
+
94
+ const graphFiller = this.createGraphFiller()
95
+ graphWrapper.append(graphFiller)
96
+
97
+ wrapper.append(filters, graphWrapper)
98
+ parentElement.append(wrapper)
99
+ }
100
+
101
+ createFilters(
102
+ channels,
103
+ currencySelect,
104
+ onChannelSelect,
105
+ onCurrencySelect,
106
+ onCustomerSelect,
107
+ onSubmit,
108
+ submitButton,
109
+ searchApiEndpoint
110
+ ) {
111
+ const filtersWrapper = document.createElement('div')
112
+
113
+ const channelSelect = document.createElement('select')
114
+ this.addSelectOptions(channelSelect, channels, 'Select channel')
115
+ channelSelect.addEventListener('change', onChannelSelect)
116
+
117
+ currencySelect.addEventListener('change', onCurrencySelect)
118
+
119
+ const customerSelect = document.createElement('select')
120
+ customerSelect.addEventListener('change', onCustomerSelect)
121
+
122
+ // this is delayed to avoid racing conditions
123
+ setTimeout(() => this.hookClientSearchOnSelect(customerSelect, searchApiEndpoint), 600)
124
+
125
+ submitButton.disabled = true
126
+ submitButton.setAttribute('class', 'ui icon primary button')
127
+ submitButton.type = 'button'
128
+ submitButton.addEventListener('click', onSubmit)
129
+
130
+ const playIcon = document.createElement('i')
131
+ playIcon.setAttribute('class', 'icon play')
132
+ submitButton.append(playIcon)
133
+
134
+ filtersWrapper.append(channelSelect, currencySelect, customerSelect, submitButton)
135
+
136
+ return filtersWrapper
137
+ }
138
+
139
+ createGraphWrapper() {
140
+ const wrapper = document.createElement('div')
141
+ wrapper.setAttribute('style', 'padding: 15px;')
142
+
143
+ return wrapper
144
+ }
145
+
146
+ createGraphFiller() {
147
+ const filler = document.createElement('div')
148
+ filler.setAttribute(
149
+ 'style',
150
+ 'border-radius: 7px; background-color: #eee; height: 350px; display: flex; ' +
151
+ 'justify-content: space-around; align-items: center; font-size: 4em;'
152
+ )
153
+
154
+ const chartIcon = document.createElement('i')
155
+ chartIcon.setAttribute('class', 'icon chart line')
156
+ filler.append(chartIcon)
157
+
158
+ return filler
159
+ }
160
+
161
+ addSelectOptions(select, choices, placeholder = null) {
162
+ if (placeholder !== null) {
163
+ const placeholderOption = document.createElement('option')
164
+ placeholderOption.innerHTML = placeholder
165
+ placeholderOption.disabled = true
166
+ placeholderOption.selected = true
167
+ select.append(placeholderOption)
168
+ }
169
+
170
+ for (const property in choices) {
171
+ const choice = choices[property]
172
+
173
+ const channelOption = document.createElement('option')
174
+ channelOption.innerHTML = choice.name
175
+ channelOption.setAttribute('value', choice.id)
176
+ select.append(channelOption)
177
+ }
178
+
179
+ return select
180
+ }
181
+
182
+ hookClientSearchOnSelect(selectElement, searchApiEndpoint) {
183
+ selectElement.setAttribute('class', `${selectElement.getAttribute('class')} search dropdown selection`)
184
+
185
+ $(selectElement).dropdown({
186
+ apiSettings: {
187
+ url: `${searchApiEndpoint}/{query}`,
188
+ cache: false,
189
+ },
190
+
191
+ minCharacters: 2,
192
+ })
193
+ }
194
+
195
+ isSubmitButtonDisabled(simulationData) {
196
+ return (
197
+ null === simulationData.productVariantId ||
198
+ null === simulationData.channelId ||
199
+ null === simulationData.currencyId ||
200
+ null === simulationData.customerId
201
+ )
202
+ }
203
+
204
+ serializeObjectToQuery(object) {
205
+ const query = []
206
+
207
+ for (const part in object) {
208
+ if (Object.prototype.hasOwnProperty.call(object, part)) {
209
+ query.push(`${encodeURIComponent(part)}=${encodeURIComponent(object[part])}`)
210
+ }
211
+ }
212
+
213
+ return query.join('&')
214
+ }
215
+
216
+ renderGraph(graphWrapper, graphData) {
217
+ graphWrapper.innerHTML = ''
218
+
219
+ const graphCanvas = document.createElement('canvas')
220
+ graphCanvas.width = 600
221
+ graphCanvas.height = 200
222
+
223
+ graphWrapper.append(graphCanvas)
224
+
225
+ const graphConfig = this.getGraphConfig(graphData)
226
+ console.log('graphConfig', graphConfig)
227
+ new Chart(graphCanvas.getContext('2d'), graphConfig)
228
+ }
229
+
230
+ /** @private */
231
+ getGraphConfig(rawGraphData) {
232
+ return {
233
+ type: 'line',
234
+ data: {
235
+ datasets: rawGraphData.datasets.map((rawDataset) => {
236
+ return {
237
+ label: rawDataset.label,
238
+ data: rawDataset.data,
239
+ borderColor: '#0000ff',
240
+ fill: false,
241
+ steppedLine: true,
242
+ pointRadius: 8,
243
+ pointHoverRadius: 10,
244
+ }
245
+ }),
246
+ },
247
+ options: {
248
+ maintainAspectRatio: true,
249
+ responsive: true,
250
+ scales: {
251
+ xAxes: [
252
+ {
253
+ type: 'linear',
254
+ position: 'bottom',
255
+ scaleLabel: {
256
+ display: true,
257
+ labelString: rawGraphData.xAxisLabel,
258
+ },
259
+ },
260
+ ],
261
+ yAxes: [
262
+ {
263
+ scaleLabel: {
264
+ display: true,
265
+ labelString: rawGraphData.yAxisLabel,
266
+ },
267
+ },
268
+ ],
269
+ },
270
+ },
271
+ }
272
+ }
273
+ }
package/src/index.js CHANGED
@@ -6,9 +6,13 @@ import 'sylius-bundle/AdminBundle/Resources/private/js/app'
6
6
 
7
7
  // Scripts - components
8
8
  import './components/taxonAttributes'
9
- import './components/feedCategorySelect'
9
+ import FeedCategorySelect from './components/feedCategorySelect'
10
+ import ProductVariantPricingGraph from './components/productVariantPricingGraph'
11
+ import CustomerGroupClientAssigner from './components/customerGroupClientAssigner'
10
12
  import TooltipHelpers from './components/tooltipHelpers'
11
13
  import AdminSidebarScroller from './components/adminSidebarScroller'
14
+ import CustomerGroupingRuleConfiguration from './components/customerGroupingRuleConfiguration'
15
+ import ProductVariantPricingSimulation from './components/productVariantPricingSimulation'
12
16
 
13
17
  // Scripts - plugin
14
18
  import './plugins/ckeditor/index'
@@ -32,7 +36,6 @@ import '@oxyshop/admin/images/admin-logo.svg'
32
36
  // Components initializations
33
37
  $(document).ready(() => {
34
38
  $('.ng-taxon-attr-dropdown').taxonAttributes()
35
- $('.ng-feed-category-select').feedCategorySelect()
36
39
 
37
40
  $(document).previewUploadedImage('#sylius_payment_method_images')
38
41
  $(document).previewUploadedImage('#sylius_shipping_method_images')
@@ -43,8 +46,29 @@ $(document).ready(() => {
43
46
  */
44
47
  TooltipHelpers.init()
45
48
 
49
+ const customerGroupingRuleConfiguration = new CustomerGroupingRuleConfiguration(
50
+ 'select.ng-grouping-rule-select',
51
+ '.ng-grouping-rule-configuration'
52
+ )
53
+ customerGroupingRuleConfiguration.init()
54
+
55
+ const feedCategorySelect = new FeedCategorySelect('.ng-feed-category-select')
56
+ feedCategorySelect.init()
57
+
46
58
  // Admin sidebar scroller
47
59
  const adminSidebarElement = document.getElementById('sidebar')
48
60
  const adminSidebarScroller = new AdminSidebarScroller(adminSidebarElement, 'a.item')
49
61
  adminSidebarScroller.scrollToActiveLink()
62
+
63
+ const productVariantPricingGraph = new ProductVariantPricingGraph('.ng-product-variant-pricing-graph')
64
+ productVariantPricingGraph.init()
65
+
66
+ const productVariantPricingSimulation = new ProductVariantPricingSimulation(
67
+ '.ng-product-variant-pricing-simulation'
68
+ )
69
+ productVariantPricingSimulation.init()
70
+
71
+ // Client search select
72
+ const clientSearchSelect = new CustomerGroupClientAssigner('.ng-customer-group-client-assigner')
73
+ clientSearchSelect.init()
50
74
  })