b34b0dd7d6
Validators (Pushes and Pull Requests) / UI & Storage Validation (pull_request) Failing after 12s
Validators (Pushes and Pull Requests) / Database & Schema Validation (pull_request) Successful in 13s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (pull_request) Failing after 28s
Validators (Pushes and Pull Requests) / WBS & Audit Validations (pull_request) Has been skipped
Validators (Pushes and Pull Requests) / .NET Contracts (pull_request) Has been skipped
Validators (Pushes and Pull Requests) / Calibration & Performance (pull_request) Has been skipped
Validators (Pushes and Pull Requests) / Operational Report & Decision Packet (pull_request) Has been skipped
Validators (Pushes and Pull Requests) / CI Workflow Lint (pull_request) Failing after 10s
Validators (Pushes and Pull Requests) / Security & Secrets (pull_request) Successful in 12s
Validators (Pushes and Pull Requests) / Notify PR Results (pull_request) Successful in 2s
Frontend CI Pipeline / ci-frontend-8-steps (pull_request) Failing after 2m50s
695 lines
16 KiB
Markdown
695 lines
16 KiB
Markdown
# 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)
|
||
```vue
|
||
<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
|
||
```vue
|
||
<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
|
||
```vue
|
||
<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
|
||
|
||
```vue
|
||
<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
|
||
```vue
|
||
<!-- Company name + Contact + Payment terms -->
|
||
```
|
||
|
||
### 6. **StockTransferField** — From/To warehouse transfer
|
||
```vue
|
||
<!-- From warehouse + To warehouse + Quantity + Transfer reason -->
|
||
```
|
||
|
||
### 7. **DateRangeField** — Start + End date pair
|
||
```vue
|
||
<!-- Start date + End date with validation (start < end) -->
|
||
```
|
||
|
||
### 8. **AddressField** — Full address with country
|
||
```vue
|
||
<!-- Street + City + Postal + Country + Validation -->
|
||
```
|
||
|
||
### 9. **BankAccountField** — Account + Bank code
|
||
```vue
|
||
<!-- Account number + Bank code + Account holder name -->
|
||
```
|
||
|
||
### 10. **TaxIDField** — Country-specific tax ID
|
||
```vue
|
||
<!-- Tax ID with country-specific validation (KRN, USN, JPN) -->
|
||
```
|
||
|
||
### 11. **RoleField** — User role with permissions
|
||
```vue
|
||
<!-- Role selection + Display role permissions + Permission matrix -->
|
||
```
|
||
|
||
### 12. **ApprovalField** — Approval with comments
|
||
```vue
|
||
<!-- Approver lookup + Approval status + Comment textarea + Timestamp -->
|
||
```
|
||
|
||
---
|
||
|
||
## Implementation Strategy
|
||
|
||
### Step 2.1: Core Domain Fields (2 days)
|
||
Implement 3 "must-have" fields:
|
||
1. **OrderLineField** — Most complex, demonstrates patterns
|
||
2. **CustomerField** — Async search pattern
|
||
3. **ProductField** — SKU lookup pattern
|
||
|
||
### Step 2.2: Supporting Domain Fields (1 day)
|
||
4. **WarehouseField** — Selection + info display
|
||
5. **DateRangeField** — Date pair validation
|
||
6. **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**:
|
||
|
||
1. **Define API endpoint** (in MockServiceWorker)
|
||
```typescript
|
||
// 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)
|
||
})
|
||
```
|
||
|
||
2. **Create API client method**
|
||
```typescript
|
||
// src/services/api/client.ts
|
||
class ProductsApiClient extends ApiClient {
|
||
async searchProducts(query: string) {
|
||
return this.get('/products/search', { params: { q: query } })
|
||
}
|
||
}
|
||
```
|
||
|
||
3. **Use in Domain Field**
|
||
```typescript
|
||
// 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**:
|
||
```typescript
|
||
// 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)
|
||
```typescript
|
||
// 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)
|
||
```typescript
|
||
// 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)
|
||
```typescript
|
||
// 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**
|
||
```typescript
|
||
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**
|
||
```typescript
|
||
const handleSelect = async (id: string) => {
|
||
const details = await api.getDetails(id)
|
||
Object.assign(model, details)
|
||
emit('update:modelValue', model)
|
||
}
|
||
```
|
||
|
||
### 3. **Multi-field Validation**
|
||
```typescript
|
||
const validateLineTotal = () => {
|
||
if (line.quantity * line.unitPrice !== line.lineTotal) {
|
||
error.value = 'Line total mismatch'
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4. **Loading State**
|
||
```typescript
|
||
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
|