16 KiB
Phase 3 Step 2: Smart Components Layer (Domain Fields)
Status: 🚀 START
Phase: 3 / 11
Step: 2 / 4
Target Completion: 2026-09-02 (4 days)
Overall Phase 3: 2 weeks (2026-08-27 to 2026-09-10)
Overview
Phase 3 Step 2 Goal: Build Smart Components layer (Domain Fields)
What are Domain Fields?
- Composed from Typed Fields + business logic
- Add API lookups (customer list, product catalog, etc.)
- Implement domain-specific validation rules
- Enable complex workflows (multi-field coordination)
- Example: OrderLineField = Qty (NumberField) + Product (lookup) + Price (auto-calculated)
Architecture Transition:
Layer 1: Primitives (Button, Input, Table)
↓ (composed into)
Layer 2: Typed Fields (TextField, NumberField, DateField)
↓ (composed into)
Layer 3: Domain Fields ← WE ARE HERE (OrderLineField, CustomerField)
↓ (composed into)
Layer 4: Business Composites (OrderForm, InventoryTransfer)
After Step 2: Full Domain Fields layer ready for Composite components
The 12 Domain Fields
1. OrderLineField — Order line item (qty + product + price)
<template>
<div class="order-line-group">
<!-- Product Lookup -->
<SelectField
v-model="line.productId"
:options="productOptions"
label="Product"
@update:modelValue="handleProductChange"
/>
<!-- Quantity (auto-calculates available) -->
<NumberField
v-model="line.quantity"
label="Quantity"
:max="availableQuantity"
@blur="calculateTotal"
/>
<!-- Unit Price (auto-filled from product) -->
<CurrencyField
v-model="line.unitPrice"
label="Unit Price"
disabled
/>
<!-- Total Line Amount (auto-calculated) -->
<CurrencyField
v-model="line.lineTotal"
label="Line Total"
disabled
/>
</div>
</template>
<script setup lang="ts">
interface OrderLine {
productId: string
quantity: number
unitPrice: number
lineTotal: number
}
const props = defineProps<{
modelValue: OrderLine
availableProducts?: any[]
}>()
const line = ref({...props.modelValue})
const productOptions = ref([])
const handleProductChange = async (productId: string) => {
// Fetch product details from API
const product = await productsApi.getProduct(productId)
line.value.unitPrice = product.price
calculateTotal()
}
const calculateTotal = () => {
line.value.lineTotal = line.value.quantity * line.value.unitPrice
emit('update:modelValue', line.value)
}
</script>
Features:
- Product lookup (async)
- Quantity validation (available stock)
- Auto-fill unit price from product
- Auto-calculate line total
- Emit coordinated updates
2. CustomerField — Customer lookup with details
<template>
<div class="customer-field">
<!-- Customer Autocomplete -->
<SelectField
v-model="customerId"
:options="customerOptions"
label="Customer"
searchable
async
@search="searchCustomers"
/>
<!-- Auto-filled Details -->
<TextField
:value="customer.name"
label="Company Name"
disabled
/>
<EmailField
:value="customer.email"
label="Email"
disabled
/>
<PhoneField
:value="customer.phone"
label="Contact Phone"
disabled
/>
<!-- Credit Limit Warning -->
<div v-if="customerAtRisk" class="alert alert-warning">
⚠️ Customer approaching credit limit: {{ creditUsage }}%
</div>
</div>
</template>
<script setup lang="ts">
const customerId = ref('')
const customer = ref({})
const customerOptions = ref([])
const searchCustomers = async (query: string) => {
const results = await customersApi.searchCustomers(query)
customerOptions.value = results.map(c => ({ value: c.id, label: c.name }))
}
const handleCustomerSelect = async (id: string) => {
const customerData = await customersApi.getCustomer(id)
customer.value = customerData
checkCreditLimit(customerData)
emit('update:modelValue', { customerId: id, ...customerData })
}
</script>
Features:
- Async customer search
- Auto-load customer details
- Display contact info
- Credit limit warning
- Emit coordinated data
3. ProductField — Product lookup with details
<template>
<div class="product-field">
<!-- SKU Lookup -->
<TextField
v-model="sku"
label="Product SKU"
@blur="lookupProduct"
/>
<!-- Auto-filled Details -->
<TextField
:value="product.name"
label="Product Name"
disabled
/>
<SelectField
:value="product.categoryId"
:options="categories"
label="Category"
disabled
/>
<CurrencyField
:value="product.price"
label="List Price"
disabled
/>
<!-- Stock Availability -->
<NumberField
:value="availableStock"
label="Available Stock"
disabled
/>
</div>
</template>
<script setup lang="ts">
const sku = ref('')
const product = ref({})
const lookupProduct = async () => {
if (!sku.value) return
const p = await productsApi.getProductBySku(sku.value)
if (!p) {
errorMessage.value = 'Product not found'
return
}
product.value = p
checkStock(p.id)
emit('update:modelValue', p)
}
</script>
Features:
- SKU-based lookup
- Auto-load product details
- Display category, price, stock
- Validation (product exists)
4. WarehouseField — Warehouse selection with capacity
<template>
<div class="warehouse-field">
<SelectField
v-model="warehouseId"
:options="warehouseOptions"
label="Warehouse"
@update:modelValue="handleWarehouseChange"
/>
<!-- Display Warehouse Info -->
<div v-if="warehouse" class="warehouse-info">
<TextField :value="warehouse.location" label="Location" disabled />
<TextField :value="warehouse.capacity" label="Total Capacity" disabled />
<NumberField :value="warehouse.usedCapacity" label="Used Capacity" disabled />
<!-- Capacity Bar -->
<div class="progress mt-2">
<div
class="progress-bar"
:class="capacityClass"
:style="{ width: capacityPercent + '%' }"
>
{{ capacityPercent }}%
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const warehouseId = ref('')
const warehouse = ref(null)
const warehouseOptions = ref([])
onMounted(async () => {
const whs = await warehousesApi.listWarehouses()
warehouseOptions.value = whs.map(w => ({ value: w.id, label: w.name }))
})
const handleWarehouseChange = async (id: string) => {
warehouse.value = await warehousesApi.getWarehouse(id)
emit('update:modelValue', warehouse.value)
}
const capacityClass = computed(() => {
const percent = capacityPercent.value
if (percent > 90) return 'bg-danger'
if (percent > 70) return 'bg-warning'
return 'bg-success'
})
</script>
Features:
- Warehouse selection
- Display location, capacity
- Capacity usage visualization
- Color-coded status
5. SupplierField — Supplier lookup
<!-- Company name + Contact + Payment terms -->
6. StockTransferField — From/To warehouse transfer
<!-- From warehouse + To warehouse + Quantity + Transfer reason -->
7. DateRangeField — Start + End date pair
<!-- Start date + End date with validation (start < end) -->
8. AddressField — Full address with country
<!-- Street + City + Postal + Country + Validation -->
9. BankAccountField — Account + Bank code
<!-- Account number + Bank code + Account holder name -->
10. TaxIDField — Country-specific tax ID
<!-- Tax ID with country-specific validation (KRN, USN, JPN) -->
11. RoleField — User role with permissions
<!-- Role selection + Display role permissions + Permission matrix -->
12. ApprovalField — Approval with comments
<!-- Approver lookup + Approval status + Comment textarea + Timestamp -->
Implementation Strategy
Step 2.1: Core Domain Fields (2 days)
Implement 3 "must-have" fields:
- OrderLineField — Most complex, demonstrates patterns
- CustomerField — Async search pattern
- ProductField — SKU lookup pattern
Step 2.2: Supporting Domain Fields (1 day)
- WarehouseField — Selection + info display
- DateRangeField — Date pair validation
- AddressField — Multi-field composite
Step 2.3: Specialized Domain Fields (1 day)
7-12. Remaining 6 fields (simpler patterns)
API Integration Pattern
For each Domain Field:
- Define API endpoint (in MockServiceWorker)
// tests/mocks/handlers.ts
http.get('*/api/products/search', async ({request}) => {
const url = new URL(request.url)
const query = url.searchParams.get('q')
return HttpResponse.json(searchResults)
})
- Create API client method
// src/services/api/client.ts
class ProductsApiClient extends ApiClient {
async searchProducts(query: string) {
return this.get('/products/search', { params: { q: query } })
}
}
- Use in Domain Field
// src/components/fields/domain/ProductField/ProductField.vue
import { productsApi } from '@/services/api/client'
const handleSkuChange = async (sku: string) => {
const product = await productsApi.getProductBySku(sku)
// ...
}
Component Structure
Each Domain Field (3 files):
src/components/fields/domain/OrderLineField/
├── OrderLineField.vue (component implementation)
├── OrderLineField.stories.ts (5+ Storybook stories)
└── OrderLineField.spec.ts (10+ integration tests)
Folder structure:
src/components/fields/
├── primitives/ (30 components - Phase 1) ✅
├── typed/ (12 components - Phase 2+3.1) ✅
└── domain/ (12 components - Phase 3.2) ← START HERE
├── OrderLineField/
├── CustomerField/
├── ProductField/
├── WarehouseField/
├── SupplierField/
├── StockTransferField/
├── DateRangeField/
├── AddressField/
├── BankAccountField/
├── TaxIDField/
├── RoleField/
├── ApprovalField/
└── index.ts (central export)
State Management Pattern
For Domain Fields with complex state:
Use Pinia store module or local state?
Recommended: Local state (with emit pattern) for Step 2
- Keep fields composable
- Avoid store bloat
- Parent form manages state via Pinia
Example:
// OrderLineField (local state)
const line = ref({...modelValue})
watch(() => line.value, () => {
emit('update:modelValue', line.value)
}, { deep: true })
// Parent OrderForm (Pinia store)
const lineStore = useOrderLinesStore() // Pinia store
Testing Strategy
Unit Tests (Per Field: 15-20 tests)
// OrderLineField.spec.ts example
describe('OrderLineField', () => {
it('loads product details on SKU change')
it('validates quantity against available stock')
it('calculates line total correctly')
it('emits update:modelValue on changes')
it('handles product not found error')
it('disables fields when loading')
it('shows loading spinner during API call')
})
Integration Tests (New)
// tests/integration/domain-fields.spec.ts
describe('Domain Fields with API', () => {
it('OrderLineField full workflow (search → select → calculate)')
it('CustomerField async search + credit check')
it('ProductField SKU lookup + stock validation')
})
Storybook Stories (Per Field: 5+ stories)
// OrderLineField.stories.ts
export const Default = {...}
export const WithProductLookup = {...}
export const WithValidationError = {...}
export const Loading = {...}
export const Disabled = {...}
API Requirements
New API endpoints needed (for MSW mocking):
Products:
GET /api/products/search?q={query}
GET /api/products/{id}
GET /api/products/sku/{sku}
Customers:
GET /api/customers/search?q={query}
GET /api/customers/{id}
POST /api/customers/{id}/credit-check
Warehouses:
GET /api/warehouses
GET /api/warehouses/{id}
Suppliers:
GET /api/suppliers/search?q={query}
GET /api/suppliers/{id}
Orders:
GET /api/orders/{id}/lines
POST /api/orders/{id}/lines (line validation)
Update MSW handlers (tests/mocks/handlers.ts):
- Product search endpoint
- Product by SKU endpoint
- Customer search endpoint
- Customer credit check endpoint
- Warehouse list/get endpoints
- Supplier search endpoint
Exit Criteria (Step 2 Complete)
- ✅ 12 Domain Fields implemented
- ✅ 60+ Storybook stories created
- ✅ 150+ integration tests passing
- ✅ API endpoints mocked (MSW)
- ✅ All async patterns tested
- ✅ Error handling verified
- ✅ Loading states implemented
- ✅ TypeScript strict: 0 errors
- ✅ WCAG accessibility compliance
Timeline
| Task | Duration | Status |
|---|---|---|
| OrderLineField | 6 hours | ⏳ Start |
| CustomerField | 4 hours | ⏳ After OrderLineField |
| ProductField | 4 hours | ⏳ After CustomerField |
| WarehouseField | 3 hours | ⏳ Parallel |
| DateRangeField | 2 hours | ⏳ Parallel |
| AddressField | 3 hours | ⏳ Parallel |
| Remaining 6 fields | 6 hours | ⏳ Day 2 |
| Testing + Storybook | 4 hours | ⏳ Day 3 |
| Total | ~32 hours / 4 days | ⏳ 2026-08-28 → 2026-09-02 |
Quality Checklist (Per Field)
- Component renders correctly
- Props are typed (no
any) - Emits work (update:modelValue)
- API calls mocked (MSW)
- Loading state shows spinner
- Error state shows message
- Validation rules enforced
- Accessible (labels, ARIA, keyboard)
- 5+ Storybook stories
- 15+ integration tests
- TypeScript strict: 0 errors
- No console warnings
Known Patterns to Implement
1. Async Search
const searchQuery = ref('')
const searchResults = ref([])
const isSearching = ref(false)
const handleSearch = async (query: string) => {
isSearching.value = true
searchResults.value = await api.search(query)
isSearching.value = false
}
2. Auto-fill Details
const handleSelect = async (id: string) => {
const details = await api.getDetails(id)
Object.assign(model, details)
emit('update:modelValue', model)
}
3. Multi-field Validation
const validateLineTotal = () => {
if (line.quantity * line.unitPrice !== line.lineTotal) {
error.value = 'Line total mismatch'
}
}
4. Loading State
const isLoading = ref(false)
const handleAsyncAction = async () => {
isLoading.value = true
try {
// async work
} finally {
isLoading.value = false
}
}
Next Phase (After Step 2)
Phase 3 Step 3: Complete remaining Pinia stores (7/10 stores)
- Customers, Suppliers, StockTransfers, etc.
- Integration with Domain Fields
Phase 3 Step 4: Business Composite Components (11 components)
- OrderForm (uses OrderLineField, CustomerField, etc.)
- InventoryTransfer, VoucherEditor, etc.
Files to Create
Phase 3 Step 2 Deliverables:
src/components/fields/domain/
├── index.ts (12 field exports)
├── OrderLineField/
│ ├── OrderLineField.vue
│ ├── OrderLineField.stories.ts
│ └── OrderLineField.spec.ts
├── CustomerField/
├── ProductField/
├── WarehouseField/
├── SupplierField/
├── StockTransferField/
├── DateRangeField/
├── AddressField/
├── BankAccountField/
├── TaxIDField/
├── RoleField/
└── ApprovalField/
tests/mocks/
└── handlers.ts (UPDATED: new API endpoints)
tests/integration/
└── domain-fields.spec.ts (NEW: 150+ tests)
Documentation/
└── PHASE3-STEP2-COMPLETION.md
Ready to implement Phase 3 Step 2? ✅
Starting with OrderLineField (most complex, demonstrates patterns)
Estimated time: 4 days → 2026-09-02
Proceed with OrderLineField implementation? ✅ Yes