diff --git a/.prettierrc.js b/.prettierrc.js index bf64a90..dfd67dc 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -2,7 +2,7 @@ module.exports = { trailingComma: 'es5', - tabWidth: 4, + tabWidth: 2, semi: true, singleQuote: true, printWidth: 190, @@ -13,5 +13,11 @@ module.exports = { singleQuote: false, }, }, + { + files: '*.{yml,yaml}', + options: { + tabWidth: 2, + }, + }, ], }; diff --git a/addon/components/context-panel.hbs b/addon/components/context-panel.hbs new file mode 100644 index 0000000..56190b0 --- /dev/null +++ b/addon/components/context-panel.hbs @@ -0,0 +1,5 @@ +{{#if this.contextPanel.currentContextRegistry}} + {{#let this.contextPanel.currentContext this.contextPanel.currentContextRegistry this.contextPanel.currentContextComponentArguments as |model registry dynamicArgs|}} + {{component registry.component context=model dynamicArgs=dynamicArgs onPressCancel=this.contextPanel.clear options=this.contextPanel.contextOptions}} + {{/let}} +{{/if}} \ No newline at end of file diff --git a/addon/components/context-panel.js b/addon/components/context-panel.js new file mode 100644 index 0000000..1a63221 --- /dev/null +++ b/addon/components/context-panel.js @@ -0,0 +1,6 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; + +export default class ContextPanelComponent extends Component { + @service contextPanel; +} diff --git a/addon/components/customer-panel.hbs b/addon/components/customer-panel.hbs new file mode 100644 index 0000000..735c983 --- /dev/null +++ b/addon/components/customer-panel.hbs @@ -0,0 +1,70 @@ + + +
+ +
+
+
+
+
+
+
+ {{this.customer.name}} + + + +
+
+

{{this.customer.name}}

+
+
+ {{smart-humanize this.customer.type}} +
+
+
+
+
+
+ +
+ +
+
+ +
+
+ +
+
+
+ {{component this.tab.component customer=this.customer tabOptions=this.tab options=this.tab.componentParams}} +
+
+
\ No newline at end of file diff --git a/addon/components/customer-panel.js b/addon/components/customer-panel.js new file mode 100644 index 0000000..8274c9b --- /dev/null +++ b/addon/components/customer-panel.js @@ -0,0 +1,171 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { isArray } from '@ember/array'; +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. + * + * @type {Service} + */ + @service fetch; + + /** + * Service for managing modals. + * + * @type {Service} + */ + @service modalsManager; + + /** + * Universe service for managing global data and settings. + * + * @type {Service} + */ + @service universe; + + /** + * Ember data store service. + * + * @type {Service} + */ + @service store; + + /** + * Service for managing routing within the host app. + * + * @type {Service} + */ + @service hostRouter; + + /** + * Service for managing the context panel. + * + * @type {Service} + */ + @service contextPanel; + + /** + * The current active tab. + * + * @type {Object} + * @tracked + */ + @tracked tab; + + /** + * The customer being displayed or edited. + * + * @type {customerModel} + * @tracked + */ + @tracked customer; + + /** + * Overlay context. + * @type {any} + */ + @tracked context; + + /** + * Initializes the customer panel component. + */ + constructor() { + super(...arguments); + this.customer = this.args.customer; + + this.tab = this.getTabUsingSlug(this.args.tab); + applyContextComponentArguments(this); + } + + /** + /** + * Returns the array of tabs available for the panel. + * + * @type {Array} + */ + get tabs() { + const registeredTabs = this.universe.getMenuItemsFromRegistry('component:customer-panel'); + + const defaultTabs = [ + this.universe._createMenuItem('Details', null, { icon: 'circle-info', component: CustomerPanelDetailsComponent }), + this.universe._createMenuItem('Orders', null, { icon: 'circle-info', component: CustomerPanelOrdersComponent }), + ]; + + if (isArray(registeredTabs)) { + return [...defaultTabs, ...registeredTabs]; + } + + return defaultTabs; + } + /** + * Sets the overlay context. + * + * @action + * @param {OverlayContextObject} overlayContext + */ + @action setOverlayContext(overlayContext) { + this.context = overlayContext; + contextComponentCallback(this, 'onLoad', ...arguments); + } + + /** + * Handles changing the active tab. + * + * @method + * @param {String} tab - The new tab to switch to. + * @action + */ + @action onTabChanged(tab) { + this.tab = this.getTabUsingSlug(tab); + contextComponentCallback(this, 'onTabChanged', tab); + } + + /** + * Handles edit action for the customer. + * + * @method + * @action + */ + @action onEdit() { + const isActionOverrided = contextComponentCallback(this, 'onEdit', this.customer); + + if (!isActionOverrided) { + this.contextPanel.focus(this.customer, 'editing', { + onAfterSave: () => { + this.contextPanel.clear(); + }, + }); + } + } + + /** + * Handles the cancel action. + * + * @method + * @action + * @returns {Boolean} Indicates whether the cancel action was overridden. + */ + @action onPressCancel() { + return contextComponentCallback(this, 'onPressCancel', this.customer); + } + + /** + * Finds and returns a tab based on its slug. + * + * @param {String} tabSlug - The slug of the tab. + * @returns {Object|null} The found tab or null. + */ + getTabUsingSlug(tabSlug) { + if (tabSlug) { + return this.tabs.find(({ slug }) => slug === tabSlug); + } + + return this.tabs[0]; + } +} diff --git a/addon/components/customer-panel/details.hbs b/addon/components/customer-panel/details.hbs new file mode 100644 index 0000000..50a3422 --- /dev/null +++ b/addon/components/customer-panel/details.hbs @@ -0,0 +1,34 @@ +
+
+ +
+
{{t "storefront.customers.customer-panel.details.web-url"}}
+
{{n-a @customer.name}}
+
+ +
+
{{t "storefront.common.title"}}
+
{{n-a @customer.title}}
+
+ +
+
{{t "storefront.common.internal-id"}}
+
{{n-a @customer.internal_id}}
+
+ +
+
{{t "storefront.common.email"}}
+
{{n-a @customer.email}}
+
+ +
+
{{t "storefront.common.phone"}}
+
{{n-a @customer.phone}}
+
+ +
+
{{t "storefront.common.type"}}
+
+
+
+
\ No newline at end of file diff --git a/addon/components/customer-panel/details.js b/addon/components/customer-panel/details.js new file mode 100644 index 0000000..34767bb --- /dev/null +++ b/addon/components/customer-panel/details.js @@ -0,0 +1,3 @@ +import Component from '@glimmer/component'; + +export default class CustomerPanelDetailsComponent extends Component {} diff --git a/addon/components/customer-panel/orders.hbs b/addon/components/customer-panel/orders.hbs new file mode 100644 index 0000000..78f560d --- /dev/null +++ b/addon/components/customer-panel/orders.hbs @@ -0,0 +1,140 @@ +{{! template-lint-disable no-unbound }} + + + +
+ + {{#if this.isLoading}} +
+ +
+ {{/if}} + +
+ {{#each this.orders as |order|}} +
+
+
+ {{order.public_id}} +
{{order.createdAt}}
+
{{order.createdAgo}}
+
+
+ +
{{format-currency order.meta.total order.meta.currency}}
+
+
+
+
+ +
+
{{t "storefront.component.widget.orders.customer"}}: {{n-a order.customer_name}}
+
{{t "storefront.component.widget.orders.driver"}}: {{n-a order.driver_name}}
+
+
+
+ {{t "storefront.component.widget.orders.subtotal"}} + {{format-currency order.meta.subtotal order.meta.currency}} +
+ {{#unless order.meta.is_pickup}} +
+ {{t "storefront.component.widget.orders.delivery-fee"}} + {{format-currency order.meta.delivery_fee order.meta.currency}} +
+ {{/unless}} + {{#if order.meta.tip}} +
+ {{t "storefront.component.widget.order.tip"}} + {{get-tip-amount order.meta.tip order.meta.subtotal order.meta.currency}} +
+ {{/if}} + {{#if order.meta.delivery_tip}} +
+ {{t "storefront.component.widget.order.delivery-tip"}} + {{get-tip-amount order.meta.delivery_tip order.meta.subtotal order.meta.currency}} +
+ {{/if}} +
+ {{t "storefront.component.widget.order.tip"}} + {{format-currency order.meta.total order.meta.currency}} +
+
+
+
+ {{/each}} +
+
\ No newline at end of file diff --git a/addon/components/customer-panel/orders.js b/addon/components/customer-panel/orders.js new file mode 100644 index 0000000..6e08a45 --- /dev/null +++ b/addon/components/customer-panel/orders.js @@ -0,0 +1,78 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { inject as controller } from '@ember/controller'; +import { action, computed, get } from '@ember/object'; +import { task } from 'ember-concurrency-decorators'; +export default class CustomerPanelOrdersComponent extends Component { + @service store; + @service storefront; + @service fetch; + @service intl; + @service appCache; + @service modalsManager; + @service contextPanel; + @tracked isLoading = true; + @tracked orders = []; + @tracked customer; + @controller('orders.index.view') orderDetailsController; + + @computed('args.title') get title() { + return this.args.title ?? this.intl.t('storefront.component.widget.orders.widget-title'); + } + + constructor() { + super(...arguments); + this.customer = this.args.customer; + this.reloadOrders.perform(); + } + + @task *reloadOrders(params = {}) { + this.orders = yield this.fetchOrders(params); + } + + @action fetchOrders(params = {}) { + this.isLoading = true; + + return new Promise((resolve) => { + const storefront = get(this.storefront, 'activeStore.public_id'); + + if (!storefront || !this.customer?.id) { + this.isLoading = false; + return resolve([]); + } + + const queryParams = { + storefront, + limit: 25, + sort: '-created_at', + customer_uuid: this.customer?.id, + ...params, + }; + + this.fetch + .get('orders', queryParams, { + namespace: 'storefront/int/v1', + normalizeToEmberData: true, + }) + .then((orders) => { + this.isLoading = false; + + resolve(orders); + }) + .catch(() => { + this.isLoading = false; + + resolve(this.orders); + }); + }); + } + + @action search(event) { + this.reloadOrders.perform({ query: event.target.value ?? '' }); + } + + @action async viewOrder(order) { + this.contextPanel.focus(order, 'viewing'); + } +} diff --git a/addon/components/display-place.hbs b/addon/components/display-place.hbs new file mode 100644 index 0000000..1d2bd16 --- /dev/null +++ b/addon/components/display-place.hbs @@ -0,0 +1,52 @@ +
+ {{#let (or @place.place @place) as |place|}} + {{#if (is-empty place)}} +
+ + {{#if @type}} + {{t "fleet-ops.component.display-panel.no-address" htmlSafe=true type=@type}} + {{else}} + {{t "fleet-ops.component.display-panel.no-address-message"}} + {{/if}} + +
+ {{else}} +
+ {{#if place.name}} + {{place.name}}
+ {{/if}} + {{#if place.street1}} + {{place.street1}}
+ {{/if}} + {{#if place.street2}} + {{place.street2}}
+ {{/if}} +
+ {{#if place.city}} + {{place.city}} + {{/if}} + {{#if place.province}} + {{place.province}} + {{/if}} + {{#if place.postal_code}} + {{place.postal_code}} + {{/if}} +
+
+ {{#if place.neighborhood}} + {{place.neighborhood}} + {{/if}} + {{#if place.district}} + {{place.district}} + {{/if}} + {{#if (and place.building (not place.street1))}} + {{place.building}} + {{/if}} +
+ {{#if place.phone}} + {{place.phone}}
+ {{/if}} +
+ {{/if}} + {{/let}} +
\ No newline at end of file diff --git a/addon/components/display-place.js b/addon/components/display-place.js new file mode 100644 index 0000000..6620741 --- /dev/null +++ b/addon/components/display-place.js @@ -0,0 +1,11 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; + +export default class DisplayPlaceComponent extends Component { + @tracked ref; + + @action setupComponent(element) { + this.ref = element; + } +} diff --git a/addon/components/modals/manage-addons.hbs b/addon/components/modals/manage-addons.hbs index 0f38266..ef716a0 100644 --- a/addon/components/modals/manage-addons.hbs +++ b/addon/components/modals/manage-addons.hbs @@ -1,20 +1,13 @@ -s +s {{/each}} diff --git a/addon/components/modals/manage-addons.js b/addon/components/modals/manage-addons.js index d18130f..6af7325 100644 --- a/addon/components/modals/manage-addons.js +++ b/addon/components/modals/manage-addons.js @@ -2,85 +2,135 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { inject as service } from '@ember/service'; import { action } from '@ember/object'; +import { isArray } from '@ember/array'; +import { task } from 'ember-concurrency'; +import { all } from 'rsvp'; +/** + * Represents a component for managing addon categories and individual addons. + * Allows for the creation, deletion, and editing of addon categories, as well as adding and removing addons within those categories. + * This component uses various Ember services like store, internationalization, currentUser, modalsManager, and notifications for its operations. + */ export default class ModalsManageAddonsComponent extends Component { + /** Service for data store operations. */ @service store; + + /** Service for internationalization. */ @service intl; + + /** Service for accessing current user information. */ @service currentUser; + + /** Service for managing modal dialogs. */ @service modalsManager; + + /** Service for displaying notifications. */ + @service notifications; + + /** Tracked array of addon categories. */ @tracked categories = []; - @tracked isLoading = true; - @action saveAddon(addon) { - this.isLoading = true; + /** Tracked options object for the component. */ + @tracked options = {}; - return addon.save().then(() => { - this.isLoading = false; - }); + /** The currently active store object. */ + @tracked activeStore; + + /** + * Constructs the ModalsManageAddonsComponent instance with the given options. + * @param {Object} owner - The owner of the instance. + * @param {Object} options - Configuration options for the component. + */ + constructor(owner, { options }) { + super(...arguments); + this.options = options; + this.activeStore = options.store; + this.getAddonCategories.perform(); } - @action insertNewAddon(category) { + /** + * Creates a new addon associated with the provided category. + * @param {Object} category - The category to which the new addon will be added. + */ + @action createNewAddon(category) { const productAddon = this.store.createRecord('product-addon', { category_uuid: category.id }); category.addons.pushObject(productAddon); } - @action removeAddon(category, index) { - const addon = category.addons.objectAt(index); + /** + * Saves changes to all the categories. + * Displays loading modal during the operation and handles errors. + */ + @task *saveChanges() { + this.modalsManager.startLoading(); + try { + yield all(this.categories.map((_) => _.save())); + } catch (error) { + this.modalsManager.stopLoading(); + return this.notifications.serverError(error); + } + yield this.modalsManager.done(); + this.categories = []; + } + /** + * Removes an addon from the specified category. + * @param {Object} category - The category from which the addon will be removed. + * @param {number} index - The index of the addon to remove. + */ + @task *removeAddon(category, index) { + const addon = category.addons.objectAt(index); category.addons.removeAt(index); - return addon.destroyRecord(); + yield addon.destroyRecord(); } - @action saveCategory(category) { - this.isLoading = true; - return category.save().then(() => { - this.isLoading = false; - }); + /** + * Saves the provided addon category. + * @param {Object} category - The addon category to save. + */ + @task *saveAddonCategory(category) { + yield category.save(); } - @action deleteCategory(index) { + /** + * Deletes an addon category at the specified index. + * @param {number} index - The index of the category to delete. + */ + @task *deleteAddonCategory(index) { const category = this.categories.objectAt(index); const result = confirm(this.intl.t('storefront.component.modals.manage-addons.delete-this-addon-category-assosiated-will-lost')); if (result) { - this.categories.removeAt(index); - return category.destroyRecord(); + this.categories = [...this.categories.filter((_, i) => i !== index)]; + yield category.destroyRecord(); } } - @action pushCategory(category) { - const { categories } = this; - categories.pushObject(category); - } - - @action createCategory(store) { + /** + * Creates a new addon category with default settings. + */ + @task *createAddonCategory() { const category = this.store.createRecord('addon-category', { name: this.intl.t('storefront.component.modals.manage-addons.untitled-addon-category'), for: 'storefront_product_addon', owner_type: 'storefront:store', - owner_uuid: store.id, + owner_uuid: this.activeStore.id, }); - this.isLoading = true; - - return category.save().then((category) => { - this.pushCategory(category); - this.isLoading = false; - }); + yield category.save(); + this.categories.pushObject(category); } - @action fetchCategories(store) { - this.isLoading = true; - - return this.store - .query('addon-category', { - owner_uuid: store.id, - }) - .then((categories) => { - this.categories = categories.toArray(); - }) - .finally(() => { - this.isLoading = false; + /** + * Retrieves and sets the addon categories associated with the active store. + */ + @task *getAddonCategories() { + const categories = yield this.store.query('addon-category', { owner_uuid: this.activeStore.id }); + if (isArray(categories)) { + this.categories = categories.map((category) => { + category.addons = isArray(category.addons) ? category.addons.filter((addon) => !addon.isNew) : []; + return category; }); + } } } diff --git a/addon/components/modals/select-addon-category.hbs b/addon/components/modals/select-addon-category.hbs index 8be44fc..e144e66 100644 --- a/addon/components/modals/select-addon-category.hbs +++ b/addon/components/modals/select-addon-category.hbs @@ -2,7 +2,7 @@