Files
QuantEngineByItz/oms-wms-erp/PHASE2-TYPED-FIELDS.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

13 KiB

Phase 2 Step 1: Typed Fields Implementation

Status: 5/12 fields scaffolded, 7 templates provided
Date: 2026-08-12 (Week 3 start)
Timeline: Week 3-4 (2 weeks)


Typed Fields Overview (Layer 2)

Purpose: Domain-aware input components with automatic validation, formatting, and user-friendly error messages

12 Total Typed Fields:

  1. TextField (text, email, password, url, tel)
  2. DateField (date picker with min/max)
  3. CurrencyField (amount with locale formatting)
  4. SelectField (dropdown with validation)
  5. StatusField (predefined statuses with colors)
  6. TimeField (time picker)
  7. PercentageField (0-100% with formatting)
  8. QuantityField (positive integer, no decimals)
  9. MultiSelectField (multiple selections)
  10. CheckboxField (boolean checkbox)
  11. SearchField (autocomplete with API lookup)
  12. PhoneField (phone number with formatting)

Completed: 5 Typed Fields

1. TextField

<!-- Features -->
- Type support: text, email, password, url, tel
- Validation rules (required, email, url, pattern, minLength, maxLength)
- Character counter
- Help text + error messages
- WCAG 2.1 AA accessibility

Props: modelValue, label, type, placeholder, disabled, required, maxLength, validationRules, etc.

2. DateField

<!-- Features -->
- HTML5 date picker (native)
- Min/Max date validation
- ISO format (YYYY-MM-DD)
- Locale-aware display
- Range validation

Props: modelValue (ISO date), label, minDate, maxDate, disabled, required, etc.

3. CurrencyField

<!-- Features -->
- Locale formatting ( Korean Won, comma separators)
- Input validation (positive, decimals)
- Currency symbol display
- Min/Max amount validation
- Decimal precision (default: 0, customizable)

Props: modelValue, currencySymbol (₩), minValue, maxValue, decimals, etc.

4. SelectField

<!-- Features -->
- Dropdown with typed options
- Placeholder support
- Required validation
- WCAG 2.1 accessibility
- Search-ready (for future autocomplete)

Props: modelValue, options (Array<{value, label}>), required, etc.

5. StatusField

<!-- Features -->
- Predefined statuses: DRAFT, PENDING, APPROVED, ACTIVE, COMPLETED, CANCELLED, FAILED
- Color-coded badges (secondary, warning, info, success, danger)
- Format status text (e.g., "DRAFT"  "Draft")
- Required validation

Props: modelValue, statusList, disabled, required, etc.


Templates: 7 Remaining Typed Fields

6. TimeField

<!-- src/components/fields/typed/TimeField/TimeField.vue -->
<template>
  <div class="mb-3">
    <label v-if="label" :for="id" class="form-label">
      {{ label }}
      <span v-if="required" class="text-danger">*</span>
    </label>
    <input
      :id="id"
      :value="modelValue"
      type="time"
      :disabled="disabled"
      :class="['form-control', { 'is-invalid': error }]"
      @input="handleInput"
      @blur="handleBlur"
    />
    <div v-if="error" class="invalid-feedback d-block">{{ error }}</div>
  </div>
</template>

<script setup lang="ts">
// Similar to DateField
// Format: HH:mm
// Props: modelValue, label, minTime, maxTime, disabled, required
</script>

7. PercentageField

<!-- src/components/fields/typed/PercentageField/PercentageField.vue -->
<template>
  <div class="mb-3">
    <label v-if="label" :for="id" class="form-label">
      {{ label }}
      <span v-if="required" class="text-danger">*</span>
    </label>
    <div class="input-group">
      <input
        :id="id"
        :value="displayValue"
        type="number"
        :min="0"
        :max="100"
        :disabled="disabled"
        :class="['form-control', 'text-end', { 'is-invalid': error }]"
        @input="handleInput"
        @blur="handleBlur"
      />
      <span class="input-group-text">%</span>
    </div>
    <div v-if="error" class="invalid-feedback d-block">{{ error }}</div>
  </div>
</template>

<script setup lang="ts">
// Validation: 0-100 range
// Decimal support (0-2 decimals default)
// Props: modelValue, label, disabled, required, decimals
</script>

8. QuantityField

<!-- src/components/fields/typed/QuantityField/QuantityField.vue -->
<template>
  <div class="mb-3">
    <label v-if="label" :for="id" class="form-label">
      {{ label }}
      <span v-if="required" class="text-danger">*</span>
    </label>
    <input
      :id="id"
      :value="modelValue"
      type="number"
      :min="minQty"
      :max="maxQty"
      :step="1"
      :disabled="disabled"
      :class="['form-control', { 'is-invalid': error }]"
      @input="handleInput"
      @blur="handleBlur"
    />
    <div v-if="error" class="invalid-feedback d-block">{{ error }}</div>
  </div>
</template>

<script setup lang="ts">
// Positive integers only (no decimals)
// Min/Max validation
// Props: modelValue, minQty, maxQty, disabled, required
</script>

9. MultiSelectField

<!-- src/components/fields/typed/MultiSelectField/MultiSelectField.vue -->
<template>
  <div class="mb-3">
    <label v-if="label" class="form-label">
      {{ label }}
      <span v-if="required" class="text-danger">*</span>
    </label>
    <div class="multi-select">
      <div class="selected-tags">
        <span
          v-for="value in modelValue"
          :key="value"
          class="badge bg-primary me-2 mb-2"
        >
          {{ getOptionLabel(value) }}
          <button type="button" @click="removeOption(value)" class="btn-close btn-close-white ms-2" />
        </span>
      </div>
      <select
        :multiple="true"
        :value="modelValue"
        :disabled="disabled"
        :class="['form-select', { 'is-invalid': error }]"
        @change="handleChange"
      >
        <option v-for="option in options" :key="option.value" :value="option.value">
          {{ option.label }}
        </option>
      </select>
    </div>
    <div v-if="error" class="invalid-feedback d-block">{{ error }}</div>
  </div>
</template>

<script setup lang="ts">
// Array of selected values
// Tag display for selected items
// Props: modelValue (array), options, maxItems, disabled, required
</script>

10. CheckboxField

<!-- src/components/fields/typed/CheckboxField/CheckboxField.vue -->
<template>
  <div class="form-check">
    <input
      :id="id"
      type="checkbox"
      class="form-check-input"
      :checked="modelValue"
      :disabled="disabled"
      @change="handleChange"
    />
    <label :for="id" class="form-check-label">
      {{ label }}
      <span v-if="required" class="text-danger">*</span>
    </label>
    <small v-if="helpText" class="form-text text-muted d-block">
      {{ helpText }}
    </small>
  </div>
</template>

<script setup lang="ts">
// Boolean checkbox
// Props: modelValue (boolean), label, disabled, required, helpText
</script>

11. SearchField

<!-- src/components/fields/typed/SearchField/SearchField.vue -->
<template>
  <div class="mb-3">
    <label v-if="label" :for="id" class="form-label">
      {{ label }}
      <span v-if="required" class="text-danger">*</span>
    </label>
    <div class="position-relative">
      <input
        :id="id"
        v-model="searchQuery"
        type="text"
        :placeholder="placeholder"
        :disabled="disabled"
        :class="['form-control', { 'is-invalid': error }]"
        @input="handleSearch"
        @focus="showSuggestions = true"
        @blur="showSuggestions = false"
      />
      <div v-if="showSuggestions && suggestions.length > 0" class="dropdown-menu show w-100">
        <a
          v-for="suggestion in suggestions"
          :key="suggestion.id"
          href="#"
          class="dropdown-item"
          @click.prevent="selectSuggestion(suggestion)"
        >
          {{ suggestion.label }}
        </a>
      </div>
    </div>
    <div v-if="error" class="invalid-feedback d-block">{{ error }}</div>
  </div>
</template>

<script setup lang="ts">
// Autocomplete search with API lookup
// Debounced search (300ms default)
// Props: modelValue, placeholder, onSearch (async function), suggestions, disabled, required
// Emits: select (when option selected)
</script>

12. PhoneField

<!-- src/components/fields/typed/PhoneField/PhoneField.vue -->
<template>
  <div class="mb-3">
    <label v-if="label" :for="id" class="form-label">
      {{ label }}
      <span v-if="required" class="text-danger">*</span>
    </label>
    <input
      :id="id"
      :value="displayValue"
      type="tel"
      :placeholder="placeholder || '010-1234-5678'"
      :disabled="disabled"
      :class="['form-control', { 'is-invalid': error }]"
      @input="handleInput"
      @blur="handleBlur"
    />
    <div v-if="error" class="invalid-feedback d-block">{{ error }}</div>
  </div>
</template>

<script setup lang="ts">
// Phone number formatting (Korean: 010-1234-5678)
// Validation: 10-11 digits
// Props: modelValue, label, disabled, required, format (default: Korean)
// useFormatting().formatPhone(value)
</script>

Shared Composables (Created)

useValidation.ts

// Validation rules
- required(message?)
- email(message?)
- minLength(min, message?)
- maxLength(max, message?)
- min(min, message?)
- max(max, message?)
- pattern(regex, message?)
- numeric(message?)
- positiveInteger(message?)
- percentage(message?)
- url(message?)

// Usage:
const validator = createValidationRules()
const error = validator.validate(value, [
  validator.required(),
  validator.email()
])

useFormatting.ts

// Formatting utilities
- formatCurrency(value, decimals, symbol)
- parseCurrency(value)
- formatPercentage(value, decimals)
- parsePercentage(value)
- formatDate(value, format)
- parseDate(value)
- formatTime(value, format)
- parseTime(value)
- formatPhone(value)
- parsePhone(value)
- formatNumber(value, decimals)
- parseNumber(value)
- truncate(value, length, suffix)
- capitalize(value)
- upperCase(value)
- lowerCase(value)

// Usage:
const { formatCurrency, formatDate } = useFormatting()
const displayPrice = formatCurrency(9999)  // ₩9,999
const isoDate = formatDate('2026-08-12')   // 2026-08-12

Testing Strategy (Phase 2)

Unit Tests for Each Typed Field

// src/components/fields/typed/TextField/TextField.spec.ts

describe('TextField', () => {
  it('validates required field', () => {
    const wrapper = mount(TextField, {
      props: {
        modelValue: '',
        required: true,
        validationRules: [validator.required()]
      }
    })
    wrapper.vm.handleBlur()
    expect(wrapper.vm.error).toBe('This field is required')
  })

  it('formats input on blur', () => {
    const wrapper = mount(TextField, {
      props: { modelValue: 'test' }
    })
    wrapper.vm.handleBlur()
    expect(wrapper.emitted('blur')).toBeTruthy()
  })
})

Target: 5-8 tests per field, ~70+ integration tests total


Storybook Stories

// src/components/fields/typed/TextField/TextField.stories.ts

export const Default: Story = {
  args: {
    label: 'Username',
    placeholder: 'Enter username',
    required: true
  }
}

export const WithError: Story = {
  args: {
    label: 'Email',
    type: 'email',
    error: 'Invalid email address'
  }
}

export const WithCounter: Story = {
  args: {
    label: 'Bio',
    type: 'text',
    maxLength: 160,
    showCounter: true
  }
}

Target: 8-12 stories per field, ~100+ stories total


Phase 2 Step 1 Completion Checklist

  • Validation composable (useValidation.ts)
  • Formatting composable (useFormatting.ts)
  • 5 Typed Fields fully implemented (TextField, DateField, CurrencyField, SelectField, StatusField)
  • 7 Typed Fields templates provided (ready to implement)
  • All 12 Storybook stories added (100+ stories)
  • All 12 unit tests added (70+ tests passing)
  • Integration tests for form validation chains
  • WCAG 2.1 AA accessibility audit

Quick Start: Generate Remaining 7 Fields

# Use the component generator from Phase 1
npm run component:create TimeField
npm run component:create PercentageField
npm run component:create QuantityField
npm run component:create MultiSelectField
npm run component:create CheckboxField
npm run component:create SearchField
npm run component:create PhoneField

# Then implement using templates above
# For each: copy template → customize → add stories → add tests

Next: Phase 2 Step 2 (Pinia Stores)

Once all 12 Typed Fields complete:

  • Generate 10 Pinia store modules
  • API client integration
  • Mock Service Worker (MSW) setup
  • Integration tests with API mocks

Timeline: Complete by 2026-08-19 (Friday, Week 3)
Next Phase: Step 2 Pinia Stores (Week 4)