@fleetbase/storefront-engine 0.3.8 → 0.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ {{#if (eq @order.type "storefront")}}
2
+ <Button @type="magic" @icon="square-plus" @text="Add Product from Storefront" @onClick={{this.promptProductSelection}} />
3
+ {{/if}}
@@ -0,0 +1,58 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { action } from '@ember/object';
4
+ import { isArray } from '@ember/array';
5
+ import { inject as service } from '@ember/service';
6
+
7
+ export default class AddProductAsEntityButtonComponent extends Component {
8
+ @service modalsManager;
9
+ @service fetch;
10
+ @service notifications;
11
+ @tracked order;
12
+ @tracked controller;
13
+
14
+ constructor(owner, { order, controller }) {
15
+ super(...arguments);
16
+ this.order = order;
17
+ this.controller = controller;
18
+ }
19
+
20
+ @action promptProductSelection() {
21
+ this.modalsManager.show('modals/select-product', {
22
+ title: 'Select Product to Add to Order',
23
+ modalClass: 'modal-lg',
24
+ acceptButtonText: 'Add Products',
25
+ acceptButtonDisabled: true,
26
+ selectedProducts: [],
27
+ selectedStorefront: null,
28
+ confirm: (modal) => {
29
+ modal.startLoading();
30
+
31
+ const selectedStorefront = modal.getOption('selectedStorefront');
32
+ const selectedProducts = modal.getOption('selectedProducts', []);
33
+ const products = selectedProducts.map((product) => product.id);
34
+
35
+ return this.fetch
36
+ .post('products/create-entities', { products }, { namespace: 'storefront/int/v1', normalizeToEmberData: true, normalizeModelType: 'entity' })
37
+ .then((entities) => {
38
+ this.controller.addEntities(entities);
39
+ if (isArray(this.order.meta)) {
40
+ this.order.meta.pushObjects([
41
+ {
42
+ key: 'storefront',
43
+ value: selectedStorefront.name
44
+ },
45
+ {
46
+ key: 'storefront_id',
47
+ value: selectedStorefront.public_id
48
+ }
49
+ ]);
50
+ }
51
+ })
52
+ .catch((error) => {
53
+ this.notifications.serverError(error);
54
+ });
55
+ },
56
+ });
57
+ }
58
+ }
@@ -7,6 +7,7 @@ import CustomerPanelDetailsComponent from './customer-panel/details';
7
7
  import CustomerPanelOrdersComponent from './customer-panel/orders';
8
8
  import contextComponentCallback from '@fleetbase/ember-core/utils/context-component-callback';
9
9
  import applyContextComponentArguments from '@fleetbase/ember-core/utils/apply-context-component-arguments';
10
+
10
11
  export default class CustomerPanelComponent extends Component {
11
12
  /**
12
13
  * Service for fetching data.
@@ -7,7 +7,14 @@ s<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confir
7
7
  {{/unless}}
8
8
  </div>
9
9
  <div>
10
- <Button @type="magic" @text={{t "storefront.component.modals.manage-addons.title"}} @icon="plus" @iconPrefix="fas" @onClick={{perform this.createAddonCategory}} @disabled={{not this.createAddonCategory.isIdle}} />
10
+ <Button
11
+ @type="magic"
12
+ @text={{t "storefront.component.modals.manage-addons.title"}}
13
+ @icon="plus"
14
+ @iconPrefix="fas"
15
+ @onClick={{perform this.createAddonCategory}}
16
+ @disabled={{not this.createAddonCategory.isIdle}}
17
+ />
11
18
  </div>
12
19
  </div>
13
20
 
@@ -17,10 +24,21 @@ s<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confir
17
24
  <div class="flex items-center rounded-md shadow-sm px-3 py-2 font-semibold bg-gray-200 dark:bg-gray-800 dark:text-gray-100 mb-2">
18
25
  <div class="flex-1 flex items-center">
19
26
  <FaIcon @icon="pencil" class="mr-1 dark:text-gray-100" />
20
- <Input @type="text" @value={{category.name}} {{on "blur" (perform this.saveAddonCategory category)}} class="w-full px-2 m-0 border-none bg-transparent dark:text-gray-100" />
27
+ <Input
28
+ @type="text"
29
+ @value={{category.name}}
30
+ {{on "blur" (perform this.saveAddonCategory category)}}
31
+ class="w-full px-2 m-0 border-none bg-transparent dark:text-gray-100"
32
+ />
21
33
  </div>
22
34
  <div class="flex items-center">
23
- <Button @text={{t "storefront.component.modals.manage-addons.new-addon"}} @icon="plus" @iconPrefix="fas" @onClick={{fn this.createNewAddon category}} class="mr-3" />
35
+ <Button
36
+ @text={{t "storefront.component.modals.manage-addons.new-addon"}}
37
+ @icon="plus"
38
+ @iconPrefix="fas"
39
+ @onClick={{fn this.createNewAddon category}}
40
+ class="mr-3"
41
+ />
24
42
  <a href="javascript:;" {{on "click" (perform this.deleteAddonCategory index)}} class="destroy-action opacity-50 hover:opacity-100 text-sm">
25
43
  {{t "storefront.component.modals.manage-addons.delete"}}
26
44
  </a>
@@ -0,0 +1,87 @@
1
+ <Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
2
+ <div class="modal-body-container">
3
+ <div class="grid grid-cols-3 gap-2">
4
+ <InputGroup @name="Select Store">
5
+ <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
6
+ <PowerSelect
7
+ @options={{this.stores}}
8
+ @selected={{this.selectedStorefront}}
9
+ @onChange={{this.onStorefrontSelect}}
10
+ @registerAPI={{this.setStorefrontSelectApi}}
11
+ @optionValue="name"
12
+ @placeholder="Select Storefront"
13
+ @triggerClass="form-select form-input"
14
+ @disabled={{not this.stores}}
15
+ as |storefront|
16
+ >
17
+ {{storefront.name}}
18
+ </PowerSelect>
19
+ </div>
20
+ </InputGroup>
21
+
22
+ {{#if (and this.selectedStorefront this.productCategories)}}
23
+ <InputGroup @name="Select Product Category">
24
+ <div class="fleetbase-model-select fleetbase-power-select ember-model-select">
25
+ <PowerSelect
26
+ @options={{this.productCategories}}
27
+ @selected={{this.selectedCategory}}
28
+ @onChange={{this.onSelectProductCategory}}
29
+ @disabled={{not this.productCategories}}
30
+ @placeholder="Filter by Product Category"
31
+ @triggerClass="form-select form-input"
32
+ as |category|
33
+ >
34
+ {{category.name}}
35
+ </PowerSelect>
36
+ </div>
37
+ </InputGroup>
38
+ {{/if}}
39
+
40
+ {{#if this.selectedProducts}}
41
+ <div class="py-2 flex items-center dark:text-white text-gray-900">
42
+ Selected
43
+ {{pluralize this.selectedProducts.length "Product"}}
44
+ </div>
45
+ {{/if}}
46
+ </div>
47
+ <div class="min-h-4r">
48
+ {{#if this.selectedStorefront}}
49
+ {{#if this.fetchProductsForStorefront.isRunning}}
50
+ <Spinner @loadingMessage="Loading products..." />
51
+ {{else}}
52
+ <div class="grid grid-cols-3 lg:grid-cols-4 gap-2">
53
+ {{#each this.products as |product|}}
54
+ {{#let (in-array product this.selectedProducts) as |isSelected|}}
55
+ <div
56
+ class="border bg-white dark:bg-gray-900 dark:text-gray-100 text-center rounded-md px-2 py-3
57
+ {{if isSelected 'border-blue-500 dark:border-blue-500 outline-offset-2 outline-blue-400 outline-dashed' 'border-gray-200 dark:border-gray-700'}}"
58
+ >
59
+ <div class="flex flex-col items-center justify-center overflow-hidden">
60
+ <div class="mb-3 flex items-center justify-center">
61
+ <img src={{product.primary_image_url}} alt={{product.name}} class="w-24 h-24" />
62
+ </div>
63
+ <h4 class="font-semibold mb-1">{{product.name}}</h4>
64
+ <p class="text-sm truncate">{{product.description}}</p>
65
+ <p class="mb-2 text-sm text-green-400">{{format-currency product.price product.currency}}</p>
66
+ <div class="flex items-center justify-evenly space-x-4">
67
+ <Button
68
+ @type={{if isSelected "danger" "default"}}
69
+ @icon="circle-plus"
70
+ @text={{if isSelected "Remove Product" "Add Product"}}
71
+ @onClick={{fn this.toggleProductSelection product}}
72
+ />
73
+ </div>
74
+ </div>
75
+ </div>
76
+ {{/let}}
77
+ {{else}}
78
+ <div>
79
+ <h3 class="dark:text-gray-100 text-opacity-75 text-sm">No products</h3>
80
+ </div>
81
+ {{/each}}
82
+ </div>
83
+ {{/if}}
84
+ {{/if}}
85
+ </div>
86
+ </div>
87
+ </Modal::Default>
@@ -0,0 +1,88 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { action } from '@ember/object';
4
+ import { inject as service } from '@ember/service';
5
+ import { task } from 'ember-concurrency';
6
+ import { pluralize } from 'ember-inflector';
7
+
8
+ export default class ModalsSelectProductComponent extends Component {
9
+ @service fetch;
10
+ @service notifications;
11
+ @service modalsManager;
12
+ @tracked stores = [];
13
+ @tracked productCategories = [];
14
+ @tracked products = [];
15
+ @tracked selectedProducts = [];
16
+ @tracked selectedStorefront;
17
+ @tracked selectedCategory;
18
+ @tracked storefrontSelectAPI;
19
+
20
+ constructor() {
21
+ super(...arguments);
22
+ this.fetchStorefronts.perform();
23
+ }
24
+
25
+ @action toggleProductSelection(product) {
26
+ if (this.selectedProducts.includes(product)) {
27
+ this.selectedProducts.removeObject(product);
28
+ } else {
29
+ this.selectedProducts.pushObject(product);
30
+ }
31
+
32
+ this.updateSelectedProducts();
33
+ }
34
+
35
+ updateSelectedProducts() {
36
+ this.modalsManager.setOption('selectedProducts', this.selectedProducts);
37
+ this.modalsManager.setOption('acceptButtonDisabled', this.selectedProducts.length === 0);
38
+ this.modalsManager.setOption('acceptButtonText', this.selectedProducts.length ? `Add ${pluralize(this.selectedProducts.length, 'Product')}` : 'Add Products');
39
+ this.modalsManager.setOption('selectedStorefront', this.selectedStorefront);
40
+ }
41
+
42
+ @action setStorefrontSelectApi(storefrontSelectAPI) {
43
+ this.storefrontSelectAPI = storefrontSelectAPI;
44
+ }
45
+
46
+ @action onStorefrontSelect(storefront) {
47
+ this.selectedStorefront = storefront;
48
+ this.selectedCategory = null;
49
+ this.selectedProducts = [];
50
+ this.updateSelectedProducts();
51
+ this.fetchCategoriesForStorefront.perform(storefront);
52
+ this.fetchProductsForStorefront.perform(storefront);
53
+ }
54
+
55
+ @action onSelectProductCategory(category) {
56
+ this.selectedCategory = category;
57
+ this.fetchProductsForStorefront.perform(this.selectedStorefront, { category_slug: category.slug });
58
+ }
59
+
60
+ @task *fetchStorefronts(queryParams = {}) {
61
+ try {
62
+ this.stores = yield this.fetch.get('stores', queryParams, { namespace: 'storefront/int/v1', normalizeToEmberData: true });
63
+ } catch (error) {
64
+ this.notifications.serverError(error);
65
+ return;
66
+ }
67
+
68
+ if (this.stores && this.storefrontSelectAPI) {
69
+ this.storefrontSelectAPI.actions.select(this.stores[0]);
70
+ }
71
+ }
72
+
73
+ @task *fetchProductsForStorefront(storefront, queryParams = {}) {
74
+ try {
75
+ this.products = yield this.fetch.get('products', { store_uuid: storefront.id, ...queryParams }, { namespace: 'storefront/int/v1', normalizeToEmberData: true });
76
+ } catch (error) {
77
+ this.notifications.serverError(error);
78
+ }
79
+ }
80
+
81
+ @task *fetchCategoriesForStorefront(storefront, queryParams = {}) {
82
+ try {
83
+ this.productCategories = yield this.fetch.get('categories', { for: 'storefront_product', owner_uuid: storefront.id, limit: -1, ...queryParams }, { normalizeToEmberData: true });
84
+ } catch (error) {
85
+ this.notifications.serverError(error);
86
+ }
87
+ }
88
+ }
@@ -1,16 +1,14 @@
1
1
  import Component from '@glimmer/component';
2
-
2
+ import { tracked } from '@glimmer/tracking';
3
3
  import { action } from '@ember/object';
4
4
  import applyContextComponentArguments from '@fleetbase/ember-core/utils/apply-context-component-arguments';
5
5
  import contextComponentCallback from '@fleetbase/ember-core/utils/context-component-callback';
6
- import { tracked } from '@glimmer/tracking';
7
6
 
8
7
  export default class OrderPanelComponent extends Component {
9
8
  @tracked context = null;
10
9
 
11
10
  constructor() {
12
11
  super(...arguments);
13
-
14
12
  applyContextComponentArguments(this);
15
13
  }
16
14
 
@@ -31,8 +31,8 @@
31
31
  <tr class="h-12">
32
32
  <td><a href="javascript:;" {{on "click" (fn this.viewOrder order)}}>{{order.public_id}}</a></td>
33
33
  <td>{{format-currency order.meta.total order.meta.currency}}</td>
34
- <td>{{n-a order.customer_name}}</td>
35
- <td>{{n-a order.driver_name}}</td>
34
+ <td>{{n-a order.customer.name}}</td>
35
+ <td>{{n-a order.driver_assigned.name}}</td>
36
36
  <td>{{order.createdAgo}}</td>
37
37
  <td>
38
38
  <Badge @status={{order.status}} />
@@ -4,6 +4,7 @@ import { inject as service } from '@ember/service';
4
4
  import { inject as controller } from '@ember/controller';
5
5
  import { action, computed, get } from '@ember/object';
6
6
  import { later } from '@ember/runloop';
7
+
7
8
  export default class WidgetOrdersComponent extends Component {
8
9
  @service store;
9
10
  @service storefront;
@@ -66,7 +66,10 @@ export default class ProductsIndexCategoryNewController extends BaseController {
66
66
 
67
67
  this.loader.removeLoader(loader);
68
68
  this.notifications.success(this.intl.t('storefront.products.index.new.new-product-created-success'));
69
- yield this.transitionToRoute('products.index.category', category.slug);
69
+
70
+ try {
71
+ yield this.transitionToRoute('products.index.category', category.slug);
72
+ } catch (error) {}
70
73
  this.reset();
71
74
  }
72
75
 
package/addon/engine.js CHANGED
@@ -5,6 +5,7 @@ import config from './config/environment';
5
5
  import services from '@fleetbase/ember-core/exports/services';
6
6
  import StorefrontKeyMetricsWidget from './components/widget/storefront-key-metrics';
7
7
  import StorefrontOrderSummaryComponent from './components/storefront-order-summary';
8
+ import AddProductAsEntityButtonComponent from './components/add-product-as-entity-button';
8
9
 
9
10
  const { modulePrefix } = config;
10
11
  const externalRoutes = ['console', 'extensions'];
@@ -35,6 +36,7 @@ export default class StorefrontEngine extends Engine {
35
36
 
36
37
  // register component to views
37
38
  universe.registerRenderableComponent('@fleetbase/fleetops-engine', 'fleet-ops:template:operations:orders:view', StorefrontOrderSummaryComponent);
39
+ universe.registerRenderableComponent('@fleetbase/fleetops-engine', 'fleet-ops:template:operations:orders:new:entities-input', AddProductAsEntityButtonComponent);
38
40
 
39
41
  // register widgets
40
42
  universe.registerDefaultDashboardWidgets([KeyMetricsWidgetDefinition]);
@@ -22,8 +22,6 @@
22
22
  @paginationMeta={{@model.meta}}
23
23
  @page={{this.page}}
24
24
  @onPageChange={{fn (mut this.page)}}
25
- @tfootVerticalOffset="53"
26
- @tfootVerticalOffsetElements=".next-view-section-subheader"
27
25
  />
28
26
  </Layout::Section::Body>
29
27
 
@@ -12,7 +12,5 @@
12
12
  @paginationMeta={{@model.meta}}
13
13
  @page={{this.page}}
14
14
  @onPageChange={{fn (mut this.page)}}
15
- @tfootVerticalOffset="53"
16
- @tfootVerticalOffsetElements=".next-view-section-subheader"
17
15
  />
18
16
  </Layout::Section::Body>
@@ -12,7 +12,5 @@
12
12
  @paginationMeta={{@model.meta}}
13
13
  @page={{this.page}}
14
14
  @onPageChange={{fn (mut this.page)}}
15
- @tfootVerticalOffset="53"
16
- @tfootVerticalOffsetElements=".next-view-section-subheader"
17
15
  />
18
16
  </Layout::Section::Body>
@@ -33,8 +33,6 @@
33
33
  @paginationMeta={{@model.meta}}
34
34
  @page={{this.page}}
35
35
  @onPageChange={{fn (mut this.page)}}
36
- @tfootVerticalOffset="53"
37
- @tfootVerticalOffsetElements=".next-view-section-subheader"
38
36
  />
39
37
  </Layout::Section::Body>
40
38
 
@@ -1,6 +1,19 @@
1
1
  <Overlay @position="right" @noBackdrop={{true}} @isResizable={{true}} @width="570px" @fullHeight={{true}} @containerClass="border-l border-transparent dark:border-gray-700">
2
- <Overlay::Header @title={{this.overlayTitle}} @titleClass="truncate" @titleWrapperClass="w-3/4" @headerLeftClass="w-70pc" @headerLeftInnerClass="w-full flex-1" @onPressCancel={{this.transitionBack}}>
3
- <Button @icon={{this.overlayActionButtonIcon}} @type="primary" @text={{this.overlayActionButtonTitle}} @onClick={{perform this.saveProduct}} @isLoading={{not this.saveProduct.isIdle}} />
2
+ <Overlay::Header
3
+ @title={{this.overlayTitle}}
4
+ @titleClass="truncate"
5
+ @titleWrapperClass="w-3/4"
6
+ @headerLeftClass="w-70pc"
7
+ @headerLeftInnerClass="w-full flex-1"
8
+ @onPressCancel={{this.transitionBack}}
9
+ >
10
+ <Button
11
+ @icon={{this.overlayActionButtonIcon}}
12
+ @type="primary"
13
+ @text={{this.overlayActionButtonTitle}}
14
+ @onClick={{perform this.saveProduct}}
15
+ @isLoading={{not this.saveProduct.isIdle}}
16
+ />
4
17
  </Overlay::Header>
5
18
 
6
19
  <Overlay::Body @increaseInnerBodyHeightBy="0" @wrapperClass="new-order-overlay-body px-4 space-y-4 pt-4">
@@ -9,21 +22,48 @@
9
22
  <Textarea @value={{this.product.description}} class="form-input w-full" placeholder="Enter a description of your product...." rows={{4}} />
10
23
  </InputGroup>
11
24
  <InputGroup @name="Product Tags">
12
- <TagInput class="form-input" @placeholder="Add tags" @allowSpacesInTags={{true}} @tags={{this.product.tags}} @addTag={{this.addTag}} @removeTagAtIndex={{this.removeTag}} as |tag|>
25
+ <TagInput
26
+ class="form-input"
27
+ @placeholder="Add tags"
28
+ @allowSpacesInTags={{true}}
29
+ @tags={{this.product.tags}}
30
+ @addTag={{this.addTag}}
31
+ @removeTagAtIndex={{this.removeTag}}
32
+ as |tag|
33
+ >
13
34
  {{tag}}
14
35
  </TagInput>
15
36
  </InputGroup>
16
37
  <InputGroup @name="Product SKU" @value={{this.product.sku}} @helpText="Enter product SKU if applicable" />
17
38
  <div class="grid grid-cols-2 gap-2">
18
39
  <InputGroup @name="Price" @helpText="Enter a price users will pay to purchase this product">
19
- <MoneyInput class="w-full" @currency={{if this.product.currency this.product.currency this.activeStore.currency}} @value={{this.product.price}} @canSelectCurrency={{false}} @onCurrencyChange={{fn (mut this.product.currency)}} />
40
+ <MoneyInput
41
+ class="w-full"
42
+ @currency={{if this.product.currency this.product.currency this.activeStore.currency}}
43
+ @value={{this.product.price}}
44
+ @canSelectCurrency={{false}}
45
+ @onCurrencyChange={{fn (mut this.product.currency)}}
46
+ />
20
47
  </InputGroup>
21
48
  <InputGroup @name="Sale Price" @helpText="Optionally add a sale price for the product if the product is put on sale">
22
- <MoneyInput class="w-full" @currency={{if this.product.currency this.product.currency this.activeStore.currency}} @value={{this.product.sale_price}} @canSelectCurrency={{false}} @onCurrencyChange={{fn (mut this.product.currency)}} />
49
+ <MoneyInput
50
+ class="w-full"
51
+ @currency={{if this.product.currency this.product.currency this.activeStore.currency}}
52
+ @value={{this.product.sale_price}}
53
+ @canSelectCurrency={{false}}
54
+ @onCurrencyChange={{fn (mut this.product.currency)}}
55
+ />
23
56
  </InputGroup>
24
57
  </div>
25
58
 
26
- <ContentPanel @title="Metadata" @open={{this.product.meta_array.length}} @actionButtons={{this.metadataButtons}} @pad={{true}} @panelBodyWrapperClass="px-0 py-4" @panelBodyClass="bg-white dark:bg-gray-800">
59
+ <ContentPanel
60
+ @title="Metadata"
61
+ @open={{this.product.meta_array.length}}
62
+ @actionButtons={{this.metadataButtons}}
63
+ @pad={{true}}
64
+ @panelBodyWrapperClass="px-0 py-4"
65
+ @panelBodyClass="bg-white dark:bg-gray-800"
66
+ >
27
67
  {{#each this.product.meta_array as |metaField index|}}
28
68
  <div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
29
69
  <div class="input-group">
@@ -103,7 +143,12 @@
103
143
  <Input @type="text" @value={{variantOption.description}} class="form-input w-full" placeholder="Option Description" />
104
144
  </div>
105
145
  <div class="col-span-2">
106
- <MoneyInput class="w-full" @currency={{if this.product.currency this.product.currency this.activeStore.currency}} @canSelectCurrency={{false}} @value={{variantOption.additional_cost}} />
146
+ <MoneyInput
147
+ class="w-full"
148
+ @currency={{if this.product.currency this.product.currency this.activeStore.currency}}
149
+ @canSelectCurrency={{false}}
150
+ @value={{variantOption.additional_cost}}
151
+ />
107
152
  </div>
108
153
  <div class="flex items-center justify-center text-center text-sm">
109
154
  <a href="javascript:;" {{on "click" (fn this.removeVariantOption variant index)}}>Remove</a>
@@ -169,7 +214,9 @@
169
214
  <ContentPanel @title="Images & Videos" @open={{this.product.files.length}} @pad={{false}} @panelBodyWrapperClass="px-0 py-4" @panelBodyClass="bg-white dark:bg-gray-800">
170
215
  <div class="px-6 space-y-4">
171
216
  {{#if this.isUploading}}
172
- <div class="min-h-56 dropzone w-full rounded-lg px-4 py-8 min-h bg-gray-50 dark:bg-gray-900 bg-opacity-25 text-gray-900 dark:text-white text-center flex flex-col items-center justify-center border-2 border-dashed border-gray-200 dark:border-indigo-500">
217
+ <div
218
+ class="min-h-56 dropzone w-full rounded-lg px-4 py-8 min-h bg-gray-50 dark:bg-gray-900 bg-opacity-25 text-gray-900 dark:text-white text-center flex flex-col items-center justify-center border-2 border-dashed border-gray-200 dark:border-indigo-500"
219
+ >
173
220
  <div class="flex items-center justify-center py-5">
174
221
  <Spinner class="text-sm dar:text-gray-100" @loadingMessage={{t "component.dropzone.uploading"}} />
175
222
  </div>
@@ -228,7 +275,14 @@
228
275
  {{#each this.product.files as |file|}}
229
276
  <FileRecord @file={{file}} @fileIconClass={{if (eq this.product.primary_image_uuid file.id) "border-blue-400"}} @onDelete={{this.removeFile}}>
230
277
  <div class="flex items-center justify-evenly">
231
- <Button @icon="magic" @text="Make Primary" @size="xs" @textClass="text-xs truncate" @onClick={{fn this.makePrimaryFile file}} @disabled={{eq this.product.primary_image_uuid file.id}} />
278
+ <Button
279
+ @icon="magic"
280
+ @text="Make Primary"
281
+ @size="xs"
282
+ @textClass="text-xs truncate"
283
+ @onClick={{fn this.makePrimaryFile file}}
284
+ @disabled={{eq this.product.primary_image_uuid file.id}}
285
+ />
232
286
  </div>
233
287
  </FileRecord>
234
288
  {{/each}}
@@ -18,8 +18,17 @@
18
18
  {{#each this.model as |gateway|}}
19
19
  <ContentPanel @title={{gateway.name}} @open={{true}} @pad={{true}}>
20
20
  <InputGroup @name={{t "storefront.settings.gateways.gateway-name"}} @value={{gateway.name}} @helpText={{t "storefront.settings.gateways.helpText"}} />
21
- <InputGroup @name={{t "storefront.settings.gateways.gateway-code"}} @value={{gateway.code}} @disabled={{true}} @helpText={{t "storefront.settings.gateways.gateway-code-help-text"}} />
22
- <InputGroup @name={{t "storefront.settings.gateways.callback-url"}} @value={{gateway.callback_url}} @helpText={{t "storefront.settings.gateways.callback-url-help-text"}} />
21
+ <InputGroup
22
+ @name={{t "storefront.settings.gateways.gateway-code"}}
23
+ @value={{gateway.code}}
24
+ @disabled={{true}}
25
+ @helpText={{t "storefront.settings.gateways.gateway-code-help-text"}}
26
+ />
27
+ <InputGroup
28
+ @name={{t "storefront.settings.gateways.callback-url"}}
29
+ @value={{gateway.callback_url}}
30
+ @helpText={{t "storefront.settings.gateways.callback-url-help-text"}}
31
+ />
23
32
  <InputGroup @name={{t "storefront.settings.gateways.return-url"}} @value={{gateway.return_url}} @helpText={{t "storefront.settings.gateways.return-url-help-text"}} />
24
33
  <div class="input-group">
25
34
  <Checkbox @value={{gateway.sandbox}} @label={{t "storefront.settings.gateways.this-is-a-sandbox-gateway"}} @onToggle={{fn (mut gateway.sandbox)}} />
@@ -43,4 +43,5 @@
43
43
  {{/each}}
44
44
  </div>
45
45
  </form>
46
+ <Spacer @height="300px" />
46
47
  </SettingsContainer>
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/storefront-engine/components/add-product-as-entity-button';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/storefront-engine/components/modals/select-product';
package/composer.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetbase/storefront-api",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "Headless Commerce & Marketplace Extension for Fleetbase",
5
5
  "keywords": [
6
6
  "fleetbase-extension",
@@ -22,8 +22,8 @@
22
22
  ],
23
23
  "require": {
24
24
  "php": "^8.0",
25
- "fleetbase/core-api": "^1.4.23",
26
- "fleetbase/fleetops-api": "^0.4.28",
25
+ "fleetbase/core-api": "^1.4.26",
26
+ "fleetbase/fleetops-api": "^0.5.0",
27
27
  "geocoder-php/google-maps-places-provider": "^1.4",
28
28
  "laravel-notification-channels/apn": "^5.0",
29
29
  "laravel-notification-channels/fcm": "^4.1",
package/extension.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "Storefront",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "Headless Commerce & Marketplace Extension for Fleetbase",
5
5
  "repository": "https://github.com/fleetbase/storefront",
6
6
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/storefront-engine",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "Headless Commerce & Marketplace Extension for Fleetbase",
5
5
  "fleetbase": {
6
6
  "route": "storefront",
@@ -43,8 +43,8 @@
43
43
  "publish:github": "npm config set '@fleetbase:registry' https://npm.pkg.github.com/ && npm publish"
44
44
  },
45
45
  "dependencies": {
46
- "@fleetbase/ember-core": "^0.2.9",
47
- "@fleetbase/ember-ui": "^0.2.14",
46
+ "@fleetbase/ember-core": "^0.2.11",
47
+ "@fleetbase/ember-ui": "^0.2.17",
48
48
  "@fleetbase/fleetops-data": "^0.1.15",
49
49
  "@babel/core": "^7.23.2",
50
50
  "@fortawesome/ember-fontawesome": "^0.4.1",
@@ -2,6 +2,7 @@
2
2
 
3
3
  namespace Fleetbase\Storefront\Http\Controllers;
4
4
 
5
+ use Fleetbase\FleetOps\Http\Resources\v1\Entity as EntityResource;
5
6
  use Fleetbase\FleetOps\Support\Utils;
6
7
  use Fleetbase\Models\Category;
7
8
  use Fleetbase\Models\File;
@@ -165,4 +166,34 @@ class ProductController extends StorefrontController
165
166
 
166
167
  return response()->json($products);
167
168
  }
169
+
170
+ /**
171
+ * Retrieves a list of product IDs from the request, finds the corresponding Product models, converts each to an Entity, and returns them as a JSON response.
172
+ *
173
+ * This function handles a request that includes an array of product UUIDs. It fetches the corresponding Product models from the database,
174
+ * converts each Product to an Entity using the Product model's toEntity method, and collects these entities. The function finally returns
175
+ * these entities as a JSON response, which can be useful for front-end applications or other services that need structured product data.
176
+ *
177
+ * @param Request $request the request object, expected to contain an array of product UUIDs under the 'products' key
178
+ *
179
+ * @return \Illuminate\Http\JsonResponse Returns a JSON response that contains an array of Entity objects, each representing a product.
180
+ * Each Entity object includes all relevant product details formatted and structured as specified in the Product model's toEntity method.
181
+ *
182
+ * @example
183
+ * // Example usage:
184
+ * POST /storefront/int/v1/products/create-entities
185
+ * Body: { "products": ["uuid1", "uuid2"] }
186
+ */
187
+ public function createEntities(Request $request)
188
+ {
189
+ $productIds = $request->array('products');
190
+ $products = Product::whereIn('uuid', $productIds)->get();
191
+ $entities = [];
192
+
193
+ foreach ($products as $product) {
194
+ $entities[] = $product->createAsEntity();
195
+ }
196
+
197
+ return EntityResource::collection($entities);
198
+ }
168
199
  }
@@ -3,6 +3,7 @@
3
3
  namespace Fleetbase\Storefront\Models;
4
4
 
5
5
  use Fleetbase\Casts\Json;
6
+ use Fleetbase\FleetOps\Models\Entity;
6
7
  use Fleetbase\FleetOps\Support\Utils;
7
8
  use Fleetbase\Models\Category;
8
9
  use Fleetbase\Models\File;
@@ -430,4 +431,73 @@ class Product extends StorefrontModel
430
431
 
431
432
  return $results;
432
433
  }
434
+
435
+ /**
436
+ * Converts a Product model instance to an Entity object, which represents a more detailed and structured form of the product data.
437
+ *
438
+ * This function utilizes the properties of the Product model along with any additional attributes provided to construct a new Entity object.
439
+ * The Entity object contains detailed information about the product, including identifiers, names, pricing information, and meta attributes.
440
+ * Meta attributes can be dynamically added to extend the data structure with additional custom information.
441
+ *
442
+ * @param array $additionalAttributes Optional. Additional attributes that can be merged into the product's meta information.
443
+ * This array can include any custom data under the 'meta' key, which is merged with the default meta attributes.
444
+ * Default is an empty array.
445
+ *
446
+ * @return Entity Returns a new Entity object populated with product information and any additional meta attributes.
447
+ * The Entity object is structured with a set of predefined keys (e.g., company_uuid, photo_uuid) and can be customized with additional meta attributes.
448
+ *
449
+ * @example
450
+ * // Example usage:
451
+ * $product = new Product();
452
+ * $entity = $product->toEntity(['meta' => ['custom_attribute' => 'value']]);
453
+ */
454
+ public function toEntity(array $additionalAttributes = []): Entity
455
+ {
456
+ $meta = data_get($additionalAttributes, 'meta', []);
457
+
458
+ return new Entity([
459
+ 'company_uuid' => session('company'),
460
+ 'photo_uuid' => $this->primary_image_uuid,
461
+ 'internal_id' => $this->public_id,
462
+ 'name' => $this->name,
463
+ 'description' => $this->description,
464
+ 'currency' => $this->currency,
465
+ 'sku' => $this->sku,
466
+ 'price' => $this->price,
467
+ 'sale_price' => $this->sale_price,
468
+ 'type' => 'storefront-product',
469
+ ...$additionalAttributes,
470
+ 'meta' => [
471
+ 'product_id' => $this->public_id,
472
+ 'image_url' => $this->primary_image_url,
473
+ ...$meta,
474
+ ],
475
+ ]);
476
+ }
477
+
478
+ /**
479
+ * Creates a new Entity from the Product model instance and saves it to the database.
480
+ *
481
+ * This function first converts the Product model to an Entity object using the toEntity method. It then saves this Entity object to the
482
+ * database, ensuring that all product details are persisted. This is particularly useful when the Product model needs to be
483
+ * represented and stored as an Entity for operations that require a more complex data structure or additional business logic.
484
+ *
485
+ * @param array $additionalAttributes Optional. Additional attributes that can be passed to the toEntity method to include custom
486
+ * meta information in the Entity creation process. Default is an empty array.
487
+ *
488
+ * @return Entity Returns the Entity object after saving it to the database. This object contains all the product information along
489
+ * with any additional attributes that were passed.
490
+ *
491
+ * @example
492
+ * // Example usage:
493
+ * $product = new Product();
494
+ * $entity = $product->createAsEntity(['meta' => ['custom_attribute' => 'value']]);
495
+ */
496
+ public function createAsEntity(array $additionalAttributes = []): Entity
497
+ {
498
+ $entity = $this->toEntity();
499
+ $entity->save();
500
+
501
+ return $entity;
502
+ }
433
503
  }
@@ -0,0 +1,17 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\Storefront\Observers;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+
7
+ class OrderObserver
8
+ {
9
+ /**
10
+ * Handle the Order "created" event.
11
+ *
12
+ * @return void
13
+ */
14
+ public function created(Order $order)
15
+ {
16
+ }
17
+ }
@@ -145,6 +145,7 @@ Route::prefix(config('storefront.api.routing.prefix', 'storefront'))->namespace(
145
145
  'products',
146
146
  function ($router, $controller) {
147
147
  $router->post('process-imports', $controller('processImports'));
148
+ $router->post('create-entities', $controller('createEntities'));
148
149
  }
149
150
  );
150
151
  $router->fleetbaseRoutes('product-hours');