@madebylars.com/mbl-plan
v1.0.2
Published
Trip planning and multi-stop route management add-on for mbl-transport
Maintainers
Readme
@madebylars.com/mbl-plan
A premium Nuxt 4 module in the MadeByLars ecosystem that adds trip planning and multi-stop route management to mbl-transport.
Enable dispatchers to group multiple orders into a single trip for one vehicle and driver. A trip has an ordered list of stops — each stop is a pickup or delivery for a specific order.
Overview
mbl-plan is a feature-flagged add-on. It is only active for tenants
where a superadmin has enabled it. When inactive, mbl-transport behaves
exactly as today. When active, the order flow gains a new planned status
between confirmed and assigned.
Order flow
Without mbl-plan (default)
confirmed → assigned → collected → in_transit → delivered
With mbl-plan enabled for tenant
confirmed → planned → assigned → collected → in_transit → deliveredplanned means the order has been added to a trip but the trip has not yet
been dispatched to the driver. Once the dispatcher sends the trip, all orders
in it move to assigned simultaneously via an atomic Postgres RPC call.
Feature flag
The flag lives in tenant.tenants.features (jsonb column). Enable planning
for a tenant:
UPDATE tenant.tenants
SET features = '{"planning": true}'
WHERE id = '<tenant-uuid>';The Supabase custom_access_token_hook injects the features object into the
JWT so every client and server handler can check it without an extra DB query.
Installation
npm install @madebylars.com/mbl-planAdd the module to your nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@madebylars.com/mbl-plan'],
})The module auto-imports all composables and registers its locale strings under
the mblPlan.* namespace (requires @nuxtjs/i18n).
Database migration
Run migrations/003_add_planning.sql against your Supabase project. It:
- Adds
features jsonbtotenant.tenants - Adds
plannedto thetransport.ordersstatus check constraint - Creates
transport.tripsandtransport.trip_stopstables with indexes and RLS - Creates
transport.dispatch_trip(p_trip_id)— atomic dispatch RPC - Creates
transport.reorder_stops(p_trip_id, p_ordered_stop_ids)— atomic reorder RPC - Creates
transport.check_trip_completion()trigger — auto-completes a trip when all stops are marked done
Also extend custom_access_token_hook with the snippet in the migration file
to inject features into JWT claims.
Composables
All composables are auto-imported. They call the app's own /api/* endpoints —
never Supabase directly.
usePlanFeature()
Check whether the planning feature is enabled for the current tenant. Read from the JWT — no extra request.
const { isEnabled } = usePlanFeature()
// isEnabled: Ref<boolean>
if (isEnabled.value) {
// show planning UI
}useTrips(filters?)
Reactive list of trips. All filters are optional.
const { data, pending, error, refresh } = useTrips({
date: '2026-06-07', // ISO date — filter by planned_date
status: 'draft',
driverId: '...',
vehicleId: '...',
})
// data: Ref<Trip[]>useTrip(tripId)
Single trip with its ordered stop list, order summaries, driver, and vehicle.
const { data, pending, error, refresh } = useTrip(tripId)
// data: Ref<TripWithStops | null>useTripActions()
Imperative actions for creating and managing trips.
const { createTrip, updateTrip, deleteTrip, dispatchTrip, completeTrip } = useTripActions()
// Create a new draft trip
const trip = await createTrip({
vehicleId: '...',
driverId: '...',
plannedDate: '2026-06-07',
notes: 'Morning run',
})
// Atomically dispatch: sets trip → dispatched, all orders → assigned
await dispatchTrip(trip.id)dispatchTrip calls POST /api/trips/[id]/dispatch, which runs the
transport.dispatch_trip() Postgres function in a single transaction.
useStopActions()
Add, remove, reorder, and complete individual stops.
const { addStop, removeStop, reorderStops, completeStop } = useStopActions()
// Add a stop — creates both pickup and delivery stops for the order
// and moves the order to 'planned'
await addStop(tripId, orderId, 'pickup')
// Reorder — single RPC call, no race conditions
await reorderStops(tripId, ['stop-id-1', 'stop-id-3', 'stop-id-2'])
// Mark a stop done — triggers auto-completion when all stops are done
await completeStop(stopId)useDriverTrip()
Driver-facing composable. Returns the active dispatched trip for the current driver with stops ordered by sequence.
const { data, pending, error } = useDriverTrip()
// data: Ref<TripWithStops | null>
const { completeStop } = useStopActions()
await completeStop(stopId)TypeScript types
import type {
Trip,
TripStop,
TripWithStops,
TripStopWithOrder,
TripStatus, // 'draft' | 'dispatched' | 'in_progress' | 'completed'
StopType, // 'pickup' | 'delivery'
CreateTripInput,
UpdateTripInput,
TripsFilters,
OrderAddress,
DriverSummary,
VehicleSummary,
} from '@madebylars.com/mbl-plan'i18n
Locale strings are registered under the mblPlan.* namespace. English and
Swedish are included. Add more by extending @nuxtjs/i18n in the host app.
// Access in templates
$t('mblPlan.actions.dispatch') // 'Dispatch trip'
$t('mblPlan.status.in_progress') // 'In progress'
$t('mblPlan.feature.disabled') // 'Trip planning is not enabled for your account'| Key | en | sv |
|---|---|---|
| mblPlan.status.draft | Draft | Utkast |
| mblPlan.status.dispatched | Dispatched | Skickad |
| mblPlan.status.in_progress | In progress | Pågående |
| mblPlan.status.completed | Completed | Avslutad |
| mblPlan.stopType.pickup | Pickup | Upphämtning |
| mblPlan.stopType.delivery | Delivery | Leverans |
| mblPlan.actions.createTrip | Create trip | Skapa tur |
| mblPlan.actions.addStop | Add stop | Lägg till stopp |
| mblPlan.actions.dispatch | Dispatch trip | Skicka tur |
| mblPlan.actions.completeStop | Mark stop done | Markera stopp klart |
mbl-transport integration
API endpoints to implement
| Method | Path | Description |
|---|---|---|
| GET | /api/trips | List trips (?date, ?status, ?driverId, ?vehicleId) |
| POST | /api/trips | Create draft trip |
| GET | /api/trips/driver/active | Active trip for the authenticated driver |
| GET | /api/trips/[id] | Trip + stops + order summaries |
| PATCH | /api/trips/[id] | Update trip metadata (draft only) |
| DELETE | /api/trips/[id] | Delete trip (draft only) |
| POST | /api/trips/[id]/dispatch | Atomic dispatch via transport.dispatch_trip() RPC |
| POST | /api/trips/[id]/complete | Mark trip completed |
| POST | /api/trips/[id]/stops | Add stop (creates pickup + delivery, moves order → planned) |
| DELETE | /api/trips/[id]/stops/[stopId] | Remove a stop |
| POST | /api/trips/[id]/stops/reorder | Reorder via transport.reorder_stops() RPC |
| POST | /api/stops/[stopId]/complete | Mark stop done |
| PATCH | /api/admin/tenants/[id]/features | Superadmin — toggle feature flags |
New pages
/dispatcher/planning — gated behind usePlanFeature().isEnabled
- Left panel: confirmed orders for today/upcoming dates with "Add to trip" button
- Right panel: trip builder with vehicle/driver/date selector, stop list with up/down reorder, and "Dispatch" button (disabled until ≥1 stop)
- Centre:
mbl-fleetmapcomponent with numbered stop pins in sequence order
/driver/trip — gated behind usePlanFeature().isEnabled
- Trip header (vehicle, planned date, total stops)
- Ordered stop list with type badge, address, order ref, "Mark done" button
- Completed stops shown with strikethrough and timestamp
- Map showing remaining stops only
Modified pages
/admin/tenants/[id] — add a toggle to enable/disable trip planning per tenant. Calls PATCH /api/admin/tenants/[id]/features. JWT picks up the change on the user's next session refresh.
/orders/[id] — when an order has status planned or was assigned via a trip, show a planning context block:
This order is part of trip #TRP-0012
Vehicle: Ford Transit — AB 123 CD | Driver: Erik Svensson
Planned: 3 stops — stop 2 of 3Order status badges and filter dropdowns — add planned as a display value using mblPlan.status i18n strings.
What does NOT change
mbl-auth,mbl-order,mbl-whereabout,mbl-fleetmap— untouched- The
assigned → collected → in_transit → deliveredflow is unchanged for both planned and unplanned orders - Tenants without
features.planning: truenever see theplannedstatus or any planning UI
File structure
src/
module.ts # Module definition, composable auto-imports, i18n hook
types.ts # All exported TypeScript types
runtime/
composables/
usePlanFeature.ts # Feature flag from JWT
useTrips.ts # Reactive trip list
useTrip.ts # Single trip with stops
useTripActions.ts # Create / update / delete / dispatch / complete
useStopActions.ts # Add / remove / reorder / complete stops
useDriverTrip.ts # Driver-facing active trip
locales/
en.ts # English strings
sv.ts # Swedish strings
migrations/
003_add_planning.sql # Run once against the target Supabase projectDevelopment
# Install dependencies
npm install
# Generate type stubs
npm run dev:prepare
# Develop with the playground
npm run dev
# Build the playground
npm run dev:build
# Run ESLint
npm run lint
# Run Vitest
npm run test
npm run test:watch
# Release new version
npm run release