Files
roaauto-web/frontend/src/components/vehicles/VehiclePicker.vue
Marius Mutu 9db4e746e3 feat: add clients nomenclator, order edit/delete/devalidate, invoice types, dashboard redesign
- New clients table with PF/PJ support, fiscal data (CUI, IBAN, eFactura fields)
- Full CRUD API for clients with search, sync integration
- Order lifecycle: edit header (DRAFT), devalidate (VALIDAT→DRAFT), delete order/invoice
- Invoice types: FACTURA (B2B) vs BON_FISCAL (B2C) with different nr formats
- OrderCreateView redesigned as multi-step flow (client→vehicle→details)
- Autocomplete from catalog_norme/catalog_preturi in OrderLineForm
- Dashboard now combines stats + full orders table with filter tabs and search
- ClientPicker and VehiclePicker with inline creation capability
- Frontend schema aligned with backend (missing columns causing sync errors)
- Mobile responsive fixes for OrderDetailView buttons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 00:36:40 +02:00

283 lines
10 KiB
Vue

<template>
<div class="relative">
<label v-if="label" class="block text-sm font-medium text-gray-700 mb-1">{{ label }}</label>
<input
v-model="query"
type="text"
:placeholder="placeholder"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
@input="onSearch"
@focus="showDropdown = true"
/>
<!-- Selected vehicle display -->
<div v-if="selected" class="mt-1 text-sm text-gray-600">
{{ selected.nr_inmatriculare }} - {{ selected.client_nume }}
<span v-if="selected.marca_denumire"> ({{ selected.marca_denumire }} {{ selected.model_denumire }})</span>
<button @click="clear" class="ml-2 text-red-500 hover:text-red-700 text-xs">Sterge</button>
</div>
<!-- Dropdown results -->
<ul
v-if="showDropdown && results.length > 0"
class="absolute z-10 mt-1 w-full bg-white border border-gray-200 rounded-md shadow-lg max-h-48 overflow-auto"
>
<li
v-for="v in results"
:key="v.id"
class="px-3 py-2 hover:bg-blue-50 cursor-pointer text-sm"
@mousedown="selectVehicle(v)"
>
<span class="font-medium">{{ v.nr_inmatriculare }}</span>
<span class="text-gray-500 ml-2">{{ v.client_nume }}</span>
<span v-if="v.marca_denumire" class="text-gray-400 ml-1">
({{ v.marca_denumire }} {{ v.model_denumire }})
</span>
</li>
</ul>
<!-- No results -->
<div
v-if="showDropdown && query.length >= 2 && results.length === 0 && !loading"
class="absolute z-10 mt-1 w-full bg-white border border-gray-200 rounded-md shadow-lg p-3 text-sm text-gray-500"
>
Niciun vehicul gasit
</div>
<!-- Create new vehicle inline -->
<div v-if="!selected" class="mt-2">
<button
type="button"
@click="showNewVehicle = !showNewVehicle"
class="text-sm text-blue-600 hover:underline"
>
{{ showNewVehicle ? 'Ascunde formular vehicul' : '+ Vehicul nou' }}
</button>
<div v-if="showNewVehicle" class="mt-3 space-y-3 bg-gray-50 rounded-lg p-4">
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-gray-500 mb-1">Nr. inmatriculare</label>
<input v-model="newVehicle.nr_inmatriculare" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" placeholder="B 123 ABC" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Serie sasiu (VIN)</label>
<input v-model="newVehicle.serie_sasiu" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" />
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-gray-500 mb-1">Marca</label>
<div class="flex gap-1">
<select v-model="newVehicle.marca_id" class="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm" @change="onNewMarcaChange">
<option :value="null">-- Selecteaza --</option>
<option v-for="m in marci" :key="m.id" :value="m.id">{{ m.denumire }}</option>
</select>
<div v-if="showNewMarca" class="flex gap-1">
<input v-model="newMarcaName" type="text" placeholder="Marca noua" class="w-28 px-2 py-2 border border-gray-300 rounded-md text-sm" />
<button type="button" @click="createMarca" class="px-2 py-1 bg-green-600 text-white text-xs rounded-md hover:bg-green-700">OK</button>
</div>
<button v-else type="button" @click="showNewMarca = true" class="px-2 py-1 text-blue-600 text-xs hover:underline">+</button>
</div>
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Model</label>
<div class="flex gap-1">
<select v-model="newVehicle.model_id" class="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm">
<option :value="null">-- Selecteaza --</option>
<option v-for="m in modele" :key="m.id" :value="m.id">{{ m.denumire }}</option>
</select>
<div v-if="showNewModel && newVehicle.marca_id" class="flex gap-1">
<input v-model="newModelName" type="text" placeholder="Model nou" class="w-28 px-2 py-2 border border-gray-300 rounded-md text-sm" />
<button type="button" @click="createModel" class="px-2 py-1 bg-green-600 text-white text-xs rounded-md hover:bg-green-700">OK</button>
</div>
<button v-else-if="newVehicle.marca_id" type="button" @click="showNewModel = true" class="px-2 py-1 text-blue-600 text-xs hover:underline">+</button>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-gray-500 mb-1">An fabricatie</label>
<input v-model.number="newVehicle.an_fabricatie" type="number" min="1900" max="2030" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Nume client</label>
<input v-model="newVehicle.client_nume" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" />
</div>
</div>
<button
type="button"
@click="handleCreateVehicle"
:disabled="creatingVehicle"
class="px-4 py-2 bg-green-600 text-white text-sm rounded-md hover:bg-green-700 disabled:opacity-50"
>
{{ creatingVehicle ? 'Se salveaza...' : 'Salveaza vehicul' }}
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, watch, onMounted } from 'vue'
import { useVehiclesStore } from '../../stores/vehicles.js'
import { execSQL, notifyTableChanged } from '../../db/database.js'
import { syncEngine } from '../../db/sync.js'
import { useAuthStore } from '../../stores/auth.js'
const props = defineProps({
modelValue: { type: String, default: null },
label: { type: String, default: 'Vehicul' },
placeholder: { type: String, default: 'Cauta dupa nr. inmatriculare sau client...' },
clientId: { type: String, default: null },
})
const emit = defineEmits(['update:modelValue', 'select'])
const vehiclesStore = useVehiclesStore()
const auth = useAuthStore()
const query = ref('')
const results = ref([])
const selected = ref(null)
const showDropdown = ref(false)
const loading = ref(false)
const showNewVehicle = ref(false)
const creatingVehicle = ref(false)
const marci = ref([])
const modele = ref([])
const showNewMarca = ref(false)
const showNewModel = ref(false)
const newMarcaName = ref('')
const newModelName = ref('')
const newVehicle = reactive({
nr_inmatriculare: '', serie_sasiu: '',
marca_id: null, model_id: null,
an_fabricatie: null, client_nume: '',
})
let searchTimeout = null
function onSearch() {
if (searchTimeout) clearTimeout(searchTimeout)
searchTimeout = setTimeout(async () => {
if (query.value.length < 2) {
results.value = []
return
}
loading.value = true
results.value = await vehiclesStore.search(query.value, props.clientId)
loading.value = false
}, 200)
}
function selectVehicle(v) {
selected.value = v
query.value = v.nr_inmatriculare
showDropdown.value = false
showNewVehicle.value = false
emit('update:modelValue', v.id)
emit('select', v)
}
function clear() {
selected.value = null
query.value = ''
emit('update:modelValue', null)
emit('select', null)
}
async function onNewMarcaChange() {
newVehicle.model_id = null
showNewModel.value = false
newModelName.value = ''
if (newVehicle.marca_id) {
modele.value = await vehiclesStore.getModele(newVehicle.marca_id)
} else {
modele.value = []
}
}
async function createMarca() {
if (!newMarcaName.value.trim()) return
const id = crypto.randomUUID()
const data = { id, tenant_id: auth.tenantId, denumire: newMarcaName.value.trim(), activ: 1 }
await execSQL(
`INSERT INTO catalog_marci (id, tenant_id, denumire, activ) VALUES (?,?,?,?)`,
[id, auth.tenantId, newMarcaName.value.trim(), 1]
)
notifyTableChanged('catalog_marci')
await syncEngine.addToQueue('catalog_marci', id, 'INSERT', data)
marci.value = await vehiclesStore.getMarci()
newVehicle.marca_id = id
showNewMarca.value = false
newMarcaName.value = ''
modele.value = []
}
async function createModel() {
if (!newModelName.value.trim() || !newVehicle.marca_id) return
const id = crypto.randomUUID()
const data = { id, marca_id: newVehicle.marca_id, denumire: newModelName.value.trim() }
await execSQL(
`INSERT INTO catalog_modele (id, marca_id, denumire) VALUES (?,?,?)`,
[id, newVehicle.marca_id, newModelName.value.trim()]
)
notifyTableChanged('catalog_modele')
await syncEngine.addToQueue('catalog_modele', id, 'INSERT', data)
modele.value = await vehiclesStore.getModele(newVehicle.marca_id)
newVehicle.model_id = id
showNewModel.value = false
newModelName.value = ''
}
async function handleCreateVehicle() {
if (!newVehicle.nr_inmatriculare) return
creatingVehicle.value = true
try {
const vehicleData = { ...newVehicle }
if (props.clientId) vehicleData.client_id = props.clientId
const id = await vehiclesStore.create(vehicleData)
const v = await vehiclesStore.getById(id)
if (v) selectVehicle(v)
showNewVehicle.value = false
Object.assign(newVehicle, {
nr_inmatriculare: '', serie_sasiu: '',
marca_id: null, model_id: null,
an_fabricatie: null, client_nume: '',
})
} finally {
creatingVehicle.value = false
}
}
// Load initial vehicle if modelValue is set
watch(() => props.modelValue, async (id) => {
if (id && !selected.value) {
const v = await vehiclesStore.getById(id)
if (v) {
selected.value = v
query.value = v.nr_inmatriculare
}
}
}, { immediate: true })
// When clientId changes, reset selection and load client vehicles
watch(() => props.clientId, async (newClientId, oldClientId) => {
if (newClientId !== oldClientId) {
selected.value = null
query.value = ''
results.value = []
emit('update:modelValue', null)
emit('select', null)
// Auto-load client vehicles
if (newClientId) {
loading.value = true
results.value = await vehiclesStore.getByClient(newClientId)
loading.value = false
if (results.value.length > 0) showDropdown.value = true
}
}
})
onMounted(async () => {
marci.value = await vehiclesStore.getMarci()
})
</script>