Merge pull request #245 from fleetbase/dev-v0.6.49 #2

Manually merged
steve.vandeheuvel merged 125 commits from steve.vandeheuvel/Fleetops Upgrade into tac 2026-05-28 10:01:10 +02:00 AGit

v0.6.49

v0.6.49
Introduces a full abstraction layer that decouples FleetOps from
Leaflet-specific APIs, enabling multiple map providers to coexist
and be selected at runtime. Google Maps is implemented as the first
alternative provider with 1-to-1 feature parity.

## New files

### Core abstraction
- addon/services/map-adapter-interface.js
  Abstract base class defining the ~30-method contract all adapters
  must implement (viewport, markers, overlays, drawing, popups,
  context menus, events, utilities).

- addon/services/map-manager.js
  Provider-agnostic central service that replaces leaflet-map-manager
  as the single point of contact for all map operations. Resolves the
  correct adapter from config/environment or runtime settings, then
  delegates every call. Exposes setActiveProvider() for runtime
  switching and waitForMap() for async initialisation.

### Adapters
- addon/services/map-adapter/leaflet.js
  Wraps the existing Leaflet/ember-leaflet implementation. Preserves
  all existing behaviour: L.Marker with leaflet-marker-rotate,
  L.Polyline routing, leaflet-draw for geofences, leaflet-contextmenu,
  slideTo() smooth animation, and layer visibility panes.

- addon/services/map-adapter/google.js
  Full Google Maps JavaScript API adapter. Implements every interface
  method using the Maps JS API v3:
  - Smooth marker animation via requestAnimationFrame interpolation
    (equivalent to Leaflet's slideTo)
  - Marker rotation via CSS transform on the icon element
  - google.maps.drawing.DrawingManager for geofence creation/editing
  - google.maps.InfoWindow-based context menus
  - google.maps.Polyline for route overlays
  - google.maps.Polygon / Circle for service area overlays
  - Traffic and Transit layer support
  - Map type switching (roadmap/satellite/hybrid/terrain)

### Components
- addon/components/map/google-live-map.{js,hbs}
  Google Maps surface component. Initialised imperatively via
  did-insert. Mirrors all features of leaflet-live-map: driver/vehicle
  markers, service area polygons, route polylines, position playback,
  context menus, and drawing tools.

- addon/components/map-settings.{js,hbs}
  Admin settings panel for selecting the map provider and configuring
  Google Maps options (API key, map type, traffic/transit layers).
  Settings are persisted via the new fleet-ops/settings/map API.

## Modified files

### Components
- addon/components/map/leaflet-live-map.{js,hbs}
  Injects mapManager service alongside existing services. Template
  conditionally renders Map::GoogleLiveMap or the existing Leaflet
  branch based on mapManager.isGoogleMaps. All marker/polygon
  registration calls now go through mapManager so both adapters
  receive the same data.

### Services
- addon/services/movement-tracker.js
  Replaces direct leafletLayer access with mapManager.updateMarkerPosition()
  and mapManager.setMarkerRotation(). Falls back gracefully when no
  marker is registered for a given model.

- addon/services/position-playback.js
  Replaces marker.slideTo() / marker.setRotationAngle() with
  mapManager.updateMarkerPosition() / mapManager.setMarkerRotation().
  The playback task is now provider-agnostic.

- addon/services/geofence.js
  Replaces leafletMapManager.showDrawControl() / hideDrawControl() /
  editPolygon() with mapManager equivalents.

### Configuration
- config/environment.js
  Adds mapProvider (default: 'leaflet'), googleMapsApiKey, and
  googleMapsLibraries config keys. All values are read from env vars
  (MAP_PROVIDER, GOOGLE_MAPS_API_KEY, GOOGLE_MAPS_LIBRARIES).

- index.js
  Adds contentFor('head') hook that injects the Google Maps JS API
  script tag when mapProvider === 'google' and a key is configured.

### Server
- server/src/Http/Controllers/Internal/v1/SettingController.php
  Adds getMapSettings() and saveMapSettings() methods. The Google Maps
  API key is stored in a separate protected setting key and is never
  returned in GET responses.

- server/src/routes.php
  Registers GET/POST fleet-ops/settings/map routes.

### i18n
- translations/en-us.yaml
  Adds map-settings.* translation keys for the new settings panel.

## App re-exports
- app/services/map-manager.js
- app/services/map-adapter-interface.js
- app/services/map-adapter/leaflet.js
- app/services/map-adapter/google.js
- app/components/map-settings.js
- app/components/map/google-live-map.js

## How to enable Google Maps

1. Set MAP_PROVIDER=google in your .env file
2. Set GOOGLE_MAPS_API_KEY=<your-key> in your .env file
3. Ensure the key has these APIs enabled in Google Cloud Console:
   - Maps JavaScript API
   - Drawing Library
   - Geometry Library
   - Geocoding API
4. Rebuild the frontend (pnpm build)

Or configure at runtime via Settings > Map Settings in the FleetOps
admin panel (no rebuild required for provider toggle).

## Adding a new map provider

1. Create addon/services/map-adapter/<name>.js extending MapAdapterInterface
2. Create app/services/map-adapter/<name>.js re-export
3. Set mapProvider: '<name>' in config/environment.js
4. No changes to any component or service are required
Wires the previously-created MapSettings component into the FleetOps
settings section as a first-class route, following the exact same
patterns used by the existing Routing and Notifications settings pages.

## New files

### Route layer
- addon/routes/settings/map.js
  Minimal Ember route class (matches settings/routing.js pattern).

- addon/controllers/settings/map.js
  Full controller with @tracked state for mapProvider, googleMapsApiKey,
  googleMapsMapType, googleMapsTrafficLayer, googleMapsTransitLayer.
  Implements getSettings and saveSettings ember-concurrency tasks that
  call the fleet-ops/settings/map API endpoints added in the previous
  commit. Applies the new provider to the live mapManager on save so
  the map switches without a page reload.

- addon/templates/settings/map.hbs
  Full settings page template following the Layout::Section::Header +
  Layout::Section::Body + ContentPanel pattern used by all other
  settings routes. Includes:
  - Save button in the header (disabled while tasks run)
  - Map provider Select (Leaflet / Google Maps)
  - Conditional Google Maps options panel:
    - API key password input
    - Map type Select (roadmap / satellite / hybrid / terrain)
    - Traffic layer Toggle
    - Transit layer Toggle
    - Required Google Cloud APIs info box
  - Loading spinner while getSettings is running
  - RegistryYield extension point for third-party panels

### App re-exports (3 files)
- app/routes/settings/map.js
- app/controllers/settings/map.js
- app/templates/settings/map.js

## Modified files

### Router
- addon/routes.js
  Adds this.route('map') inside the settings route group, between
  routing and payments (alphabetical / logical order).

### Sidebar
- addon/components/layout/fleet-ops-sidebar.js
  Adds a Map Settings item to the settingsItems array:
  - intl key: menu.map
  - icon: map
  - route: settings.map
  - permission: fleet-ops view map-settings
  Inserted between Routing and Custom Fields to maintain logical order.

### Translations
- translations/en-us.yaml
  Adds two new translation blocks:
  1. menu.map: "Map" — sidebar label
  2. settings.map.* — 20 keys covering all labels, help texts,
     placeholders, and status messages used by the new route template.
     Keys mirror the settings.routing.* structure for consistency.
The Google Maps API key is already managed at the system admin level
through the core-api Settings → Services panel, which stores it at
`config('services.google_maps.api_key')` via `Setting::configureSystem`.
FleetOps should not duplicate this responsibility.

## Backend (server/src/Http/Controllers/Internal/v1/SettingController.php)

`getMapSettings`
- Removed the separate `fleet-ops.map-settings.google-api-key` lookup.
- Now reads `config('services.google_maps.api_key', env('GOOGLE_MAPS_API_KEY', ''))`
  — the same key managed by core-api — and includes it in the response
  so the frontend Google Maps adapter can initialise correctly.
- Single source of truth: the key lives only in the system-level services
  config; FleetOps reads but never stores it.

`saveMapSettings`
- Removed all API key acceptance and storage logic.
- Any `googleMapsApiKey` field sent by a client is silently stripped
  before the settings blob is persisted, preventing accidental storage.
- Comment updated to make the delegation to core-api explicit.

## Frontend (addon/controllers/settings/map.js)

- Removed `@tracked googleMapsApiKey` property.
- Removed `onApiKeyChange` action.
- Removed the conditional API key inclusion from the `saveSettings` task
  payload — the key is no longer sent to the server from this page.
- Removed the stale comment about the key not being returned by the server
  (it now is returned, sourced from the system config).
- Fixed the `notifications.success` call to use the correct `settings.map.*`
  translation key namespace (was incorrectly referencing `map-settings.*`).

## Frontend (addon/templates/settings/map.hbs)

- Removed the API key `<InputGroup>` block (password input + help text).
- Removed the "Required Google Cloud APIs" info box — this information
  belongs in the core-api admin panel where the key is configured.
- The Google Maps conditional section now shows only the map type selector
  and the traffic/transit layer toggles.

## Translations (translations/en-us.yaml)

Removed 8 translation keys from the `settings.map` block that were
exclusively used by the now-deleted API key input and info box:
- `google-maps-api-key`
- `google-maps-api-key-help-text`
- `google-maps-api-key-placeholder`
- `google-api-key-requirements-title`
- `google-api-key-requirement-maps-js`
- `google-api-key-requirement-drawing`
- `google-api-key-requirement-geometry`
- `google-api-key-requirement-geocoding`
The standalone MapSettings component (addon/components/map-settings.js,
addon/components/map-settings.hbs, app/components/map-settings.js) was
never invoked in any template or imported by any other file. Its
functionality is fully covered by the proper route-based implementation
at addon/templates/settings/map.hbs + addon/controllers/settings/map.js.

Also removes the entire top-level map-settings: block from
translations/en-us.yaml (22 lines). All active translation keys for the
map settings page now live under the settings.map.* namespace, which is
consistent with every other settings section in FleetOps.
- introduce provider-agnostic route engine and adapter-owned routing controls for Google and Leaflet

- add admin routing and map settings surfaces, including Google map ID configuration and engine-specific settings wiring

- improve order route visualization with shared waypoint semantics, status-driven polylines, and consistent route list markers

- harden public payload/order route updates, map overlays, OSRM/Google routing integration, and supporting map/geofence services
feat: Provider-agnostic map architecture with Google Maps support
bump version v0.6.46
Normalize order customer polymorphic type
Add orchestrator consumable API and capacity allocation
Implement order tracking intelligence providers
Fix customer contact user type invariant
Fix unassigned tracking driver state
v0.6.48
Add VROOM capacity-only vehicle packing bias
Improve FleetOps live map, alerts, and telematics providers
Pass order resource to form registry
v0.6.49
Introduces a B2C customer surface directly in FleetOps so portals can
authenticate end-customers with the existing flb_live_… API credential —
no Storefront publishable-key + Store/Network coupling required.

Endpoints (all under /v1/customers/...):

Public (API credential only):
  POST request-creation-code   send email/SMS verification code
  POST /                       create Contact+User after verifying code
  POST login                   email/phone + password → Sanctum token
  POST login-with-sms          send login code (SMS, falls back to email)
  POST verify-code             verify code → Sanctum token
  POST forgot-password         send reset code
  POST reset-password          verify reset code + set new password

Authenticated (require Customer-Token):
  GET  me / PUT me             profile read/update (mirrors to linked User)
  POST logout / logout-all     revoke current / all tokens for this user
  GET  orders                  scoped Order::where('customer_uuid', …)
  POST orders                  create freight order with customer_uuid set
  GET  orders/{id}             owner-checked order detail
  GET  places                  customer's saved Places
  POST register-device         push-token registration for linked User

Implementation:
- Tokens are Sanctum PersonalAccessToken with `name` = Contact UUID
  (matches Storefront convention so SDKs and headers are interchangeable).
- AuthenticateCustomerToken middleware verifies the Customer-Token header
  and cross-checks the resolved Contact's company_uuid against the API
  credential's session('company'); 401/403 on mismatch.
- CustomerAuth helper resolves the token with a company-preferred
  fallback for the multi-company edge case.
- Customer model is a thin Contact specialization with type=customer.
- Verification slugs are fleetops_* (create_customer, customer_login,
  customer_password_reset) — no Storefront slugs reused.
- No new tables, no migrations: contacts.email/phone/user_uuid,
  orders.customer_uuid, and personal_access_tokens already cover it.
- OAuth providers (Apple/Google/Facebook) deferred to a follow-up.

Static-shape tests live in server/tests/CustomerEndpointTest.php and
follow the package's existing pest convention. End-to-end HTTP tests
belong in the parent api/ harness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related bugs in the freshly-added v1/customers endpoints surfaced on
first live test (HAR shows: 400 from /v1/customers/request-creation-code
with "Attempt to read property 'name' on null (View:
.../mail/verification.blade.php)").

Bug 1: requestCreationCode passed an unsaved Contact as the verification
subject, so the morphTo subject_uuid was null. The verification mail
template references {{ $user->name }} via the morphTo relation, which
resolves null → fatal in blade.

Fix: look up an existing User by identity, or stub-create one with
`name = "Pending Customer"`. The stub gives the mail renderer a real
record to greet, and create() backfills name + password on verification.

Bug 2: `password` and `type` are guarded on the User model, so
`User::create([... 'password' => ..., 'type' => 'customer'])` silently
dropped both fields. Customers created via signup would therefore have
no password and no type — login would fail.

Fix: assign password via `$user->password = $plaintext` (the model's
setPasswordAttribute mutator hashes) and set type via `setUserType()`.
Also stop double-hashing in resetPassword (was Hash::make then mutator).

Also makes create() idempotent on the Contact: reuse the existing
customer-Contact for (user, company) instead of crashing on the second
signup attempt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The signup form already collected the customer's name (and phone) by the
time it asked for a verification code. Plumb those through so:
  - the verification email greets the customer by their real name
  - the pre-created User row holds real values instead of the
    "Pending Customer" placeholder
  - create() doesn't need to overwrite stub values on confirmation

VerifyCreateCustomerRequest now accepts optional `name` and `phone`;
requestCreationCode uses them when stub-creating the User, and refreshes
the row's name when an existing pending stub is re-prompted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When `POST /v1/customers` receives a home-address payload (either as a
top-level `address` object or nested under `meta.address`), create a
Place record:
  - company_uuid = the API credential's company
  - owner_uuid / owner_type = the new customer Contact (polymorphic)
  - type = "residential"
  - Both Storefront-style (street1/province/postal_code) and
    portal-form-style (line1/state/zip) keys are accepted.

Then link the Place to the customer via Contact.place_uuid so it appears
as the default address and `GET /v1/customers/places` returns it.

Idempotent: only creates a Place when the Contact has no place_uuid set
and the payload has at least one usable address field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This is a public Fleetbase API surface — it must follow established model
field shapes, not invent client-specific aliases. Audited and removed
every divergence I'd introduced:

- `create()` and the `place` field — drop the line1/line2/state/zip
  aliases. Accept only the canonical Place fillable shape (street1,
  street2, city, province, postal_code, neighborhood, district, building,
  country, phone, meta) or an existing Place public_id. Field whitelist
  via `array_intersect_key` so unknown keys are silently ignored.

- Drop `meta.origin = 'fleetops_customer_portal'` invention from
  create(), login(), and verifyCode(). Storefront uses meta.storefront_id
  legitimately; FleetOps customers don't need a portal-specific tag.

- `createOrder()` — full rewrite to mirror `OrderController::create`'s
  canonical Order shape. Accepts the same fields the operator API does:
  type / order_config / scheduled_at / notes / meta / internal_id, and
  either a `payload` (object or public_id) or top-level pickup / dropoff /
  return / waypoints / entities. No top-level `item`, `weight`, `value`,
  `mode`, `delivery`, or `category` aliases — clients translate their
  form shapes into entities + meta before calling.

  The customer endpoint *forces* customer_uuid from the Customer-Token,
  status='created', and ignores any client-supplied customer / driver /
  vehicle / facilitator / dispatch fields. Payload-building delegates to
  `Payload::setPickup` / `setDropoff` / `setEntities` so customer-created
  orders are indistinguishable from operator-created ones at the data
  layer.

- CreateCustomerOrderRequest validates only the canonical fields.

- CustomerEndpointTest gains a "no client-portal field aliases" assertion
  that fails the build if any of those forbidden patterns reappear in
  the controller source, plus a "createOrder mirrors the canonical
  Fleet-Ops order shape" assertion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Make the principle explicit at the only meta write that future contributors
might be tempted to touch: customer creation. `meta` is client-owned;
controller-side stamps are only justified when the backend itself reads
that data back (e.g. Storefront's `meta.storefront_id` for query scoping).
The customer surface has no such backend need.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two additive, canonical extensions to the public Fleet-Ops API surface,
both convention-aligned with existing resources:

1. OrderConfigs as a first-class read-only public resource (mirrors how
   Places, Vendors, Contacts, Orders are already exposed):

     GET /v1/order-configs        — list configs for the company
     GET /v1/order-configs/{id}   — find by uuid|public_id|namespace|key

   `find()` defers to `OrderConfig::resolveFromIdentifier()` so callers
   can use `/transport`, the namespace, the public_id, or the uuid.

   A new `v1/OrderConfig` resource projects only the public-safe shape:
   id, key, name, namespace, description, tags, status, version, and the
   activity `flow[]` carrying `{code, status, details, color, complete,
   pod_method, require_pod}`. Internal-only fields (raw entities JSON,
   flow logic blocks) are filtered out so the public response is small,
   safe, and useful for drivers/portals/integrations that need to render
   status chips and activity labels from the canonical config.

2. `company` sub-object on the `Customer` resource (returned by
   /v1/customers/me, /login, /signup, /verify-code, etc.). Resolves
   currency through the existing canonical helper
   `Utils::getCompanyTransactionCurrency()` which already does the
   `companies.currency` → ledger `base_currency` → "USD" chain. The
   sub-object exposes id, name, currency, country, phone — same fields
   any caller could already discover via other channels, just bundled
   conveniently so authenticated customer apps don't need a separate
   request to render currency labels or contact info.

No new write surface, no new auth requirements, no client-portal
aliases. Both endpoints require only the public Fleet-Ops API key, like
the existing /v1/tracking-numbers/{n} public read endpoint.

Static-shape tests assert:
  - order-configs routes register both methods
  - OrderConfigController exposes only read-only methods (no create/
    update/delete on the public surface)
  - OrderConfig resource emits `flow[]` with the canonical keys and
    never exposes the raw entities JSON
  - Customer resource exposes `company` and uses
    `Utils::getCompanyTransactionCurrency`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an optional `service_quote` field to the customer order create
endpoint, matching how `OrderController::create` resolves quotes:

  - CreateCustomerOrderRequest accepts a `service_quote` string (uuid or
    `sqte_…` public_id).
  - `createOrder()` resolves it via `ServiceQuote::resolveFromRequest`
    and calls `$order->purchaseServiceQuote()` after creation so the
    PurchaseRate is locked onto the order with the quoted pricing.

This lets customer portals pull live quotes from
`GET /v1/service-quotes`, present them to the customer, and submit the
chosen one when creating the order — same flow operators use.

No write-surface changes beyond accepting one more optional field; all
existing tests + canonical-shape assertions still hold.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add public customer API + OrderConfig resource + Customer.company sub-object
Add multi-zone distance service rates
Add required indicators to order form
Harden live map viewport loading
Use available driver and vehicle status defaults
v0.6.50
steve.vandeheuvel manually merged commit a1171e3594 into tac 2026-05-28 10:01:10 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
tac/fleetops!2
No description provided.