From 3f4a0df816c8f67c91e541c02321e4c678ba9b03 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Wed, 13 Mar 2024 14:58:57 +0800 Subject: [PATCH] Refactored the service rates to utilize the new order config, activity can now require proof of delivery, patched create new order strict validation, added `order_config_uuid` column to service_rates table, few other improvements --- addon/components/activity-form-panel.hbs | 19 +++ addon/components/activity-form-panel.js | 18 +++ addon/components/live-map.js | 21 ++- .../operations/orders/index/new.js | 5 + .../operations/service-rates/index/new.js | 25 +++- .../operations/service-rates/index/edit.js | 19 ++- .../operations/service-rates/index/new.js | 8 +- addon/services/location.js | 98 +++++++------ .../operations/service-rates/index/new.hbs | 11 +- composer.json | 2 +- extension.json | 2 +- package.json | 4 +- pnpm-lock.yaml | 8 +- server/config/fleetops.php | 131 +++++++++++++++++- ...fig_uuid_column_to_service_rates_table.php | 28 ++++ .../Commands/FixLegacyOrderConfigs.php | 28 ++-- .../Api/v1/ServiceQuoteController.php | 15 +- .../Internal/v1/ServiceRateController.php | 3 + .../src/Http/Requests/CreateOrderRequest.php | 45 ++++-- .../Requests/Internal/CreateOrderRequest.php | 51 +++++-- server/src/Http/Resources/v1/ServiceRate.php | 65 ++++----- server/src/Models/ServiceRate.php | 9 ++ translations/en-us.yaml | 7 +- 23 files changed, 474 insertions(+), 148 deletions(-) create mode 100644 server/migrations/2024_03_13_061249_add_order_config_uuid_column_to_service_rates_table.php diff --git a/addon/components/activity-form-panel.hbs b/addon/components/activity-form-panel.hbs index 57637521..c6e5cb51 100644 --- a/addon/components/activity-form-panel.hbs +++ b/addon/components/activity-form-panel.hbs @@ -16,6 +16,25 @@ {{t "fleet-ops.component.activity-form-panel.completes-order"}} + + + {{t "fleet-ops.component.activity-form-panel.require-pod"}} + + + {{#if this.activity.require_pod}} + + + + {{/if}} diff --git a/addon/components/activity-form-panel.js b/addon/components/activity-form-panel.js index f7667820..58fddffa 100644 --- a/addon/components/activity-form-panel.js +++ b/addon/components/activity-form-panel.js @@ -25,6 +25,13 @@ export default class ActivityFormPanelComponent extends Component { */ @tracked targetActivity; + /** + * Proof of delivery options. + * + * @memberof ActivityFormPanelComponent + */ + @tracked podOptions = ['scan', 'signature', 'photo']; + /** * Constructor for ActivityFormPanelComponent. * Applies context component arguments upon instantiation. @@ -46,6 +53,17 @@ export default class ActivityFormPanelComponent extends Component { } } + /** + * Sets the proof of delivery method to be used for this activity. + * + * @param {Event} event + * @memberof ActivityFormPanelComponent + */ + @action setProofOfDeliveryMethod(event) { + const value = event.target.value; + this.activity.set('pod_method', value); + } + /** * Action method to set the activity code. It uses the underscore function to format * the code and updates the status by capitalizing each word. diff --git a/addon/components/live-map.js b/addon/components/live-map.js index 0eb743df..016bd45e 100644 --- a/addon/components/live-map.js +++ b/addon/components/live-map.js @@ -280,7 +280,6 @@ export default class LiveMapComponent extends Component { this.tileSourceUrl = 'https://{s}.tile.jawg.io/jawg-matrix/{z}/{x}/{y}{r}.png?access-token='; } - this.ready(); this.setupComponent(); } @@ -332,7 +331,6 @@ export default class LiveMapComponent extends Component { }) .finally(() => { this.listen(); - this.ready(); }); } @@ -386,14 +384,23 @@ export default class LiveMapComponent extends Component { * if available, or null if the function is skipped. */ async setInitialCoordinates() { - const { latitude, longitude } = await this.location.getUserLocation(); + try { + const { latitude, longitude } = await this.location.getUserLocation(); + + this.latitude = latitude || this.location.DEFAULT_LATITUDE; + this.longitude = longitude || this.location.DEFAULT_LONGITUDE; + } catch (error) { + this.latitude = this.location.DEFAULT_LATITUDE; + this.longitude = this.location.DEFAULT_LONGITUDE; + } - this.latitude = latitude; - this.longitude = longitude; this.ready(); - // trigger that initial coordinates is set to livemap component - this.universe.trigger('fleet-ops.live-map.has_coordinates', { latitude: this.latitude, longitude: this.longitude }); + // Trigger that initial coordinates are set to live map component + this.universe.trigger('fleet-ops.live-map.has_coordinates', { + latitude: this.latitude, + longitude: this.longitude, + }); } /** diff --git a/addon/controllers/operations/orders/index/new.js b/addon/controllers/operations/orders/index/new.js index 684d0119..c043decd 100644 --- a/addon/controllers/operations/orders/index/new.js +++ b/addon/controllers/operations/orders/index/new.js @@ -534,6 +534,11 @@ export default class OperationsOrdersIndexNewController extends BaseController { params.facilitator = this.order.facilitator.public_id; } + // filter by order config type + if (this.orderConfig) { + params.service_type = this.orderConfig.key; + } + if (shouldCheck) { try { serviceRates = await this.fetch.get(`service-rates/for-route`, params); diff --git a/addon/controllers/operations/service-rates/index/new.js b/addon/controllers/operations/service-rates/index/new.js index d09bdde8..cc766ec0 100644 --- a/addon/controllers/operations/service-rates/index/new.js +++ b/addon/controllers/operations/service-rates/index/new.js @@ -48,11 +48,11 @@ export default class OperationsServiceRatesIndexNewController extends BaseContro @tracked serviceRate = this.store.createRecord('service-rate', { per_meter_unit: 'm' }); /** - * Different service types available, based on order type. + * Available order configs. * * @var {Array} */ - @tracked serviceTypes = []; + @tracked orderConfigs = []; /** * Service areas. @@ -75,6 +75,13 @@ export default class OperationsServiceRatesIndexNewController extends BaseContro */ @tracked isCreatingServiceRate = false; + /** + * The current selected order config. + * + * @var {OrderConfigModel|null} + */ + @tracked orderConfig; + /** * True if updating service rate. * @@ -279,6 +286,20 @@ export default class OperationsServiceRatesIndexNewController extends BaseContro }); } + @action setConfig(event) { + const orderConfigId = event.target.value; + if (!orderConfigId) { + return; + } + + const orderConfig = this.store.peekRecord('order-config', orderConfigId); + if (orderConfig) { + this.orderConfig = orderConfig; + this.serviceRate.set('order_config_uuid', orderConfig.id); + this.serviceRate.set('service_type', orderConfig.key); + } + } + /** * Adds a per drop-off rate fee */ diff --git a/addon/routes/operations/service-rates/index/edit.js b/addon/routes/operations/service-rates/index/edit.js index fb762557..670a40d2 100644 --- a/addon/routes/operations/service-rates/index/edit.js +++ b/addon/routes/operations/service-rates/index/edit.js @@ -7,8 +7,20 @@ export default class OperationsServiceRatesIndexEditRoute extends Route { @service currentUser; @service notifications; + /** + * Re-use the new service rate form template. + * + * @memberof OperationsServiceRatesIndexEditRoute + */ templateName = 'operations.service-rates.index.new'; + /** + * Handle any async error. + * + * @param {*} error + * @return {*} + * @memberof OperationsServiceRatesIndexEditRoute + */ @action error(error) { this.notifications.serverError(error); return this.transitionTo('operations.service-rates.index'); @@ -33,10 +45,7 @@ export default class OperationsServiceRatesIndexEditRoute extends Route { controller.parcelFees = model.parcel_fees; } - const serviceTypes = await this.currentUser.getInstalledOrderConfigs(); - const serviceAreas = await this.store.findAll('service-area'); - - controller.serviceTypes = serviceTypes; - controller.serviceAreas = serviceAreas; + controller.orderConfigs = await this.store.findAll('order-config'); + controller.serviceAreas = await this.store.findAll('service-area'); } } diff --git a/addon/routes/operations/service-rates/index/new.js b/addon/routes/operations/service-rates/index/new.js index 2b55e9f8..6e102e5a 100644 --- a/addon/routes/operations/service-rates/index/new.js +++ b/addon/routes/operations/service-rates/index/new.js @@ -3,13 +3,9 @@ import { inject as service } from '@ember/service'; export default class OperationsServiceRatesIndexNewRoute extends Route { @service store; - @service currentUser; async setupController(controller) { - const serviceTypes = await this.currentUser.getInstalledOrderConfigs(); - const serviceAreas = await this.store.findAll('service-area'); - - controller.serviceTypes = serviceTypes; - controller.serviceAreas = serviceAreas; + controller.orderConfigs = await this.store.findAll('order-config'); + controller.serviceAreas = await this.store.findAll('service-area'); } } diff --git a/addon/services/location.js b/addon/services/location.js index db36af15..6fd91589 100644 --- a/addon/services/location.js +++ b/addon/services/location.js @@ -78,66 +78,74 @@ export default class LocationService extends Service { } /** - * Attempts to fetch the user's location from various sources. - * Uses cached data, navigator geolocation, or WHOIS data as fallbacks. + * Attempts to fetch the user's location from various sources including cached data, + * navigator geolocation, or WHOIS data. It first tries to get the cached coordinates. + * If not available or outdated, it tries the browser's geolocation API. + * As a fallback, it uses WHOIS data associated with the user's account. + * * @returns {Promise} A promise that resolves to an object containing latitude and longitude. */ - getUserLocation() { - return this.fetch.cachedGet('fleet-ops/live/coordinates', {}, { expirationInterval: 1, expirationIntervalUnit: 'hour' }).then((coordinates) => { + async getUserLocation() { + // If the location has already been located, return the existing coordinates + if (this.located) { + return { latitude: this.latitude, longitude: this.longitude }; + } + + try { + const coordinates = await this.fetch.cachedGet('fleet-ops/live/coordinates', {}, { expirationInterval: 1, expirationIntervalUnit: 'hour' }); + if (isBlank(coordinates)) { - return this.getUserLocationFromNavigator().then((navigatorCoordinates) => { - this.updateLocation(navigatorCoordinates); - return navigatorCoordinates; - }); + return await this.getUserLocationFromNavigator(); } - if (isArray(coordinates)) { - const validCoordinates = coordinates.filter((point) => point.coordinates[0] !== 0); + if (isArray(coordinates) && coordinates.length > 0) { + // Ensure the coordinates array contains valid data + const validCoordinates = coordinates.find((point) => point.coordinates[0] !== 0 && point.coordinates[1] !== 0); if (validCoordinates) { - const [longitude, latitude] = getWithDefault(validCoordinates, '0.coordinates', [0, 0]); - const userCoordinates = { - latitude, - longitude, - }; - - this.updateLocation(userCoordinates); - return userCoordinates; + const [longitude, latitude] = validCoordinates.coordinates; + this.updateLocation({ latitude, longitude }); + return { latitude, longitude }; } } - return this.getUserLocationFromWhois(); - }); + return await this.getUserLocationFromWhois(); + } catch (error) { + return await this.getUserLocationFromWhois(); + } } /** * Retrieves the user's location using the browser's navigator geolocation API. - * @returns {Promise} A promise that resolves to geolocation coordinates. + * It creates a promise that resolves with the geolocation if successful, + * or with WHOIS data as a fallback in case of failure or absence of navigator geolocation. + * + * @returns {Promise} A promise that resolves to geolocation coordinates or WHOIS data. */ - getUserLocationFromNavigator() { - return new Promise((resolve) => { - // eslint-disable-next-line no-undef - if (window.navigator && window.navigator.geolocation) { - // eslint-disable-next-line no-undef - return navigator.geolocation.getCurrentPosition( - ({ coords }) => { - this.updateLocation(coords); - return resolve(coords); - }, - () => { - // If failed use user whois - return resolve(this.getUserLocationFromWhois()); - } - ); + async getUserLocationFromNavigator() { + if (window.navigator && window.navigator.geolocation) { + try { + const position = await new Promise((resolve, reject) => { + navigator.geolocation.getCurrentPosition(resolve, reject, { + enableHighAccuracy: true, + timeout: 600, + }); + }); + const { latitude, longitude } = position.coords; + this.updateLocation({ latitude, longitude }); + return { latitude, longitude }; + } catch (error) { + return await this.getUserLocationFromWhois(); } - - // default to whois lookup coordinates - return resolve(this.getUserLocationFromWhois()); - }); + } else { + return await this.getUserLocationFromWhois(); + } } /** - * Retrieves the user's location based on WHOIS data associated with the user's account. - * @returns {Object} An object containing latitude and longitude from WHOIS data. + * Retrieves the user's location based on WHOIS data associated with their account. + * Defaults to predefined coordinates if WHOIS data is not available. + * + * @returns {Object} An object containing latitude and longitude from WHOIS data or default values. */ getUserLocationFromWhois() { const whois = this.currentUser.getOption('whois'); @@ -151,8 +159,10 @@ export default class LocationService extends Service { } /** - * Updates the service's tracked properties with the new location data and triggers an event. - * @param {Object} coordinates - An object containing the latitude and longitude. + * Updates the service's tracked properties with the new location data. + * Triggers an event to notify other parts of the application that the user's location has been updated. + * + * @param {Object} coordinates - An object containing the latitude and longitude to be set. */ updateLocation({ latitude, longitude }) { this.latitude = latitude; diff --git a/addon/templates/operations/service-rates/index/new.hbs b/addon/templates/operations/service-rates/index/new.hbs index 30240074..1f010c42 100644 --- a/addon/templates/operations/service-rates/index/new.hbs +++ b/addon/templates/operations/service-rates/index/new.hbs @@ -17,7 +17,16 @@
- + + {{#each this.orderConfigs as |orderConfig|}} + + {{/each}} +
diff --git a/composer.json b/composer.json index 6a7d12dc..4b5950cb 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "fleetbase/fleetops-api", - "version": "0.4.18", + "version": "0.4.19", "description": "Fleet & Transport Management Extension for Fleetbase", "keywords": [ "fleetbase-extension", diff --git a/extension.json b/extension.json index d014dc40..c174b5f8 100644 --- a/extension.json +++ b/extension.json @@ -1,6 +1,6 @@ { "name": "Fleet-Ops", - "version": "0.4.18", + "version": "0.4.19", "description": "Fleet & Transport Management Extension for Fleetbase", "repository": "https://github.com/fleetbase/fleetops", "license": "MIT", diff --git a/package.json b/package.json index 82efc36a..c8f4b252 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fleetbase/fleetops-engine", - "version": "0.4.18", + "version": "0.4.19", "description": "Fleet & Transport Management Extension for Fleetbase", "fleetbase": { "route": "fleet-ops" @@ -44,7 +44,7 @@ "@babel/core": "^7.23.2", "@fleetbase/ember-core": "^0.2.6", "@fleetbase/ember-ui": "^0.2.11", - "@fleetbase/fleetops-data": "^0.1.13", + "@fleetbase/fleetops-data": "^0.1.14", "@fleetbase/leaflet-routing-machine": "^3.2.16", "@fortawesome/ember-fontawesome": "^0.4.1", "@fortawesome/fontawesome-svg-core": "^6.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d71b461a..fe3e70af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,8 +11,8 @@ dependencies: specifier: ^0.2.11 version: 0.2.11(@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.13 - version: 0.1.13 + specifier: ^0.1.14 + version: 0.1.14 '@fleetbase/leaflet-routing-machine': specifier: ^3.2.16 version: 3.2.16 @@ -2565,8 +2565,8 @@ packages: - webpack-command dev: false - /@fleetbase/fleetops-data@0.1.13: - resolution: {integrity: sha512-+JgP0p15TxMOxfJCSPJt/2nTA8ZugukGuaFYQTLzD0TFFKXjZE9bmYKPf+Z5w9L4oJ4b1oLsvRv6BL5dFt11lw==} + /@fleetbase/fleetops-data@0.1.14: + resolution: {integrity: sha512-CNgr/De//I2+USYBhFvaRD4c1KBbNJUUmPy5oT07fkFbCGplYeVfPhYlCxj2JpeGbtBBqfGEujrViRVRfR2JKg==} engines: {node: '>= 18'} dependencies: '@babel/core': 7.23.2 diff --git a/server/config/fleetops.php b/server/config/fleetops.php index cee9be79..2a14ecd4 100644 --- a/server/config/fleetops.php +++ b/server/config/fleetops.php @@ -2,10 +2,15 @@ /** * ------------------------------------------- - * Fleetbase Core API Configuration + * FleetOps API Configuration * ------------------------------------------- */ return [ + /* + |-------------------------------------------------------------------------- + | API Config + |-------------------------------------------------------------------------- + */ 'api' => [ 'version' => '0.0.1', 'routing' => [ @@ -13,10 +18,128 @@ 'internal_prefix' => 'int' ] ], + 'connection' => [ + 'db' => env('DB_CONNECTION', 'mysql') + ], + + /* + |-------------------------------------------------------------------------- + | Navigator App + |-------------------------------------------------------------------------- + */ 'navigator' => [ 'bypass_verification_code' => env('NAVIGATOR_BYPASS_VERIFICATION_CODE', '999000') ], - 'connection' => [ - 'db' => env('DB_CONNECTION', 'mysql') - ] + + /* + |-------------------------------------------------------------------------- + | API Events + |-------------------------------------------------------------------------- + */ + 'events' => [ + // order events + 'order.created', + 'order.updated', + 'order.deleted', + 'order.dispatched', + 'order.dispatch_failed', + 'order.completed', + 'order.failed', + 'order.driver_assigned', + 'order.completed', + + // payload events + 'payload.created', + 'payload.updated', + 'payload.deleted', + + // entity events + 'entity.created', + 'entity.updated', + 'entity.deleted', + 'entity.driver_assigned', + + // driver events + 'driver.created', + 'driver.updated', + 'driver.deleted', + 'driver.assigned', + // 'driver.entered_zone', + // 'driver.exited_zone', + + // fleet events + 'fleet.created', + 'fleet.updated', + 'fleet.deleted', + + // purchase_rate events + 'purchase_rate.created', + 'purchase_rate.updated', + 'purchase_rate.deleted', + + // contact events + 'contact.created', + 'contact.updated', + 'contact.deleted', + + // place events + 'place.created', + 'place.updated', + 'place.deleted', + + // service_area events + 'service_area.created', + 'service_area.updated', + 'service_area.deleted', + + // service_quote events + 'service_quote.created', + 'service_quote.updated', + 'service_quote.deleted', + + // service_rate events + 'service_rate.created', + 'service_rate.updated', + 'service_rate.deleted', + + // tracking_number events + 'tracking_number.created', + 'tracking_number.updated', + 'tracking_number.deleted', + + // tracking_status events + 'tracking_status.created', + 'tracking_status.updated', + 'tracking_status.deleted', + + // vehicle events + 'vehicle.created', + 'vehicle.updated', + 'vehicle.deleted', + + // vendor events + 'vendor.created', + 'vendor.updated', + 'vendor.deleted', + + // zone events + 'zone.created', + 'zone.updated', + 'zone.deleted', + ], + + /* + |-------------------------------------------------------------------------- + | Proof of Delivery Methods + |-------------------------------------------------------------------------- + */ + 'pod_methods' => 'scan,signature,photo', + + /* + |-------------------------------------------------------------------------- + | API/Webhook Versions + |-------------------------------------------------------------------------- + */ + 'versions' => ['2020-09-30', '2024-03-12'], + 'version' => '2024-03-12', ]; diff --git a/server/migrations/2024_03_13_061249_add_order_config_uuid_column_to_service_rates_table.php b/server/migrations/2024_03_13_061249_add_order_config_uuid_column_to_service_rates_table.php new file mode 100644 index 00000000..2c61220b --- /dev/null +++ b/server/migrations/2024_03_13_061249_add_order_config_uuid_column_to_service_rates_table.php @@ -0,0 +1,28 @@ +foreignUuid('order_config_uuid')->nullable()->after('zone_uuid')->references('uuid')->on('order_configs')->onUpdate('CASCADE')->onDelete('CASCADE'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('service_rates', function (Blueprint $table) { + $table->dropForeign(['order_config_uuid']); + $table->dropColumn(['order_config_uuid']); + }); + } +}; diff --git a/server/src/Console/Commands/FixLegacyOrderConfigs.php b/server/src/Console/Commands/FixLegacyOrderConfigs.php index a6868977..0c44ea2e 100644 --- a/server/src/Console/Commands/FixLegacyOrderConfigs.php +++ b/server/src/Console/Commands/FixLegacyOrderConfigs.php @@ -4,9 +4,9 @@ use Fleetbase\FleetOps\Models\Order; use Fleetbase\FleetOps\Models\OrderConfig; -use Fleetbase\Support\Utils; +use Fleetbase\FleetOps\Support\FleetOps; +use Fleetbase\Models\Company; use Illuminate\Console\Command; -use Illuminate\Support\Facades\Artisan; class FixLegacyOrderConfigs extends Command { @@ -15,7 +15,7 @@ class FixLegacyOrderConfigs extends Command * * @var string */ - protected $signature = 'fleetops:fix-legacy-order-configs'; + protected $signature = 'fleetops:fix-legacy-order-configs {--create-configs}'; /** * The console command description. @@ -35,14 +35,20 @@ class FixLegacyOrderConfigs extends Command */ public function handle() { - if (Utils::classExists('\\Fleetbase\\FleetOps\\Seeders\\OrderConfigSeeder')) { - $this->info('Starting the database seeding...'); - - Artisan::call('db:seed', [ - '--class' => '\\Fleetbase\\FleetOps\\Seeders\\OrderConfigSeeder', - ]); - - $this->info('Database seeding completed.'); + $shouldCreateConfigs = $this->option('create-configs'); + if ($shouldCreateConfigs) { + $companies = Company::all(); + $totalCompanies = $companies->count(); + $this->info('Initializing transport config for ' . $totalCompanies . ' companies.'); + $progressBar = $this->output->createProgressBar($totalCompanies); + $progressBar->start(); + foreach ($companies as $company) { + FleetOps::createTransportConfig($company); + $progressBar->advance(); + } + $progressBar->finish(); + $this->line(''); + $this->info('All transport configs created.'); } $orders = Order::whereNull('order_config_uuid')->get(); diff --git a/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php b/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php index 085a5c8e..6d26f8b0 100644 --- a/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +++ b/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php @@ -4,6 +4,7 @@ use Fleetbase\FleetOps\Http\Requests\QueryServiceQuotesRequest; use Fleetbase\FleetOps\Http\Resources\v1\ServiceQuote as ServiceQuoteResource; +use Fleetbase\FleetOps\Models\Entity; use Fleetbase\FleetOps\Models\IntegratedVendor; use Fleetbase\FleetOps\Models\Payload; use Fleetbase\FleetOps\Models\Place; @@ -76,11 +77,15 @@ public function query(QueryServiceQuotesRequest $request) // if quote for single service if ($service && $service !== 'all') { - $serviceRate = ServiceRate::where('uuid', $service)->where(function ($q) use ($currency) { - if ($currency) { - $q->where(DB::raw('lower(currency)'), strtolower($currency)); - } - })->first(); + $serviceRate = ServiceRate::where( + function ($query) use ($service) { + $query->where('uuid', $service)->orWhere('public_id', $service); + })->where( + function ($q) use ($currency) { + if ($currency) { + $q->where(DB::raw('lower(currency)'), strtolower($currency)); + } + })->first(); $serviceQuotes = collect(); if ($serviceRate) { diff --git a/server/src/Http/Controllers/Internal/v1/ServiceRateController.php b/server/src/Http/Controllers/Internal/v1/ServiceRateController.php index 3eefab10..91277be3 100644 --- a/server/src/Http/Controllers/Internal/v1/ServiceRateController.php +++ b/server/src/Http/Controllers/Internal/v1/ServiceRateController.php @@ -39,6 +39,9 @@ function ($coord) { $waypoints, function ($query) use ($request) { $query->where('company_uuid', $request->session()->get('company')); + if ($request->filled('service_type')) { + $query->where('service_type', $request->input('service_type')); + } } ); diff --git a/server/src/Http/Requests/CreateOrderRequest.php b/server/src/Http/Requests/CreateOrderRequest.php index e2e14fb6..593913fb 100644 --- a/server/src/Http/Requests/CreateOrderRequest.php +++ b/server/src/Http/Requests/CreateOrderRequest.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Requests; +use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Requests\FleetbaseRequest; use Fleetbase\Rules\ExistsInAny; @@ -25,21 +26,26 @@ public function authorize() public function rules() { $validations = [ - 'adhoc' => 'in:true,false,1,0', - 'dispatch' => ['nullable', 'boolean'], - 'adhoc_distance' => 'numeric', - 'pod_required' => 'in:true,false,1,0', - 'pod_method' => 'in:' . config('api.pod_methods'), - 'scheduled_at' => ['nullable', 'date'], - 'driver' => 'nullable|exists:drivers,public_id', - 'service_quote' => 'nullable|exists:service_quotes,public_id', - 'purchase_rate' => 'nullable|exists:purchase_rates,public_id', - 'facilitator' => ['nullable', new ExistsInAny(['vendors', 'contacts', 'integrated_vendors'], ['public_id', 'provider'])], - 'customer' => ['nullable', new ExistsInAny(['vendors', 'contacts'], 'public_id')], - 'status' => 'string', - 'type' => 'string', + 'adhoc' => ['nullable', 'boolean'], + 'dispatch' => ['nullable', 'boolean'], + 'adhoc_distance' => ['nullable', 'numeric'], + 'pod_required' => ['nullable', 'boolean'], + 'pod_method' => ['nullable', 'in:' . config('fleetops.pod_methods')], + 'scheduled_at' => ['nullable', 'date'], + 'driver' => ['nullable', 'exists:drivers,public_id'], + 'service_quote' => ['nullable', 'exists:service_quotes,public_id'], + 'purchase_rate' => ['nullable', 'exists:purchase_rates,public_id'], + 'facilitator' => ['nullable', new ExistsInAny(['vendors', 'contacts', 'integrated_vendors'], ['public_id', 'provider'])], + 'customer' => ['nullable', new ExistsInAny(['vendors', 'contacts'], 'public_id')], + 'status' => ['nullable', 'string'], + 'type' => ['string'], ]; + // Conditionally require 'pod_method' if 'pod_required' is truthy + if (Utils::isTrue($this->input('pod_required'))) { + $validations['pod_method'] = ['required', 'in:' . implode(',', config('fleetops.pod_methods'))]; + } + if ($this->has('payload')) { $validations['payload.entities'] = 'array'; $validations['payload.waypoints'] = 'array'; @@ -77,4 +83,17 @@ public function rules() return $validations; } + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + public function attributes(): array + { + return [ + 'pod_required' => 'proof of delivery required', + 'pod_method' => 'proof of delivery method', + ]; + } } diff --git a/server/src/Http/Requests/Internal/CreateOrderRequest.php b/server/src/Http/Requests/Internal/CreateOrderRequest.php index e205fa31..2153a684 100644 --- a/server/src/Http/Requests/Internal/CreateOrderRequest.php +++ b/server/src/Http/Requests/Internal/CreateOrderRequest.php @@ -2,6 +2,7 @@ namespace Fleetbase\FleetOps\Http\Requests\Internal; +use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Requests\FleetbaseRequest; use Fleetbase\Rules\ExistsInAny; @@ -25,22 +26,27 @@ public function authorize() public function rules() { $validations = [ - 'order_config_uuid' => 'required', - // 'adhoc' => 'in:true,false,1,0', + 'order_config_uuid' => ['required'], + 'adhoc' => ['nullable', 'boolean'], 'dispatch' => ['nullable', 'boolean'], - // 'adhoc_distance' => 'numeric', - 'pod_required' => 'in:true,false,1,0', - 'pod_method' => 'in:' . config('api.pod_methods'), + 'adhoc_distance' => ['nullable', 'numeric'], + 'pod_required' => ['nullable', 'boolean'], + 'pod_method' => ['nullable', 'in:' . config('fleetops.pod_methods')], 'scheduled_at' => ['nullable', 'date'], - 'driver' => 'nullable|exists:drivers,uuid', - 'service_quote' => 'nullable|exists:service_quotes,uuid', - 'purchase_rate' => 'nullable|exists:purchase_rates,uuid', + 'driver' => ['nullable', 'exists:drivers,uuid'], + 'service_quote' => ['nullable', 'exists:service_quotes,uuid'], + 'purchase_rate' => ['nullable', 'exists:purchase_rates,uuid'], 'facilitator' => ['nullable', new ExistsInAny(['vendors', 'contacts', 'integrated_vendors'], ['uuid', 'provider'])], 'customer' => ['nullable', new ExistsInAny(['vendors', 'contacts'], 'uuid')], - 'status' => 'nullable|string', - 'type' => 'string', + 'status' => ['nullable', 'string'], + 'type' => ['string'], ]; + // Conditionally require 'pod_method' if 'pod_required' is truthy + if (Utils::isTrue($this->input('order.pod_required'))) { + $validations['pod_method'] = ['required', 'in:' . config('fleetops.pod_methods')]; + } + if ($this->has('payload')) { $validations['payload.entities'] = 'array'; $validations['payload.waypoints'] = 'array'; @@ -63,4 +69,29 @@ public function rules() return $validations; } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'pod_method.required' => 'A proof of delivery method is required.', + ]; + } + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + public function attributes(): array + { + return [ + 'pod_required' => 'proof of delivery required', + 'pod_method' => 'proof of delivery method', + ]; + } } diff --git a/server/src/Http/Resources/v1/ServiceRate.php b/server/src/Http/Resources/v1/ServiceRate.php index 253ccb07..4174931b 100644 --- a/server/src/Http/Resources/v1/ServiceRate.php +++ b/server/src/Http/Resources/v1/ServiceRate.php @@ -18,37 +18,40 @@ class ServiceRate extends FleetbaseResource public function toArray($request) { return [ - 'id' => $this->when(Http::isInternalRequest(), $this->id, $this->public_id), - 'uuid' => $this->when(Http::isInternalRequest(), $this->uuid), - 'public_id' => $this->when(Http::isInternalRequest(), $this->public_id), - 'service_area' => $this->whenLoaded('serviceArea', new ServiceArea($this->serviceArea)), - 'service_area_name' => $this->when(Http::isInternalRequest(), data_get($this, 'serviceArea.name')), - 'zone' => $this->whenLoaded('zone', new Zone($this->zone)), - 'zone_name' => $this->when(Http::isInternalRequest(), data_get($this, 'zone.name')), - 'service_name' => $this->service_name, - 'service_type' => $this->service_type, - 'base_fee' => $this->base_fee, - 'rate_calculation_method' => $this->rate_calculation_method, - 'per_meter_flat_rate_fee' => $this->per_meter_flat_rate_fee, - 'per_meter_unit' => $this->per_meter_unit, - 'meter_fees' => ServiceRateFee::collection($this->rateFees ?? []), - 'parcel_fees' => ServiceRateParcelFee::collection($this->rateFees ?? []), - 'algorithm' => $this->algorithm, - 'has_cod_fee' => Utils::castBoolean($this->has_cod_fee), - 'cod_calculation_method' => $this->cod_calculation_method, - 'cod_flat_fee' => $this->cod_flat_fee, - 'cod_percent' => $this->cod_percent, - 'has_peak_hours_fee' => Utils::castBoolean($this->has_peak_hours_fee), - 'peak_hours_calculation_method' => $this->peak_hours_calculation_method, - 'peak_hours_flat_fee' => $this->peak_hours_flat_fee, - 'peak_hours_percent' => $this->peak_hours_percent, - 'peak_hours_start' => $this->peak_hours_start, - 'peak_hours_end' => $this->peak_hours_end, - 'currency' => $this->currency, - 'duration_terms' => $this->duration_terms, - 'estimated_days' => $this->estimated_days, - 'updated_at' => $this->updated_at, - 'created_at' => $this->created_at, + 'id' => $this->when(Http::isInternalRequest(), $this->id, $this->public_id), + 'uuid' => $this->when(Http::isInternalRequest(), $this->uuid), + 'service_area_uuid' => $this->when(Http::isInternalRequest(), $this->service_area_uuid), + 'zone_uuid' => $this->when(Http::isInternalRequest(), $this->zone_uuid), + 'order_config_uuid' => $this->when(Http::isInternalRequest(), $this->order_config_uuid), + 'public_id' => $this->when(Http::isInternalRequest(), $this->public_id), + 'service_area' => $this->whenLoaded('serviceArea', new ServiceArea($this->serviceArea)), + 'service_area_name' => $this->when(Http::isInternalRequest(), data_get($this, 'serviceArea.name')), + 'zone' => $this->whenLoaded('zone', new Zone($this->zone)), + 'zone_name' => $this->when(Http::isInternalRequest(), data_get($this, 'zone.name')), + 'service_name' => $this->service_name, + 'service_type' => $this->service_type, + 'base_fee' => $this->base_fee, + 'rate_calculation_method' => $this->rate_calculation_method, + 'per_meter_flat_rate_fee' => $this->per_meter_flat_rate_fee, + 'per_meter_unit' => $this->per_meter_unit, + 'meter_fees' => ServiceRateFee::collection($this->rateFees ?? []), + 'parcel_fees' => ServiceRateParcelFee::collection($this->rateFees ?? []), + 'algorithm' => $this->algorithm, + 'has_cod_fee' => Utils::castBoolean($this->has_cod_fee), + 'cod_calculation_method' => $this->cod_calculation_method, + 'cod_flat_fee' => $this->cod_flat_fee, + 'cod_percent' => $this->cod_percent, + 'has_peak_hours_fee' => Utils::castBoolean($this->has_peak_hours_fee), + 'peak_hours_calculation_method' => $this->peak_hours_calculation_method, + 'peak_hours_flat_fee' => $this->peak_hours_flat_fee, + 'peak_hours_percent' => $this->peak_hours_percent, + 'peak_hours_start' => $this->peak_hours_start, + 'peak_hours_end' => $this->peak_hours_end, + 'currency' => $this->currency, + 'duration_terms' => $this->duration_terms, + 'estimated_days' => $this->estimated_days, + 'updated_at' => $this->updated_at, + 'created_at' => $this->created_at, ]; } diff --git a/server/src/Models/ServiceRate.php b/server/src/Models/ServiceRate.php index bff0f2a3..2e37d63e 100644 --- a/server/src/Models/ServiceRate.php +++ b/server/src/Models/ServiceRate.php @@ -53,6 +53,7 @@ class ServiceRate extends Model 'company_uuid', 'service_area_uuid', 'zone_uuid', + 'order_config_uuid', 'service_name', 'service_type', 'per_meter_flat_rate_fee', @@ -124,6 +125,14 @@ public function parcelFees() return $this->hasMany(ServiceRateParcelFee::class); } + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function orderConfig() + { + return $this->belongsTo(OrderConfig::class)->withTrashed(); + } + /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ diff --git a/translations/en-us.yaml b/translations/en-us.yaml index 22613a60..19f39580 100644 --- a/translations/en-us.yaml +++ b/translations/en-us.yaml @@ -139,6 +139,11 @@ fleet-ops: code: Code status: Status details: Details + proof-of-delivery: Proof of Delivery + require-pod: Requires Proof of Delivery + require-pod-help-text: This activity requires a proof to be created before it can be applied to the order. + select-pod-method: Proof of Delivery Method + pod-method-placeholder: Select Proof of Delivery Method completes-order: Completes Order complete: Complete key-help: "Enter a key used for programmatic decision-making. This key can represent one or several activities with the same identifier. Note: This key is not required to be unique." @@ -1406,7 +1411,7 @@ fleet-ops: service-name: Service Name service-name-help-text: Display name for this service. service-order-label: Service Order Type - service-order-help-text: Restrict this service rate to an explicit `order type`. + service-order-help-text: Select an order configuration to which this service rate will be exclusively applied. This ensures that only orders created with the selected order configuration can utilize this specific service rate. service-order-placeholder: Select order type to restrict service to base-fee-label: Base Fee base-fee-help-text: Set a base fee which represents the minimum cost for this serice.