Files
QuantEngineByItz/oms-wms-erp/PHASE3-STEP2-SMART-COMPONENTS.md
T
kjh2064 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
Add OMS WMS ERP platform
2026-07-27 00:45:39 +09:00

16 KiB
Raw Blame History

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:

  1. OrderLineField — Most complex, demonstrates patterns
  2. CustomerField — Async search pattern
  3. ProductField — SKU lookup pattern

Step 2.2: Supporting Domain Fields (1 day)

  1. WarehouseField — Selection + info display
  2. DateRangeField — Date pair validation
  3. 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)
// 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)
})
  1. 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 } })
  }
}
  1. 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

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