diff --git a/addon/components/add-product-as-entity-button.hbs b/addon/components/add-product-as-entity-button.hbs
new file mode 100644
index 0000000..4275e58
--- /dev/null
+++ b/addon/components/add-product-as-entity-button.hbs
@@ -0,0 +1,3 @@
+{{#if (eq @order.type "storefront")}}
+
+{{/if}}
\ No newline at end of file
diff --git a/addon/components/add-product-as-entity-button.js b/addon/components/add-product-as-entity-button.js
new file mode 100644
index 0000000..4db8f67
--- /dev/null
+++ b/addon/components/add-product-as-entity-button.js
@@ -0,0 +1,58 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+import { isArray } from '@ember/array';
+import { inject as service } from '@ember/service';
+
+export default class AddProductAsEntityButtonComponent extends Component {
+ @service modalsManager;
+ @service fetch;
+ @service notifications;
+ @tracked order;
+ @tracked controller;
+
+ constructor(owner, { order, controller }) {
+ super(...arguments);
+ this.order = order;
+ this.controller = controller;
+ }
+
+ @action promptProductSelection() {
+ this.modalsManager.show('modals/select-product', {
+ title: 'Select Product to Add to Order',
+ modalClass: 'modal-lg',
+ acceptButtonText: 'Add Products',
+ acceptButtonDisabled: true,
+ selectedProducts: [],
+ selectedStorefront: null,
+ confirm: (modal) => {
+ modal.startLoading();
+
+ const selectedStorefront = modal.getOption('selectedStorefront');
+ const selectedProducts = modal.getOption('selectedProducts', []);
+ const products = selectedProducts.map((product) => product.id);
+
+ return this.fetch
+ .post('products/create-entities', { products }, { namespace: 'storefront/int/v1', normalizeToEmberData: true, normalizeModelType: 'entity' })
+ .then((entities) => {
+ this.controller.addEntities(entities);
+ if (isArray(this.order.meta)) {
+ this.order.meta.pushObjects([
+ {
+ key: 'storefront',
+ value: selectedStorefront.name
+ },
+ {
+ key: 'storefront_id',
+ value: selectedStorefront.public_id
+ }
+ ]);
+ }
+ })
+ .catch((error) => {
+ this.notifications.serverError(error);
+ });
+ },
+ });
+ }
+}
diff --git a/addon/components/customer-panel.js b/addon/components/customer-panel.js
index 8274c9b..976e5f8 100644
--- a/addon/components/customer-panel.js
+++ b/addon/components/customer-panel.js
@@ -7,6 +7,7 @@ import CustomerPanelDetailsComponent from './customer-panel/details';
import CustomerPanelOrdersComponent from './customer-panel/orders';
import contextComponentCallback from '@fleetbase/ember-core/utils/context-component-callback';
import applyContextComponentArguments from '@fleetbase/ember-core/utils/apply-context-component-arguments';
+
export default class CustomerPanelComponent extends Component {
/**
* Service for fetching data.
diff --git a/addon/components/modals/select-product.hbs b/addon/components/modals/select-product.hbs
new file mode 100644
index 0000000..75c5855
--- /dev/null
+++ b/addon/components/modals/select-product.hbs
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+ {{storefront.name}}
+
+
+
+
+ {{#if (and this.selectedStorefront this.productCategories)}}
+
+
+
+ {{category.name}}
+
+
+
+ {{/if}}
+
+ {{#if this.selectedProducts}}
+
+ Selected
+ {{pluralize this.selectedProducts.length "Product"}}
+
+ {{/if}}
+
+
+ {{#if this.selectedStorefront}}
+ {{#if this.fetchProductsForStorefront.isRunning}}
+
+ {{else}}
+
+ {{#each this.products as |product|}}
+ {{#let (in-array product this.selectedProducts) as |isSelected|}}
+
+
+
+
+
+
{{product.name}}
+
{{product.description}}
+
{{format-currency product.price product.currency}}
+
+
+
+
+
+ {{/let}}
+ {{else}}
+
+
No products
+
+ {{/each}}
+
+ {{/if}}
+ {{/if}}
+
+
+
\ No newline at end of file
diff --git a/addon/components/modals/select-product.js b/addon/components/modals/select-product.js
new file mode 100644
index 0000000..570f74d
--- /dev/null
+++ b/addon/components/modals/select-product.js
@@ -0,0 +1,88 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+import { inject as service } from '@ember/service';
+import { task } from 'ember-concurrency';
+import { pluralize } from 'ember-inflector';
+
+export default class ModalsSelectProductComponent extends Component {
+ @service fetch;
+ @service notifications;
+ @service modalsManager;
+ @tracked stores = [];
+ @tracked productCategories = [];
+ @tracked products = [];
+ @tracked selectedProducts = [];
+ @tracked selectedStorefront;
+ @tracked selectedCategory;
+ @tracked storefrontSelectAPI;
+
+ constructor() {
+ super(...arguments);
+ this.fetchStorefronts.perform();
+ }
+
+ @action toggleProductSelection(product) {
+ if (this.selectedProducts.includes(product)) {
+ this.selectedProducts.removeObject(product);
+ } else {
+ this.selectedProducts.pushObject(product);
+ }
+
+ this.updateSelectedProducts();
+ }
+
+ updateSelectedProducts() {
+ this.modalsManager.setOption('selectedProducts', this.selectedProducts);
+ this.modalsManager.setOption('acceptButtonDisabled', this.selectedProducts.length === 0);
+ this.modalsManager.setOption('acceptButtonText', this.selectedProducts.length ? `Add ${pluralize(this.selectedProducts.length, 'Product')}` : 'Add Products');
+ this.modalsManager.setOption('selectedStorefront', this.selectedStorefront);
+ }
+
+ @action setStorefrontSelectApi(storefrontSelectAPI) {
+ this.storefrontSelectAPI = storefrontSelectAPI;
+ }
+
+ @action onStorefrontSelect(storefront) {
+ this.selectedStorefront = storefront;
+ this.selectedCategory = null;
+ this.selectedProducts = [];
+ this.updateSelectedProducts();
+ this.fetchCategoriesForStorefront.perform(storefront);
+ this.fetchProductsForStorefront.perform(storefront);
+ }
+
+ @action onSelectProductCategory(category) {
+ this.selectedCategory = category;
+ this.fetchProductsForStorefront.perform(this.selectedStorefront, { category_slug: category.slug });
+ }
+
+ @task *fetchStorefronts(queryParams = {}) {
+ try {
+ this.stores = yield this.fetch.get('stores', queryParams, { namespace: 'storefront/int/v1', normalizeToEmberData: true });
+ } catch (error) {
+ this.notifications.serverError(error);
+ return;
+ }
+
+ if (this.stores && this.storefrontSelectAPI) {
+ this.storefrontSelectAPI.actions.select(this.stores[0]);
+ }
+ }
+
+ @task *fetchProductsForStorefront(storefront, queryParams = {}) {
+ try {
+ this.products = yield this.fetch.get('products', { store_uuid: storefront.id, ...queryParams }, { namespace: 'storefront/int/v1', normalizeToEmberData: true });
+ } catch (error) {
+ this.notifications.serverError(error);
+ }
+ }
+
+ @task *fetchCategoriesForStorefront(storefront, queryParams = {}) {
+ try {
+ this.productCategories = yield this.fetch.get('categories', { for: 'storefront_product', owner_uuid: storefront.id, limit: -1, ...queryParams }, { normalizeToEmberData: true });
+ } catch (error) {
+ this.notifications.serverError(error);
+ }
+ }
+}
diff --git a/addon/components/order-panel.js b/addon/components/order-panel.js
index 86b526e..afc9b73 100644
--- a/addon/components/order-panel.js
+++ b/addon/components/order-panel.js
@@ -1,16 +1,14 @@
import Component from '@glimmer/component';
-
+import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import applyContextComponentArguments from '@fleetbase/ember-core/utils/apply-context-component-arguments';
import contextComponentCallback from '@fleetbase/ember-core/utils/context-component-callback';
-import { tracked } from '@glimmer/tracking';
export default class OrderPanelComponent extends Component {
@tracked context = null;
constructor() {
super(...arguments);
-
applyContextComponentArguments(this);
}
diff --git a/addon/components/widget/orders.hbs b/addon/components/widget/orders.hbs
index 668902b..c887734 100644
--- a/addon/components/widget/orders.hbs
+++ b/addon/components/widget/orders.hbs
@@ -31,8 +31,8 @@
{{order.public_id}} |
{{format-currency order.meta.total order.meta.currency}} |
- {{n-a order.customer_name}} |
- {{n-a order.driver_name}} |
+ {{n-a order.customer.name}} |
+ {{n-a order.driver_assigned.name}} |
{{order.createdAgo}} |
diff --git a/addon/components/widget/orders.js b/addon/components/widget/orders.js
index a3ddcff..aaf7092 100644
--- a/addon/components/widget/orders.js
+++ b/addon/components/widget/orders.js
@@ -4,6 +4,7 @@ import { inject as service } from '@ember/service';
import { inject as controller } from '@ember/controller';
import { action, computed, get } from '@ember/object';
import { later } from '@ember/runloop';
+
export default class WidgetOrdersComponent extends Component {
@service store;
@service storefront;
diff --git a/addon/controllers/products/index/category/new.js b/addon/controllers/products/index/category/new.js
index a3d0f18..befe937 100644
--- a/addon/controllers/products/index/category/new.js
+++ b/addon/controllers/products/index/category/new.js
@@ -66,7 +66,10 @@ export default class ProductsIndexCategoryNewController extends BaseController {
this.loader.removeLoader(loader);
this.notifications.success(this.intl.t('storefront.products.index.new.new-product-created-success'));
- yield this.transitionToRoute('products.index.category', category.slug);
+
+ try {
+ yield this.transitionToRoute('products.index.category', category.slug);
+ } catch (error) {}
this.reset();
}
diff --git a/addon/engine.js b/addon/engine.js
index 48706ad..fa2bb1a 100644
--- a/addon/engine.js
+++ b/addon/engine.js
@@ -5,6 +5,7 @@ import config from './config/environment';
import services from '@fleetbase/ember-core/exports/services';
import StorefrontKeyMetricsWidget from './components/widget/storefront-key-metrics';
import StorefrontOrderSummaryComponent from './components/storefront-order-summary';
+import AddProductAsEntityButtonComponent from './components/add-product-as-entity-button';
const { modulePrefix } = config;
const externalRoutes = ['console', 'extensions'];
@@ -35,6 +36,7 @@ export default class StorefrontEngine extends Engine {
// register component to views
universe.registerRenderableComponent('@fleetbase/fleetops-engine', 'fleet-ops:template:operations:orders:view', StorefrontOrderSummaryComponent);
+ universe.registerRenderableComponent('@fleetbase/fleetops-engine', 'fleet-ops:template:operations:orders:new:entities-input', AddProductAsEntityButtonComponent);
// register widgets
universe.registerDefaultDashboardWidgets([KeyMetricsWidgetDefinition]);
diff --git a/app/components/add-product-as-entity-button.js b/app/components/add-product-as-entity-button.js
new file mode 100644
index 0000000..d2c2516
--- /dev/null
+++ b/app/components/add-product-as-entity-button.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/components/add-product-as-entity-button';
diff --git a/app/components/modals/select-product.js b/app/components/modals/select-product.js
new file mode 100644
index 0000000..0095ba1
--- /dev/null
+++ b/app/components/modals/select-product.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/storefront-engine/components/modals/select-product';
diff --git a/composer.json b/composer.json
index d524e0a..ce66170 100644
--- a/composer.json
+++ b/composer.json
@@ -1,6 +1,6 @@
{
"name": "fleetbase/storefront-api",
- "version": "0.3.9",
+ "version": "0.3.10",
"description": "Headless Commerce & Marketplace Extension for Fleetbase",
"keywords": [
"fleetbase-extension",
@@ -22,8 +22,8 @@
],
"require": {
"php": "^8.0",
- "fleetbase/core-api": "^1.4.25",
- "fleetbase/fleetops-api": "^0.4.30",
+ "fleetbase/core-api": "^1.4.26",
+ "fleetbase/fleetops-api": "^0.5.0",
"geocoder-php/google-maps-places-provider": "^1.4",
"laravel-notification-channels/apn": "^5.0",
"laravel-notification-channels/fcm": "^4.1",
diff --git a/package.json b/package.json
index 7b8ce56..f740831 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@fleetbase/storefront-engine",
- "version": "0.3.9",
+ "version": "0.3.10",
"description": "Headless Commerce & Marketplace Extension for Fleetbase",
"fleetbase": {
"route": "storefront",
@@ -44,7 +44,7 @@
},
"dependencies": {
"@fleetbase/ember-core": "^0.2.11",
- "@fleetbase/ember-ui": "^0.2.16",
+ "@fleetbase/ember-ui": "^0.2.17",
"@fleetbase/fleetops-data": "^0.1.15",
"@babel/core": "^7.23.2",
"@fortawesome/ember-fontawesome": "^0.4.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f33fd80..6d0ecbf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,8 +8,8 @@ dependencies:
specifier: ^0.2.11
version: 0.2.11(@ember/test-helpers@3.2.0)(ember-source@5.4.0)(webpack@5.89.0)
'@fleetbase/ember-ui':
- specifier: ^0.2.16
- version: 0.2.16(@ember/test-helpers@3.2.0)(@glimmer/component@1.1.2)(@glimmer/tracking@1.1.2)(ember-source@5.4.0)(postcss@8.4.35)(rollup@4.12.0)(tracked-built-ins@3.3.0)(webpack@5.89.0)
+ specifier: ^0.2.17
+ version: 0.2.17(@ember/test-helpers@3.2.0)(@glimmer/component@1.1.2)(@glimmer/tracking@1.1.2)(ember-source@5.4.0)(postcss@8.4.35)(rollup@4.12.0)(tracked-built-ins@3.3.0)(webpack@5.89.0)
'@fleetbase/fleetops-data':
specifier: ^0.1.15
version: 0.1.15
@@ -1546,9 +1546,9 @@ packages:
peerDependencies:
postcss: ^8.4
dependencies:
- '@csstools/selector-specificity': 3.1.1(postcss-selector-parser@6.0.15)
+ '@csstools/selector-specificity': 3.1.1(postcss-selector-parser@6.1.0)
postcss: 8.4.35
- postcss-selector-parser: 6.0.15
+ postcss-selector-parser: 6.1.0
dev: false
/@csstools/postcss-color-function@3.0.16(postcss@8.4.35):
@@ -1669,9 +1669,9 @@ packages:
peerDependencies:
postcss: ^8.4
dependencies:
- '@csstools/selector-specificity': 3.1.1(postcss-selector-parser@6.0.15)
+ '@csstools/selector-specificity': 3.1.1(postcss-selector-parser@6.1.0)
postcss: 8.4.35
- postcss-selector-parser: 6.0.15
+ postcss-selector-parser: 6.1.0
dev: false
/@csstools/postcss-light-dark-function@1.0.5(postcss@8.4.35):
@@ -1826,7 +1826,7 @@ packages:
postcss: ^8.4
dependencies:
postcss: 8.4.35
- postcss-selector-parser: 6.0.15
+ postcss-selector-parser: 6.1.0
dev: false
/@csstools/postcss-stepped-value-functions@3.0.8(postcss@8.4.35):
@@ -1891,15 +1891,6 @@ packages:
postcss-selector-parser: 6.0.15
dev: true
- /@csstools/selector-specificity@3.1.1(postcss-selector-parser@6.0.15):
- resolution: {integrity: sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==}
- engines: {node: ^14 || ^16 || >=18}
- peerDependencies:
- postcss-selector-parser: ^6.0.13
- dependencies:
- postcss-selector-parser: 6.0.15
- dev: false
-
/@csstools/selector-specificity@3.1.1(postcss-selector-parser@6.1.0):
resolution: {integrity: sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==}
engines: {node: ^14 || ^16 || >=18}
@@ -2511,8 +2502,8 @@ packages:
- webpack
dev: false
- /@fleetbase/ember-ui@0.2.16(@ember/test-helpers@3.2.0)(@glimmer/component@1.1.2)(@glimmer/tracking@1.1.2)(ember-source@5.4.0)(postcss@8.4.35)(rollup@4.12.0)(tracked-built-ins@3.3.0)(webpack@5.89.0):
- resolution: {integrity: sha512-RLdqzTDAEbOyIfSrNb3JsS/i9wZ5hy0/CnfK68ccIpC6AePImxBrmqUUYDiTzN3afUFjBGpg7P+uwNT8FwM2nw==}
+ /@fleetbase/ember-ui@0.2.17(@ember/test-helpers@3.2.0)(@glimmer/component@1.1.2)(@glimmer/tracking@1.1.2)(ember-source@5.4.0)(postcss@8.4.35)(rollup@4.12.0)(tracked-built-ins@3.3.0)(webpack@5.89.0):
+ resolution: {integrity: sha512-eJLMBZfIotEeGH7QjlAA2Ns/K83QQztVlnWzvLBjD5YxvQflw2dAJE6EPxPWxbjeCi8B9hOoyIP4ZmtSdcz6mQ==}
engines: {node: '>= 18'}
dependencies:
'@babel/core': 7.23.2
@@ -6380,7 +6371,7 @@ packages:
postcss: ^8.4
dependencies:
postcss: 8.4.35
- postcss-selector-parser: 6.0.15
+ postcss-selector-parser: 6.1.0
dev: false
/css-color-converter@2.0.0:
@@ -6402,9 +6393,9 @@ packages:
peerDependencies:
postcss: ^8.4
dependencies:
- '@csstools/selector-specificity': 3.1.1(postcss-selector-parser@6.0.15)
+ '@csstools/selector-specificity': 3.1.1(postcss-selector-parser@6.1.0)
postcss: 8.4.35
- postcss-selector-parser: 6.0.15
+ postcss-selector-parser: 6.1.0
postcss-value-parser: 4.2.0
dev: false
@@ -11614,14 +11605,9 @@ packages:
engines: {node: '>=8'}
dev: true
- /minipass@7.0.4:
- resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==}
- engines: {node: '>=16 || 14 >=14.17'}
-
/minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
- dev: false
/mississippi@3.0.0:
resolution: {integrity: sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==}
@@ -12318,7 +12304,7 @@ packages:
engines: {node: '>=16 || 14 >=14.18'}
dependencies:
lru-cache: 10.2.0
- minipass: 7.0.4
+ minipass: 7.1.2
/path-to-regexp@0.1.7:
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
@@ -12431,7 +12417,7 @@ packages:
postcss: ^8.4
dependencies:
postcss: 8.4.35
- postcss-selector-parser: 6.0.15
+ postcss-selector-parser: 6.1.0
dev: false
/postcss-clamp@4.1.0(postcss@8.4.35):
@@ -12527,7 +12513,7 @@ packages:
'@csstools/css-parser-algorithms': 2.6.3(@csstools/css-tokenizer@2.3.1)
'@csstools/css-tokenizer': 2.3.1
postcss: 8.4.35
- postcss-selector-parser: 6.0.15
+ postcss-selector-parser: 6.1.0
dev: false
/postcss-dir-pseudo-class@8.0.1(postcss@8.4.35):
@@ -12537,7 +12523,7 @@ packages:
postcss: ^8.4
dependencies:
postcss: 8.4.35
- postcss-selector-parser: 6.0.15
+ postcss-selector-parser: 6.1.0
dev: false
/postcss-double-position-gradients@5.0.6(postcss@8.4.35):
@@ -12568,7 +12554,7 @@ packages:
postcss: ^8.4
dependencies:
postcss: 8.4.35
- postcss-selector-parser: 6.0.15
+ postcss-selector-parser: 6.1.0
dev: false
/postcss-focus-within@8.0.1(postcss@8.4.35):
@@ -12578,7 +12564,7 @@ packages:
postcss: ^8.4
dependencies:
postcss: 8.4.35
- postcss-selector-parser: 6.0.15
+ postcss-selector-parser: 6.1.0
dev: false
/postcss-font-variant@5.0.0(postcss@8.4.35):
@@ -12729,7 +12715,7 @@ packages:
postcss: ^8.2.14
dependencies:
postcss: 8.4.35
- postcss-selector-parser: 6.0.15
+ postcss-selector-parser: 6.1.0
dev: false
/postcss-nesting@12.1.5(postcss@8.4.35):
@@ -12857,7 +12843,7 @@ packages:
postcss: ^8.4
dependencies:
postcss: 8.4.35
- postcss-selector-parser: 6.0.15
+ postcss-selector-parser: 6.1.0
dev: false
/postcss-replace-overflow-wrap@4.0.0(postcss@8.4.35):
@@ -12888,7 +12874,7 @@ packages:
postcss: ^8.4
dependencies:
postcss: 8.4.35
- postcss-selector-parser: 6.0.15
+ postcss-selector-parser: 6.1.0
dev: false
/postcss-selector-parser@6.0.15:
@@ -14630,7 +14616,7 @@ packages:
postcss-js: 4.0.1(postcss@8.4.35)
postcss-load-config: 4.0.2(postcss@8.4.35)
postcss-nested: 6.0.1(postcss@8.4.35)
- postcss-selector-parser: 6.0.15
+ postcss-selector-parser: 6.1.0
resolve: 1.22.8
sucrase: 3.35.0
transitivePeerDependencies:
diff --git a/server/src/Http/Controllers/ProductController.php b/server/src/Http/Controllers/ProductController.php
index 95667e6..b4df27e 100644
--- a/server/src/Http/Controllers/ProductController.php
+++ b/server/src/Http/Controllers/ProductController.php
@@ -2,6 +2,7 @@
namespace Fleetbase\Storefront\Http\Controllers;
+use Fleetbase\FleetOps\Http\Resources\v1\Entity as EntityResource;
use Fleetbase\FleetOps\Support\Utils;
use Fleetbase\Models\Category;
use Fleetbase\Models\File;
@@ -165,4 +166,34 @@ public function processImports(Request $request)
return response()->json($products);
}
+
+ /**
+ * 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.
+ *
+ * This function handles a request that includes an array of product UUIDs. It fetches the corresponding Product models from the database,
+ * converts each Product to an Entity using the Product model's toEntity method, and collects these entities. The function finally returns
+ * these entities as a JSON response, which can be useful for front-end applications or other services that need structured product data.
+ *
+ * @param Request $request the request object, expected to contain an array of product UUIDs under the 'products' key
+ *
+ * @return \Illuminate\Http\JsonResponse Returns a JSON response that contains an array of Entity objects, each representing a product.
+ * Each Entity object includes all relevant product details formatted and structured as specified in the Product model's toEntity method.
+ *
+ * @example
+ * // Example usage:
+ * POST /storefront/int/v1/products/create-entities
+ * Body: { "products": ["uuid1", "uuid2"] }
+ */
+ public function createEntities(Request $request)
+ {
+ $productIds = $request->array('products');
+ $products = Product::whereIn('uuid', $productIds)->get();
+ $entities = [];
+
+ foreach ($products as $product) {
+ $entities[] = $product->createAsEntity();
+ }
+
+ return EntityResource::collection($entities);
+ }
}
diff --git a/server/src/Models/Product.php b/server/src/Models/Product.php
index 6514465..f209465 100644
--- a/server/src/Models/Product.php
+++ b/server/src/Models/Product.php
@@ -3,6 +3,7 @@
namespace Fleetbase\Storefront\Models;
use Fleetbase\Casts\Json;
+use Fleetbase\FleetOps\Models\Entity;
use Fleetbase\FleetOps\Support\Utils;
use Fleetbase\Models\Category;
use Fleetbase\Models\File;
@@ -430,4 +431,73 @@ public static function findFromNetwork($search, $store = null, $limit = 20, $net
return $results;
}
+
+ /**
+ * Converts a Product model instance to an Entity object, which represents a more detailed and structured form of the product data.
+ *
+ * This function utilizes the properties of the Product model along with any additional attributes provided to construct a new Entity object.
+ * The Entity object contains detailed information about the product, including identifiers, names, pricing information, and meta attributes.
+ * Meta attributes can be dynamically added to extend the data structure with additional custom information.
+ *
+ * @param array $additionalAttributes Optional. Additional attributes that can be merged into the product's meta information.
+ * This array can include any custom data under the 'meta' key, which is merged with the default meta attributes.
+ * Default is an empty array.
+ *
+ * @return Entity Returns a new Entity object populated with product information and any additional meta attributes.
+ * 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.
+ *
+ * @example
+ * // Example usage:
+ * $product = new Product();
+ * $entity = $product->toEntity(['meta' => ['custom_attribute' => 'value']]);
+ */
+ public function toEntity(array $additionalAttributes = []): Entity
+ {
+ $meta = data_get($additionalAttributes, 'meta', []);
+
+ return new Entity([
+ 'company_uuid' => session('company'),
+ 'photo_uuid' => $this->primary_image_uuid,
+ 'internal_id' => $this->public_id,
+ 'name' => $this->name,
+ 'description' => $this->description,
+ 'currency' => $this->currency,
+ 'sku' => $this->sku,
+ 'price' => $this->price,
+ 'sale_price' => $this->sale_price,
+ 'type' => 'storefront-product',
+ ...$additionalAttributes,
+ 'meta' => [
+ 'product_id' => $this->public_id,
+ 'image_url' => $this->primary_image_url,
+ ...$meta,
+ ],
+ ]);
+ }
+
+ /**
+ * Creates a new Entity from the Product model instance and saves it to the database.
+ *
+ * This function first converts the Product model to an Entity object using the toEntity method. It then saves this Entity object to the
+ * database, ensuring that all product details are persisted. This is particularly useful when the Product model needs to be
+ * represented and stored as an Entity for operations that require a more complex data structure or additional business logic.
+ *
+ * @param array $additionalAttributes Optional. Additional attributes that can be passed to the toEntity method to include custom
+ * meta information in the Entity creation process. Default is an empty array.
+ *
+ * @return Entity Returns the Entity object after saving it to the database. This object contains all the product information along
+ * with any additional attributes that were passed.
+ *
+ * @example
+ * // Example usage:
+ * $product = new Product();
+ * $entity = $product->createAsEntity(['meta' => ['custom_attribute' => 'value']]);
+ */
+ public function createAsEntity(array $additionalAttributes = []): Entity
+ {
+ $entity = $this->toEntity();
+ $entity->save();
+
+ return $entity;
+ }
}
diff --git a/server/src/Observers/OrderObserver.php b/server/src/Observers/OrderObserver.php
new file mode 100644
index 0000000..41c6486
--- /dev/null
+++ b/server/src/Observers/OrderObserver.php
@@ -0,0 +1,17 @@
+post('process-imports', $controller('processImports'));
+ $router->post('create-entities', $controller('createEntities'));
}
);
$router->fleetbaseRoutes('product-hours');
diff --git a/tests/integration/components/add-product-as-entity-button-test.js b/tests/integration/components/add-product-as-entity-button-test.js
new file mode 100644
index 0000000..7cc1073
--- /dev/null
+++ b/tests/integration/components/add-product-as-entity-button-test.js
@@ -0,0 +1,26 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'dummy/tests/helpers';
+import { render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Component | add-product-as-entity-button', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ // Set any properties with this.set('myProperty', 'value');
+ // Handle any actions with this.set('myAction', function(val) { ... });
+
+ await render(hbs``);
+
+ assert.dom().hasText('');
+
+ // Template block usage:
+ await render(hbs`
+
+ template block text
+
+ `);
+
+ assert.dom().hasText('template block text');
+ });
+});
diff --git a/tests/integration/components/modals/select-product-test.js b/tests/integration/components/modals/select-product-test.js
new file mode 100644
index 0000000..8033c79
--- /dev/null
+++ b/tests/integration/components/modals/select-product-test.js
@@ -0,0 +1,26 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'dummy/tests/helpers';
+import { render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Component | modals/select-product', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ // Set any properties with this.set('myProperty', 'value');
+ // Handle any actions with this.set('myAction', function(val) { ... });
+
+ await render(hbs``);
+
+ assert.dom().hasText('');
+
+ // Template block usage:
+ await render(hbs`
+
+ template block text
+
+ `);
+
+ assert.dom().hasText('template block text');
+ });
+});
|