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
396 lines
9.6 KiB
Vue
396 lines
9.6 KiB
Vue
<template>
|
||
<div class="tax-id-field">
|
||
<div class="tax-id-group">
|
||
<!-- Country Selection -->
|
||
<div class="form-group">
|
||
<label class="form-label">
|
||
Country
|
||
<span class="text-danger">*</span>
|
||
</label>
|
||
<select
|
||
v-model="taxId.country"
|
||
class="form-control"
|
||
:class="{ 'is-invalid': countryError }"
|
||
@blur="validateCountry"
|
||
>
|
||
<option value="">-- Select Country --</option>
|
||
<option value="KR">South Korea (사업자등록번호)</option>
|
||
<option value="US">United States (EIN)</option>
|
||
<option value="JP">Japan (法人番号)</option>
|
||
<option value="CN">China (统一社会信用代码)</option>
|
||
<option value="SG">Singapore (UEN)</option>
|
||
<option value="TW">Taiwan (統一編號)</option>
|
||
</select>
|
||
<div v-if="countryError" class="invalid-feedback d-block">
|
||
{{ countryError }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tax ID Number -->
|
||
<div class="form-group col-full">
|
||
<label class="form-label">
|
||
{{ getFieldLabel() }}
|
||
<span class="text-danger">*</span>
|
||
</label>
|
||
<input
|
||
v-model="taxId.taxIdNumber"
|
||
type="text"
|
||
class="form-control"
|
||
:class="{ 'is-invalid': taxIdError }"
|
||
:placeholder="getPlaceholder()"
|
||
@blur="validateTaxId"
|
||
/>
|
||
<small class="text-muted d-block mt-1">
|
||
{{ getFormatHint() }}
|
||
</small>
|
||
<div v-if="taxIdError" class="invalid-feedback d-block">
|
||
{{ taxIdError }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Company/Individual Type (optional) -->
|
||
<div class="form-group">
|
||
<label class="form-label">Type</label>
|
||
<select v-model="taxId.entityType" class="form-control">
|
||
<option value="">-- Select Type --</option>
|
||
<option value="INDIVIDUAL">Individual</option>
|
||
<option value="BUSINESS">Business</option>
|
||
<option value="CORPORATION">Corporation</option>
|
||
<option value="PARTNERSHIP">Partnership</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Verification Status (optional) -->
|
||
<div class="form-group">
|
||
<label class="form-label">Verification Status</label>
|
||
<select v-model="taxId.verificationStatus" class="form-control">
|
||
<option value="">-- Not Verified --</option>
|
||
<option value="PENDING">Pending</option>
|
||
<option value="VERIFIED">Verified</option>
|
||
<option value="FAILED">Failed</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tax ID Summary -->
|
||
<div v-if="isComplete" class="tax-id-summary mt-2">
|
||
<small class="text-muted">
|
||
📋 <strong>{{ getCountryName(taxId.country) }}</strong> —
|
||
{{ formatTaxId() }}
|
||
<span v-if="taxId.verificationStatus" class="badge" :class="getStatusBadgeClass()">
|
||
{{ taxId.verificationStatus }}
|
||
</span>
|
||
</small>
|
||
</div>
|
||
|
||
<!-- Validation Status -->
|
||
<div v-if="hasErrors" class="alert alert-danger mt-2">
|
||
⚠️ Please complete all required fields
|
||
</div>
|
||
|
||
<div v-if="isComplete && !hasErrors" class="alert alert-success mt-2">
|
||
✅ Tax ID information is valid
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed } from 'vue'
|
||
|
||
interface TaxID {
|
||
country: string
|
||
taxIdNumber: string
|
||
entityType?: string
|
||
verificationStatus?: string
|
||
}
|
||
|
||
const props = defineProps<{
|
||
modelValue: TaxID | null
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
'update:modelValue': [value: TaxID | null]
|
||
}>()
|
||
|
||
// State
|
||
const taxId = ref<TaxID>({
|
||
country: props.modelValue?.country || '',
|
||
taxIdNumber: props.modelValue?.taxIdNumber || '',
|
||
entityType: props.modelValue?.entityType || '',
|
||
verificationStatus: props.modelValue?.verificationStatus || ''
|
||
})
|
||
|
||
const countryError = ref<string | null>(null)
|
||
const taxIdError = ref<string | null>(null)
|
||
|
||
// Computed
|
||
const isComplete = computed(() => {
|
||
return (
|
||
taxId.value.country.length > 0 &&
|
||
taxId.value.taxIdNumber.trim().length > 0 &&
|
||
!hasErrors.value
|
||
)
|
||
})
|
||
|
||
const hasErrors = computed(() => {
|
||
return countryError.value !== null || taxIdError.value !== null
|
||
})
|
||
|
||
// Methods
|
||
const validateCountry = () => {
|
||
countryError.value = null
|
||
|
||
if (!taxId.value.country) {
|
||
countryError.value = 'Please select a country'
|
||
return
|
||
}
|
||
|
||
emitUpdate()
|
||
}
|
||
|
||
const validateTaxId = () => {
|
||
taxIdError.value = null
|
||
|
||
if (!taxId.value.taxIdNumber.trim()) {
|
||
taxIdError.value = 'Tax ID is required'
|
||
return
|
||
}
|
||
|
||
// Validate based on country
|
||
const country = taxId.value.country
|
||
const value = taxId.value.taxIdNumber.trim()
|
||
|
||
if (country === 'KR') {
|
||
// Korean: 사업자등록번호 (10 digits with optional hyphens: XXX-XX-XXXXX)
|
||
if (!/^(\d{3}-\d{2}-\d{5}|\d{10})$/.test(value)) {
|
||
taxIdError.value = 'Korean Tax ID format: XXX-XX-XXXXX or 10 digits'
|
||
return
|
||
}
|
||
} else if (country === 'US') {
|
||
// US: EIN (9 digits with optional hyphen: XX-XXXXXXX)
|
||
if (!/^(\d{2}-\d{7}|\d{9})$/.test(value)) {
|
||
taxIdError.value = 'US EIN format: XX-XXXXXXX or 9 digits'
|
||
return
|
||
}
|
||
} else if (country === 'JP') {
|
||
// Japan: 12 digits (XXXXXXXXXXXX)
|
||
if (!/^\d{12,13}$/.test(value)) {
|
||
taxIdError.value = 'Japan Tax ID format: 12-13 digits'
|
||
return
|
||
}
|
||
} else if (country === 'CN') {
|
||
// China: 18 digits (统一社会信用代码)
|
||
if (!/^\d{18}$/.test(value)) {
|
||
taxIdError.value = 'China Tax ID format: 18 digits'
|
||
return
|
||
}
|
||
} else if (country === 'SG') {
|
||
// Singapore: UEN (9 digits with optional hyphen: XXXXXXXXX or XXX-XXXXXX)
|
||
if (!/^(\d{9}|\d{3}-\d{6})$/.test(value)) {
|
||
taxIdError.value = 'Singapore UEN format: 9 digits or XXX-XXXXXX'
|
||
return
|
||
}
|
||
} else if (country === 'TW') {
|
||
// Taiwan: 8 digits (統一編號)
|
||
if (!/^\d{8}$/.test(value)) {
|
||
taxIdError.value = 'Taiwan Tax ID format: 8 digits'
|
||
return
|
||
}
|
||
}
|
||
|
||
emitUpdate()
|
||
}
|
||
|
||
const emitUpdate = () => {
|
||
if (isComplete.value) {
|
||
emit('update:modelValue', { ...taxId.value })
|
||
}
|
||
}
|
||
|
||
const getFieldLabel = (): string => {
|
||
const labels: Record<string, string> = {
|
||
KR: 'Business Registration Number (사업자등록번호)',
|
||
US: 'Employer Identification Number (EIN)',
|
||
JP: 'Corporate Number (法人番号)',
|
||
CN: 'Unified Social Credit Code (统一社会信用代码)',
|
||
SG: 'Unique Entity Number (UEN)',
|
||
TW: 'Uniform Number (統一編號)'
|
||
}
|
||
return labels[taxId.value.country] || 'Tax ID Number'
|
||
}
|
||
|
||
const getPlaceholder = (): string => {
|
||
const placeholders: Record<string, string> = {
|
||
KR: 'e.g., 123-45-67890',
|
||
US: 'e.g., 12-3456789',
|
||
JP: 'e.g., 1234567890123',
|
||
CN: 'e.g., 123456789012345678',
|
||
SG: 'e.g., 123456789 or 123-456789',
|
||
TW: 'e.g., 12345678'
|
||
}
|
||
return placeholders[taxId.value.country] || ''
|
||
}
|
||
|
||
const getFormatHint = (): string => {
|
||
const hints: Record<string, string> = {
|
||
KR: 'Format: XXX-XX-XXXXX (10 digits)',
|
||
US: 'Format: XX-XXXXXXX (9 digits)',
|
||
JP: 'Format: 12-13 digits',
|
||
CN: 'Format: 18 digits',
|
||
SG: 'Format: 9 digits or XXX-XXXXXX',
|
||
TW: 'Format: 8 digits'
|
||
}
|
||
return hints[taxId.value.country] || 'Format: country-specific'
|
||
}
|
||
|
||
const getCountryName = (code: string): string => {
|
||
const countries: Record<string, string> = {
|
||
KR: 'South Korea',
|
||
US: 'United States',
|
||
JP: 'Japan',
|
||
CN: 'China',
|
||
SG: 'Singapore',
|
||
TW: 'Taiwan'
|
||
}
|
||
return countries[code] || 'Country'
|
||
}
|
||
|
||
const formatTaxId = (): string => {
|
||
const value = taxId.value.taxIdNumber
|
||
const country = taxId.value.country
|
||
|
||
// Format with country indicator
|
||
return `${getCountryName(country)}: ${value}`
|
||
}
|
||
|
||
const getStatusBadgeClass = (): string => {
|
||
const status = taxId.value.verificationStatus
|
||
if (status === 'VERIFIED') return 'bg-success'
|
||
if (status === 'PENDING') return 'bg-warning'
|
||
if (status === 'FAILED') return 'bg-danger'
|
||
return 'bg-secondary'
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.tax-id-field {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.tax-id-group {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 1rem;
|
||
padding: 1rem;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 4px;
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
.col-full {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.form-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.form-label {
|
||
font-weight: 500;
|
||
margin-bottom: 0.5rem;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.form-control {
|
||
border-radius: 4px;
|
||
border: 1px solid #dee2e6;
|
||
padding: 0.5rem 0.75rem;
|
||
font-size: 0.875rem;
|
||
font-family: inherit;
|
||
}
|
||
|
||
.form-control:focus {
|
||
border-color: #80bdff;
|
||
outline: 0;
|
||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||
}
|
||
|
||
.form-control.is-invalid {
|
||
border-color: #dc3545;
|
||
}
|
||
|
||
.text-danger {
|
||
color: #dc3545;
|
||
margin-left: 0.25rem;
|
||
}
|
||
|
||
.text-muted {
|
||
color: #6c757d;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.invalid-feedback {
|
||
color: #dc3545;
|
||
font-size: 0.875rem;
|
||
margin-top: 0.25rem;
|
||
display: block;
|
||
}
|
||
|
||
.d-block {
|
||
display: block;
|
||
}
|
||
|
||
.mt-1 {
|
||
margin-top: 0.25rem;
|
||
}
|
||
|
||
.mt-2 {
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
.tax-id-summary {
|
||
padding: 0.75rem;
|
||
background-color: #e7f3ff;
|
||
border-left: 3px solid #0d6efd;
|
||
border-radius: 4px;
|
||
font-size: 0.875rem;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.badge {
|
||
font-size: 0.7rem;
|
||
padding: 0.25rem 0.5rem;
|
||
margin-left: 0.5rem;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.alert {
|
||
padding: 0.75rem 1rem;
|
||
border-radius: 4px;
|
||
font-size: 0.875rem;
|
||
border: 1px solid transparent;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.alert-danger {
|
||
background-color: #f8d7da;
|
||
color: #721c24;
|
||
border-color: #f5c6cb;
|
||
}
|
||
|
||
.alert-success {
|
||
background-color: #d4edda;
|
||
color: #155724;
|
||
border-color: #c3e6cb;
|
||
}
|
||
|
||
small {
|
||
display: block;
|
||
}
|
||
|
||
strong {
|
||
font-weight: 600;
|
||
}
|
||
</style>
|