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>
This commit is contained in:
2026-03-14 00:36:40 +02:00
parent 3e449d0b0b
commit 9db4e746e3
34 changed files with 2221 additions and 211 deletions

View File

@@ -12,7 +12,7 @@
<!-- 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_denounire }} {{ selected.model_denumire }})</span>
<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 -->
@@ -40,27 +40,117 @@
>
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, watch } from 'vue'
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
@@ -72,7 +162,7 @@ function onSearch() {
return
}
loading.value = true
results.value = await vehiclesStore.search(query.value)
results.value = await vehiclesStore.search(query.value, props.clientId)
loading.value = false
}, 200)
}
@@ -81,6 +171,7 @@ 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)
}
@@ -92,6 +183,70 @@ function clear() {
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) {
@@ -103,6 +258,25 @@ watch(() => props.modelValue, async (id) => {
}
}, { immediate: true })
// Close dropdown on outside click
function onClickOutside() { showDropdown.value = false }
// 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>