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,537 @@
|
||||
<template>
|
||||
<div class="supplier-field">
|
||||
<!-- Supplier Search/Select -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
Supplier
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Search by company name..."
|
||||
@input="handleSearch"
|
||||
/>
|
||||
<button
|
||||
v-if="isSearching"
|
||||
class="btn btn-outline-secondary"
|
||||
disabled
|
||||
>
|
||||
<span class="spinner-border spinner-border-sm"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search Results Dropdown -->
|
||||
<div v-if="searchResults.length > 0" class="supplier-dropdown">
|
||||
<div
|
||||
v-for="supplier in searchResults"
|
||||
:key="supplier.id"
|
||||
class="dropdown-item"
|
||||
@click="selectSupplier(supplier)"
|
||||
>
|
||||
<strong>{{ supplier.name }}</strong>
|
||||
<small class="text-muted d-block">{{ supplier.email }} | {{ supplier.phone }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="searchError" class="invalid-feedback d-block">
|
||||
{{ searchError }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selected Supplier Details -->
|
||||
<div v-if="selectedSupplier" class="card mt-2">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ selectedSupplier.name }}</h5>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<small>
|
||||
<strong>Email:</strong> {{ selectedSupplier.email }}<br>
|
||||
<strong>Phone:</strong> {{ selectedSupplier.phone }}<br>
|
||||
<strong>City:</strong> {{ selectedSupplier.city }}<br>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<small>
|
||||
<strong>Status:</strong>
|
||||
<span :class="statusClass">{{ selectedSupplier.status }}</span><br>
|
||||
<strong>Rating:</strong>
|
||||
<span class="rating-stars">{{ renderStars(selectedSupplier.rating) }}</span><br>
|
||||
<strong>Joined:</strong> {{ formatDate(selectedSupplier.joinedDate) }}<br>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delivery & Payment Terms -->
|
||||
<div class="terms-section mt-3 pt-3 border-top">
|
||||
<h6>Terms & Conditions</h6>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<small>
|
||||
<strong>Lead Time:</strong>
|
||||
<span :class="leadTimeClass">{{ selectedSupplier.leadTimeDays }} days</span><br>
|
||||
<strong>Min Order:</strong> {{ formatCurrency(selectedSupplier.minOrderAmount) }}<br>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<small>
|
||||
<strong>Payment Terms:</strong>
|
||||
<span class="badge bg-info">{{ selectedSupplier.paymentTerms }}</span><br>
|
||||
<strong>Reliability:</strong>
|
||||
<span :class="reliabilityClass">{{ selectedSupplier.onTimeDeliveryRate }}% on-time</span><br>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reliability Alert -->
|
||||
<div v-if="selectedSupplier.onTimeDeliveryRate < 85" class="alert alert-warning mt-2">
|
||||
⚠️ Delivery Reliability: {{ selectedSupplier.onTimeDeliveryRate }}%
|
||||
(Below 85% standard)
|
||||
</div>
|
||||
|
||||
<!-- Clear Button -->
|
||||
<button
|
||||
class="btn btn-sm btn-outline-secondary mt-2"
|
||||
@click="clearSelection"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useFormatting } from '@/composables/useFormatting'
|
||||
|
||||
interface Supplier {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
phone: string
|
||||
city: string
|
||||
status: string
|
||||
rating: number // 1-5
|
||||
joinedDate: string
|
||||
leadTimeDays: number
|
||||
minOrderAmount: number
|
||||
paymentTerms: string
|
||||
onTimeDeliveryRate: number // 0-100
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string // supplier ID
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
select: [supplier: Supplier]
|
||||
}>()
|
||||
|
||||
const { formatCurrency, formatDate } = useFormatting()
|
||||
|
||||
// State
|
||||
const searchQuery = ref('')
|
||||
const searchResults = ref<Supplier[]>([])
|
||||
const selectedSupplier = ref<Supplier | null>(null)
|
||||
const isSearching = ref(false)
|
||||
const searchError = ref<string | null>(null)
|
||||
|
||||
// Computed
|
||||
const statusClass = computed(() => {
|
||||
if (!selectedSupplier.value) return ''
|
||||
switch (selectedSupplier.value.status) {
|
||||
case 'ACTIVE':
|
||||
return 'badge bg-success'
|
||||
case 'INACTIVE':
|
||||
return 'badge bg-secondary'
|
||||
case 'SUSPENDED':
|
||||
return 'badge bg-danger'
|
||||
case 'TRIAL':
|
||||
return 'badge bg-warning'
|
||||
default:
|
||||
return 'badge bg-secondary'
|
||||
}
|
||||
})
|
||||
|
||||
const leadTimeClass = computed(() => {
|
||||
if (!selectedSupplier.value) return ''
|
||||
if (selectedSupplier.value.leadTimeDays > 14) return 'text-danger'
|
||||
if (selectedSupplier.value.leadTimeDays > 7) return 'text-warning'
|
||||
return 'text-success'
|
||||
})
|
||||
|
||||
const reliabilityClass = computed(() => {
|
||||
if (!selectedSupplier.value) return ''
|
||||
if (selectedSupplier.value.onTimeDeliveryRate < 85) return 'text-danger'
|
||||
if (selectedSupplier.value.onTimeDeliveryRate < 95) return 'text-warning'
|
||||
return 'text-success'
|
||||
})
|
||||
|
||||
// Methods
|
||||
const renderStars = (rating: number): string => {
|
||||
return '★'.repeat(Math.round(rating)) + '☆'.repeat(5 - Math.round(rating))
|
||||
}
|
||||
|
||||
const handleSearch = async (e: Event) => {
|
||||
const input = e.target as HTMLInputElement
|
||||
searchQuery.value = input.value
|
||||
|
||||
if (!searchQuery.value) {
|
||||
searchResults.value = []
|
||||
searchError.value = null
|
||||
return
|
||||
}
|
||||
|
||||
isSearching.value = true
|
||||
searchError.value = null
|
||||
|
||||
try {
|
||||
// Mock API call - would be: await suppliersApi.searchSuppliers(searchQuery.value)
|
||||
const results = await mockSearchSuppliers(searchQuery.value)
|
||||
searchResults.value = results
|
||||
} catch (error) {
|
||||
searchError.value = 'Failed to search suppliers'
|
||||
} finally {
|
||||
isSearching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const selectSupplier = async (supplier: Supplier) => {
|
||||
selectedSupplier.value = supplier
|
||||
searchQuery.value = supplier.name
|
||||
searchResults.value = []
|
||||
|
||||
emit('update:modelValue', supplier.id)
|
||||
emit('select', supplier)
|
||||
}
|
||||
|
||||
const clearSelection = () => {
|
||||
selectedSupplier.value = null
|
||||
searchQuery.value = ''
|
||||
searchResults.value = []
|
||||
emit('update:modelValue', '')
|
||||
}
|
||||
|
||||
// Mock API - would be replaced with real API call
|
||||
const mockSearchSuppliers = async (query: string): Promise<Supplier[]> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const mockSuppliers: Supplier[] = [
|
||||
{
|
||||
id: 'SUP-001',
|
||||
name: 'Premium Electronics Co.',
|
||||
email: 'sales@premium-elec.com',
|
||||
phone: '010-1111-2222',
|
||||
city: 'Seoul',
|
||||
status: 'ACTIVE',
|
||||
rating: 5,
|
||||
joinedDate: '2023-01-15',
|
||||
leadTimeDays: 3,
|
||||
minOrderAmount: 500000,
|
||||
paymentTerms: 'Net 30',
|
||||
onTimeDeliveryRate: 98
|
||||
},
|
||||
{
|
||||
id: 'SUP-002',
|
||||
name: 'Standard Parts Supplier',
|
||||
email: 'contact@standard-parts.com',
|
||||
phone: '010-3333-4444',
|
||||
city: 'Busan',
|
||||
status: 'ACTIVE',
|
||||
rating: 4,
|
||||
joinedDate: '2022-06-20',
|
||||
leadTimeDays: 7,
|
||||
minOrderAmount: 300000,
|
||||
paymentTerms: 'Net 45',
|
||||
onTimeDeliveryRate: 92
|
||||
},
|
||||
{
|
||||
id: 'SUP-003',
|
||||
name: 'Budget Components Ltd',
|
||||
email: 'procurement@budget-comp.com',
|
||||
phone: '010-5555-6666',
|
||||
city: 'Incheon',
|
||||
status: 'ACTIVE',
|
||||
rating: 3,
|
||||
joinedDate: '2021-11-10',
|
||||
leadTimeDays: 14,
|
||||
minOrderAmount: 200000,
|
||||
paymentTerms: 'COD',
|
||||
onTimeDeliveryRate: 78
|
||||
},
|
||||
{
|
||||
id: 'SUP-004',
|
||||
name: 'International Imports Inc',
|
||||
email: 'trade@intl-imports.com',
|
||||
phone: '010-7777-8888',
|
||||
city: 'Seoul',
|
||||
status: 'TRIAL',
|
||||
rating: 4,
|
||||
joinedDate: '2024-08-01',
|
||||
leadTimeDays: 21,
|
||||
minOrderAmount: 1000000,
|
||||
paymentTerms: 'Prepaid',
|
||||
onTimeDeliveryRate: 88
|
||||
}
|
||||
]
|
||||
|
||||
resolve(
|
||||
mockSuppliers.filter((s) =>
|
||||
s.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
s.city.toLowerCase().includes(query.toLowerCase())
|
||||
)
|
||||
)
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.supplier-field {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.btn-outline-secondary {
|
||||
color: #6c757d;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
.btn-outline-secondary:disabled {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.spinner-border {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-width: 0.2em;
|
||||
}
|
||||
|
||||
.supplier-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-top: 0;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
z-index: 1000;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.dropdown-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
small {
|
||||
line-height: 1.8;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.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-warning {
|
||||
background-color: #ffc107 !important;
|
||||
color: #212529 !important;
|
||||
}
|
||||
|
||||
.bg-info {
|
||||
background-color: #17a2b8 !important;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.rating-stars {
|
||||
font-size: 1rem;
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
color: #dc3545;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.d-block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.border-top {
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-3 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.pt-3 {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.terms-section {
|
||||
background-color: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn-outline-secondary:hover:not(:disabled) {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user