Files
QuantEngineByItz/oms-wms-erp/src/components/fields/domain/ProductField/ProductField.vue
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

436 lines
9.0 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="product-field">
<!-- SKU Input for Product Lookup -->
<div class="form-group">
<label class="form-label">
Product SKU
<span class="text-danger">*</span>
</label>
<div class="input-group">
<input
v-model="sku"
type="text"
class="form-control"
placeholder="e.g., SKU-001"
:class="{ 'is-invalid': skuError }"
@blur="handleSkuLookup"
/>
<button
v-if="isLoading"
class="btn btn-outline-secondary"
disabled
>
<span class="spinner-border spinner-border-sm me-2"></span>
Loading...
</button>
</div>
<div v-if="skuError" class="invalid-feedback d-block">
{{ skuError }}
</div>
</div>
<!-- Product Details Panel -->
<div v-if="product" class="card mt-2">
<div class="card-body">
<h5 class="card-title">{{ product.name }}</h5>
<div class="row mb-3">
<!-- Left Column -->
<div class="col-md-6">
<small>
<strong>SKU:</strong> {{ product.sku }}<br>
<strong>Category:</strong>
<span class="badge bg-info">{{ product.categoryId }}</span><br>
<strong>Status:</strong>
<span :class="statusClass">{{ product.status }}</span><br>
</small>
</div>
<!-- Right Column -->
<div class="col-md-6">
<small>
<strong>List Price:</strong>
<span class="text-primary">{{ formatCurrency(product.price) }}</span><br>
<strong>Available Stock:</strong>
<strong :class="stockClass">{{ product.qtyOnHand }}</strong> units<br>
<strong>Last Updated:</strong> {{ formatDate(product.lastUpdated) }}<br>
</small>
</div>
</div>
<!-- Stock Level Warning -->
<div v-if="product.qtyOnHand < 10" class="alert alert-warning">
Low Stock Alert: Only {{ product.qtyOnHand }} units available
</div>
<!-- Description -->
<div v-if="product.description" class="alert alert-info">
<strong>Description:</strong> {{ product.description }}
</div>
<!-- Clear Button -->
<button
class="btn btn-sm btn-outline-secondary"
@click="clearProduct"
>
Clear
</button>
</div>
</div>
<!-- Error State -->
<div v-if="productNotFound" class="alert alert-danger mt-2">
Product not found. Please check the SKU and try again.
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useFormatting } from '@/composables/useFormatting'
interface Product {
productId: string
sku: string
name: string
categoryId: string
price: number
qtyOnHand: number
status: string
description?: string
lastUpdated: string
}
const props = defineProps<{
modelValue: Product | null
}>()
const emit = defineEmits<{
'update:modelValue': [value: Product | null]
select: [product: Product]
}>()
const { formatCurrency, formatDate } = useFormatting()
// State
const sku = ref('')
const product = ref<Product | null>(null)
const isLoading = ref(false)
const skuError = ref<string | null>(null)
const productNotFound = ref(false)
// Computed
const statusClass = computed(() => {
if (!product.value) return ''
switch (product.value.status) {
case 'ACTIVE':
return 'badge bg-success'
case 'INACTIVE':
return 'badge bg-secondary'
case 'DISCONTINUED':
return 'badge bg-danger'
default:
return 'badge bg-secondary'
}
})
const stockClass = computed(() => {
if (!product.value) return ''
if (product.value.qtyOnHand === 0) return 'text-danger'
if (product.value.qtyOnHand < 10) return 'text-warning'
return 'text-success'
})
// Methods
const handleSkuLookup = async () => {
skuError.value = null
productNotFound.value = false
if (!sku.value) {
product.value = null
emit('update:modelValue', null)
return
}
// Validate SKU format
if (sku.value.trim().length < 3) {
skuError.value = 'SKU must be at least 3 characters'
return
}
isLoading.value = true
try {
// Mock API call - would be: await productsApi.getProductBySku(sku.value)
const foundProduct = await mockGetProductBySku(sku.value.trim())
if (!foundProduct) {
productNotFound.value = true
product.value = null
skuError.value = null
emit('update:modelValue', null)
return
}
product.value = foundProduct
emit('update:modelValue', foundProduct)
emit('select', foundProduct)
} catch (error) {
skuError.value = 'Failed to look up product'
product.value = null
} finally {
isLoading.value = false
}
}
const clearProduct = () => {
sku.value = ''
product.value = null
skuError.value = null
productNotFound.value = false
emit('update:modelValue', null)
}
// Mock API - would be replaced with real API call
const mockGetProductBySku = async (skuQuery: string): Promise<Product | null> => {
return new Promise((resolve) => {
setTimeout(() => {
const mockProducts: Record<string, Product> = {
'SKU-001': {
productId: 'PROD-001',
sku: 'SKU-001',
name: 'Standard Widget',
categoryId: 'CAT-001',
price: 50000,
qtyOnHand: 150,
status: 'ACTIVE',
description: 'High-quality standard widget for industrial use',
lastUpdated: new Date().toISOString().split('T')[0]
},
'SKU-002': {
productId: 'PROD-002',
sku: 'SKU-002',
name: 'Premium Gadget',
categoryId: 'CAT-002',
price: 125000,
qtyOnHand: 5,
status: 'ACTIVE',
description: 'Premium grade gadget with extended warranty',
lastUpdated: new Date().toISOString().split('T')[0]
},
'SKU-003': {
productId: 'PROD-003',
sku: 'SKU-003',
name: 'Discontinued Item',
categoryId: 'CAT-001',
price: 25000,
qtyOnHand: 0,
status: 'DISCONTINUED',
description: 'This product has been discontinued',
lastUpdated: new Date().toISOString().split('T')[0]
}
}
resolve(mockProducts[skuQuery] || null)
}, 400)
})
}
</script>
<style scoped>
.product-field {
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-label {
font-weight: 500;
margin-bottom: 0.5rem;
display: block;
font-size: 0.875rem;
}
.input-group {
display: flex;
position: relative;
}
.form-control {
border-radius: 4px;
border: 1px solid #dee2e6;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
flex: 1;
}
.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;
}
.btn {
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
border-radius: 0 4px 4px 0;
border: 1px solid #dee2e6;
border-left: 0;
cursor: pointer;
transition: all 0.15s ease-in-out;
}
.btn-outline-secondary {
color: #6c757d;
border-color: #6c757d;
}
.btn-outline-secondary:disabled {
opacity: 0.65;
}
.btn-outline-secondary:hover:not(:disabled) {
background-color: #6c757d;
color: white;
}
.spinner-border {
width: 1rem;
height: 1rem;
border-width: 0.2em;
}
.me-2 {
margin-right: 0.5rem;
}
.card {
border: 1px solid #dee2e6;
border-radius: 4px;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.card-body {
padding: 1rem;
}
.card-title {
margin-bottom: 1rem;
font-weight: 600;
font-size: 1rem;
}
.row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.col-md-6 {
flex: 1;
}
.mb-3 {
margin-bottom: 1rem;
}
small {
line-height: 1.8;
display: block;
}
.badge {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
border-radius: 4px;
color: white;
}
.bg-success {
background-color: #28a745 !important;
}
.bg-secondary {
background-color: #6c757d !important;
}
.bg-danger {
background-color: #dc3545 !important;
}
.bg-info {
background-color: #17a2b8 !important;
}
.text-primary {
color: #0d6efd;
font-weight: 600;
}
.text-success {
color: #28a745;
font-weight: 600;
}
.text-warning {
color: #ffc107;
font-weight: 600;
}
.text-danger {
color: #dc3545;
font-weight: 600;
}
.text-danger {
color: #dc3545;
margin-left: 0.25rem;
}
.alert {
padding: 0.75rem 1rem;
border-radius: 4px;
font-size: 0.875rem;
border: 1px solid transparent;
margin-bottom: 1rem;
}
.alert-warning {
background-color: #fff3cd;
color: #856404;
border-color: #ffeaa7;
}
.alert-info {
background-color: #d1ecf1;
color: #0c5460;
border-color: #bee5eb;
}
.alert-danger {
background-color: #f8d7da;
color: #721c24;
border-color: #f5c6cb;
}
.invalid-feedback {
color: #dc3545;
font-size: 0.875rem;
margin-top: 0.25rem;
display: block;
}
.d-block {
display: block;
}
.mt-2 {
margin-top: 0.5rem;
}
</style>