Add OMS WMS ERP platform
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
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
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user