feat(frontend): Dashboard + Orders UI + Vehicle Picker + Vehicles list
- Pinia stores: orders (CRUD, line management, totals recalc, stats) and vehicles (CRUD, search, marca/model cascade) - useSync composable: auto-sync on window focus + periodic 60s interval - VehiclePicker component: debounced autocomplete search by nr. inmatriculare or client name - OrderLineForm component: manopera/material toggle with live total preview - DashboardView: stats cards (orders, vehicles, revenue), recent orders list - OrdersListView: filterable table (all/draft/validat/facturat), clickable rows - OrderCreateView: vehicle picker + inline new vehicle form, tip deviz select, km/observatii - OrderDetailView: order info, lines table with add/remove, totals, validate action - VehiclesListView: searchable table, inline create form with marca/model cascade - AppLayout: mobile hamburger menu with slide-in sidebar overlay Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
108
frontend/src/components/vehicles/VehiclePicker.vue
Normal file
108
frontend/src/components/vehicles/VehiclePicker.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<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_denounire }} {{ 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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useVehiclesStore } from '../../stores/vehicles.js'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: null },
|
||||
label: { type: String, default: 'Vehicul' },
|
||||
placeholder: { type: String, default: 'Cauta dupa nr. inmatriculare sau client...' },
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'select'])
|
||||
|
||||
const vehiclesStore = useVehiclesStore()
|
||||
|
||||
const query = ref('')
|
||||
const results = ref([])
|
||||
const selected = ref(null)
|
||||
const showDropdown = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
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)
|
||||
loading.value = false
|
||||
}, 200)
|
||||
}
|
||||
|
||||
function selectVehicle(v) {
|
||||
selected.value = v
|
||||
query.value = v.nr_inmatriculare
|
||||
showDropdown.value = false
|
||||
emit('update:modelValue', v.id)
|
||||
emit('select', v)
|
||||
}
|
||||
|
||||
function clear() {
|
||||
selected.value = null
|
||||
query.value = ''
|
||||
emit('update:modelValue', null)
|
||||
emit('select', null)
|
||||
}
|
||||
|
||||
// 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 })
|
||||
|
||||
// Close dropdown on outside click
|
||||
function onClickOutside() { showDropdown.value = false }
|
||||
</script>
|
||||
Reference in New Issue
Block a user