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 chargeStore
client/src/stores/views/services.store.jsuseServicesStore (Pinia) holds the service detail state. Minimal store — sections manage their own loading and save logic.
| Property | Type | Description |
|---|---|---|
serviceDetails | Object | Service data: { id, carrier, service_code, locale_id, costs, pricing, taxes, volumetric_factor, ... } |
setServiceDetails(data) | Function | Merge data into serviceDetails |
setKeyValue(key, value) | Function | Set a single key on serviceDetails |
getServiceDetails() | Function | Return 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 Displayed | Source |
|---|---|
| Country name | tax.country_name |
| Rule description | tax.rule_description |
| Concept | tax.concept_description |
| Application type | tax.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}/coststo 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.jsRegistered as api.services. Methods used by the service detail page:
| Method | Endpoint | Used By |
|---|---|---|
getDetailsInfo(id) | GET /services/{id}/data | index.vue |
getServiceTaxes(id) | GET /services/{id}/taxes | Taxes.vue |
getServiceCosts(id) | GET /services/{id}/costs | Costs.vue, Zones.vue |
saveServiceCosts(id, data) | PUT /services/{id}/costs | Costs.vue |
getServicePricing(id) | GET /services/{id}/price | Pricing.vue |
saveServicePricing(id, data) | PUT /services/{id}/price | Pricing.vue |
updateServiceVolumetricFactor(id, factorId) | PATCH /services/{id}/volumetric-factor | Volumetric.vue |
getServiceDefaultRates(id) | GET /services/{id}/default-rates | Zones.vue |
saveServiceDefaultRates(id, data) | PUT /services/{id}/default-rates | Zones.vue |
getAdditionalServices(id, params) | GET /services/{id}/additionals | AdditionalServices.vue, AdditionalCharges.vue |
toggleAdditionalServiceActive(id, itemId, active) | PATCH /services/{id}/additionals/{itemId} | AdditionalServices.vue, AdditionalCharges.vue |
createAdditionalService(id, data) | POST /services/{id}/additionals | AdditionalService.offcanvas.vue |
updateAdditionalService(id, itemId, data) | PATCH /services/{id}/additionals/{itemId} | AdditionalService.offcanvas.vue |
Key Patterns
- Scroll-spy navigation:
NavPillwithscroll-offset-topand CSSscroll-margin-topfor 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 callsv-candirective: Permission-based visibility for save buttons
