Flask + Vue rewrite of the classic-ASP shopdb, built as a plugin framework so sister GE Aerospace sites can adopt it. Product version 0.7.0-pre-0.8.0, plugin contract 0.11.0, 966 backend tests. Key entry points: - docs/INSTALL-WINDOWS-IIS.md - tested Windows + IIS install runbook (own-site and subpath deployment methods) - docs/PILOT-DEPLOY.md - production pilot: stand up + import + verify - docs/IMPORT-API.md - schema-agnostic legacy data import contract - scripts/site_imports/wjf/ - West Jefferson reference import loader - docs/adr/ - twelve accepted architecture decision records
252 lines
5.7 KiB
Vue
252 lines
5.7 KiB
Vue
<template>
|
|
<div class="employee-search">
|
|
<div class="employee-search-container">
|
|
<input
|
|
v-model="searchQuery"
|
|
type="text"
|
|
class="form-control"
|
|
:placeholder="placeholder"
|
|
:disabled="disabled"
|
|
@input="onSearch"
|
|
@keydown.enter.prevent="addCustom"
|
|
/>
|
|
<div v-if="results.length" class="employee-dropdown">
|
|
<div
|
|
v-for="emp in results"
|
|
:key="emp.SSO"
|
|
class="employee-option"
|
|
@click="selectEmployee(emp)"
|
|
>
|
|
<span class="emp-name">{{ emp.First_Name }} {{ emp.Last_Name }}</span>
|
|
<span class="emp-sso">{{ emp.SSO }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<small v-if="allowCustom" class="form-hint">Search for employees or press Enter to add a custom name</small>
|
|
|
|
<!-- Selected employee(s) display -->
|
|
<div v-if="multiple && selectedList.length" class="selected-employees">
|
|
<div
|
|
v-for="(emp, idx) in selectedList"
|
|
:key="idx"
|
|
class="selected-employee"
|
|
>
|
|
<span>{{ emp.name }}</span>
|
|
<span v-if="emp.sso && !emp.sso.startsWith('NAME:')" class="emp-sso-tag">{{ emp.sso }}</span>
|
|
<button v-if="!disabled" type="button" class="btn-remove" @click="removeEmployee(idx)">×</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else-if="!multiple && selected" class="selected-employee-display">
|
|
<span>{{ selected.name }}</span>
|
|
<span v-if="selected.sso" class="emp-sso-tag">{{ selected.sso }}</span>
|
|
<button v-if="!disabled" type="button" class="btn-remove" @click="clearSelection">×</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, watch } from 'vue'
|
|
import { employeesApi } from '../api'
|
|
|
|
const props = defineProps({
|
|
modelValue: { type: [Object, Array], default: null },
|
|
multiple: { type: Boolean, default: false },
|
|
allowCustom: { type: Boolean, default: false },
|
|
placeholder: { type: String, default: 'Search by name...' },
|
|
disabled: { type: Boolean, default: false }
|
|
})
|
|
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
const searchQuery = ref('')
|
|
const results = ref([])
|
|
const selected = ref(null)
|
|
const selectedList = ref([])
|
|
|
|
let searchTimeout = null
|
|
|
|
// Initialize from modelValue
|
|
watch(() => props.modelValue, (val) => {
|
|
if (props.multiple) {
|
|
selectedList.value = val || []
|
|
} else {
|
|
selected.value = val
|
|
}
|
|
}, { immediate: true })
|
|
|
|
async function onSearch() {
|
|
if (searchTimeout) clearTimeout(searchTimeout)
|
|
|
|
const query = searchQuery.value.trim()
|
|
if (query.length < 2) {
|
|
results.value = []
|
|
return
|
|
}
|
|
|
|
searchTimeout = setTimeout(async () => {
|
|
try {
|
|
const response = await employeesApi.search(query)
|
|
results.value = response.data.data || []
|
|
} catch (err) {
|
|
console.error('Employee search error:', err)
|
|
results.value = []
|
|
}
|
|
}, 300)
|
|
}
|
|
|
|
function selectEmployee(emp) {
|
|
const employee = {
|
|
sso: String(emp.SSO),
|
|
name: `${emp.First_Name} ${emp.Last_Name}`.trim()
|
|
}
|
|
|
|
if (props.multiple) {
|
|
// Check if already selected
|
|
if (selectedList.value.some(e => e.sso === employee.sso)) {
|
|
return
|
|
}
|
|
selectedList.value.push(employee)
|
|
emit('update:modelValue', selectedList.value)
|
|
} else {
|
|
selected.value = employee
|
|
emit('update:modelValue', employee)
|
|
}
|
|
|
|
searchQuery.value = ''
|
|
results.value = []
|
|
}
|
|
|
|
function addCustom() {
|
|
if (!props.allowCustom) return
|
|
|
|
const name = searchQuery.value.trim()
|
|
if (!name) return
|
|
|
|
const employee = {
|
|
sso: `NAME:${name}`,
|
|
name: name
|
|
}
|
|
|
|
if (props.multiple) {
|
|
selectedList.value.push(employee)
|
|
emit('update:modelValue', selectedList.value)
|
|
} else {
|
|
selected.value = employee
|
|
emit('update:modelValue', employee)
|
|
}
|
|
|
|
searchQuery.value = ''
|
|
results.value = []
|
|
}
|
|
|
|
function removeEmployee(idx) {
|
|
selectedList.value.splice(idx, 1)
|
|
emit('update:modelValue', selectedList.value)
|
|
}
|
|
|
|
function clearSelection() {
|
|
selected.value = null
|
|
emit('update:modelValue', null)
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.employee-search-container {
|
|
position: relative;
|
|
}
|
|
|
|
.employee-dropdown {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
background: var(--bg-card-solid, #1a1a1a);
|
|
border: 1px solid var(--border);
|
|
border-radius: 0.25rem;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
z-index: 100;
|
|
}
|
|
|
|
.employee-option {
|
|
padding: 0.5rem 0.75rem;
|
|
cursor: pointer;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.employee-option:hover {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.emp-name {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.emp-sso {
|
|
font-size: 0.85rem;
|
|
color: var(--text-light);
|
|
}
|
|
|
|
.form-hint {
|
|
display: block;
|
|
margin-top: 0.25rem;
|
|
font-size: 0.8rem;
|
|
color: var(--text-light);
|
|
}
|
|
|
|
/* Multiple selection */
|
|
.selected-employees {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.selected-employee {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
background: var(--primary);
|
|
color: white;
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
/* Single selection */
|
|
.selected-employee-display {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
background: var(--primary);
|
|
color: white;
|
|
padding: 0.5rem 0.75rem;
|
|
border-radius: 0.25rem;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.emp-sso-tag {
|
|
font-size: 0.8rem;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.btn-remove {
|
|
background: none;
|
|
border: none;
|
|
color: white;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
font-size: 1.2rem;
|
|
line-height: 1;
|
|
opacity: 0.7;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.btn-remove:hover {
|
|
opacity: 1;
|
|
}
|
|
</style>
|