- 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>
283 lines
10 KiB
Vue
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>
|