Skip to content

Service Configuration UI

Frontend screens, components, and architecture for the Service Configuration module.

Route Structure

/services              → Service list (DataTable)
/services/:id          → Service detail / configuration (single page, 7 sections)

The route is defined in client/src/routes/index.js as a dynamic segment [id], resolved by the route factory to views/services/[id]/index.vue.

Page Layout

The service detail page uses a scroll-spy navigation pattern — all 7 sections render as cards on a single page, with a fixed NavPill bar for quick navigation.

┌──────────────────────────────────────────────────────────────┐
│  SectionTitle (fixed)                                         │
│  ┌──────────────────────────────────────────────────────────┐│
│  │ DetailsV2: Logo | Carrier Name | Service Code | Status  ││
│  └──────────────────────────────────────────────────────────┘│
│  ┌──────────────────────────────────────────────────────────┐│
│  │ NavPill: [Taxes][Costs][Pricing][Volumetric][Zones]      ││
│  │          [Additional Services][Additional Charges]        ││
│  └──────────────────────────────────────────────────────────┘│
├──────────────────────────────────────────────────────────────┤
│                                                               │
│  #taxes       → <Taxes />                                    │
│  #costs       → <Costs />                                    │
│  #pricing     → <Pricing />                                  │
│  #volumetric  → <Volumetric />                               │
│  #zones       → <Zones />                                    │
│  #additional-services → <AdditionalServices />               │
│  #additional-charges  → <AdditionalCharges />                │
│                                                               │
└──────────────────────────────────────────────────────────────┘

Components are loaded eagerly via import.meta.glob('./components/*.vue', { eager: true }) and rendered dynamically using <component :is="...">.

Component Tree

views/services/[id]/
├── index.vue                              # Page container, NavPill, service header
└── components/
    ├── Taxes.vue                          # Tax rules display (read-only)
    ├── Costs.vue                          # Operational cost matrix
    ├── Pricing.vue                        # Pricing matrix (4 tabs)
    ├── Volumetric.vue                     # Volumetric factor select
    ├── Zones.vue                          # Default rates table
    ├── AdditionalServices.vue             # Additional services table (mandatory=false)
    ├── AdditionalCharges.vue              # Additional charges table (mandatory=true)
    └── Modals/
        ├── Bulk.modal.vue                 # Bulk cell operations (costs & pricing)
        ├── UpgradePricing.modal.vue       # Recalculate pricing from costs
        └── AdditionalService.offcanvas.vue # Add/edit additional service or charge

Store

client/src/stores/views/services.store.js

useServicesStore (Pinia) holds the service detail state. Minimal store — sections manage their own loading and save logic.

PropertyTypeDescription
serviceDetailsObjectService data: { id, carrier, service_code, locale_id, costs, pricing, taxes, volumetric_factor, ... }
setServiceDetails(data)FunctionMerge data into serviceDetails
setKeyValue(key, value)FunctionSet a single key on serviceDetails
getServiceDetails()FunctionReturn current serviceDetails

Sections use setKeyValue to update their slice of the store after API calls (e.g., setKeyValue('costs', response.data)).

Simple Sections

Taxes (Taxes.vue)

Read-only display using DetailsV2 cards. Each card represents a tax rule for the service's locale.

Field DisplayedSource
Country nametax.country_name
Rule descriptiontax.rule_description
Concepttax.concept_description
Application typetax.application_type
Rate (%)tax.amount (formatted with percentage())

Volumetric (Volumetric.vue)

A Select2 dropdown populated from GET /catalog/volumetric-factors. On change, confirms via Swal.question() and calls PATCH /services/{id}/volumetric-factor.

The formula example ((L × W × H) / factor) updates in the subtitle via a computed from the store.

Zones (Zones.vue)

A SimpleTable with zone rows and a Select2 per row for tariff type selection.

  • Zones are sourced from the cost matrix (serviceDetails.costs.zones)
  • If costs aren't loaded yet, falls back to GET /services/{id}/costs to fetch zones
  • Tariff type options: Basic (1), Pro (2), Enterprise (3), Corporate (4), or no tariff
  • All zones must have a tariff assigned before saving (validation)
  • Cancel button reverts to last saved state

Service Layer

client/src/services/services.service.js

Registered as api.services. Methods used by the service detail page:

MethodEndpointUsed By
getDetailsInfo(id)GET /services/{id}/dataindex.vue
getServiceTaxes(id)GET /services/{id}/taxesTaxes.vue
getServiceCosts(id)GET /services/{id}/costsCosts.vue, Zones.vue
saveServiceCosts(id, data)PUT /services/{id}/costsCosts.vue
getServicePricing(id)GET /services/{id}/pricePricing.vue
saveServicePricing(id, data)PUT /services/{id}/pricePricing.vue
updateServiceVolumetricFactor(id, factorId)PATCH /services/{id}/volumetric-factorVolumetric.vue
getServiceDefaultRates(id)GET /services/{id}/default-ratesZones.vue
saveServiceDefaultRates(id, data)PUT /services/{id}/default-ratesZones.vue
getAdditionalServices(id, params)GET /services/{id}/additionalsAdditionalServices.vue, AdditionalCharges.vue
toggleAdditionalServiceActive(id, itemId, active)PATCH /services/{id}/additionals/{itemId}AdditionalServices.vue, AdditionalCharges.vue
createAdditionalService(id, data)POST /services/{id}/additionalsAdditionalService.offcanvas.vue
updateAdditionalService(id, itemId, data)PATCH /services/{id}/additionals/{itemId}AdditionalService.offcanvas.vue

Key Patterns

  • Scroll-spy navigation: NavPill with scroll-offset-top and CSS scroll-margin-top for fixed header offset
  • Dynamic component rendering: import.meta.glob + <component :is> for section cards
  • Shared store: All sections read/write to the same Pinia store via setKeyValue
  • Swal.question(): Confirmation dialogs before save/update operations
  • $toast: Success/error notifications after API calls
  • v-can directive: Permission-based visibility for save buttons

Envia Admin