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

538 lines
12 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="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>