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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user