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:
132
frontend/src/components/orders/OrderLineForm.vue
Normal file
132
frontend/src/components/orders/OrderLineForm.vue
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
|
||||||
|
<h4 class="text-sm font-medium text-gray-700 mb-3">Adauga linie</h4>
|
||||||
|
<form @submit.prevent="handleSubmit" class="space-y-3">
|
||||||
|
<!-- Tip -->
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<label class="flex items-center gap-1.5 text-sm">
|
||||||
|
<input v-model="form.tip" type="radio" value="manopera" class="text-blue-600" />
|
||||||
|
Manopera
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-1.5 text-sm">
|
||||||
|
<input v-model="form.tip" type="radio" value="material" class="text-blue-600" />
|
||||||
|
Material
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Descriere -->
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
v-model="form.descriere"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="Descriere operatiune / material"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Manopera fields -->
|
||||||
|
<div v-if="form.tip === 'manopera'" class="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-500 mb-1">Ore</label>
|
||||||
|
<input
|
||||||
|
v-model.number="form.ore"
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
min="0"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-500 mb-1">Pret/ora (RON)</label>
|
||||||
|
<input
|
||||||
|
v-model.number="form.pret_ora"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Material fields -->
|
||||||
|
<div v-if="form.tip === 'material'" class="grid grid-cols-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-500 mb-1">Cantitate</label>
|
||||||
|
<input
|
||||||
|
v-model.number="form.cantitate"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-500 mb-1">Pret unitar (RON)</label>
|
||||||
|
<input
|
||||||
|
v-model.number="form.pret_unitar"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-500 mb-1">UM</label>
|
||||||
|
<input
|
||||||
|
v-model="form.um"
|
||||||
|
type="text"
|
||||||
|
placeholder="buc"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Total preview + submit -->
|
||||||
|
<div class="flex items-center justify-between pt-2">
|
||||||
|
<span class="text-sm text-gray-600">
|
||||||
|
Total: <strong>{{ computedTotal.toFixed(2) }} RON</strong>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Adauga
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, computed } from 'vue'
|
||||||
|
|
||||||
|
const emit = defineEmits(['add'])
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
tip: 'manopera',
|
||||||
|
descriere: '',
|
||||||
|
ore: 0,
|
||||||
|
pret_ora: 0,
|
||||||
|
cantitate: 0,
|
||||||
|
pret_unitar: 0,
|
||||||
|
um: 'buc',
|
||||||
|
})
|
||||||
|
|
||||||
|
const computedTotal = computed(() => {
|
||||||
|
if (form.tip === 'manopera') return (form.ore || 0) * (form.pret_ora || 0)
|
||||||
|
return (form.cantitate || 0) * (form.pret_unitar || 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
if (!form.descriere) return
|
||||||
|
emit('add', { ...form })
|
||||||
|
// Reset
|
||||||
|
form.descriere = ''
|
||||||
|
form.ore = 0
|
||||||
|
form.pret_ora = 0
|
||||||
|
form.cantitate = 0
|
||||||
|
form.pret_unitar = 0
|
||||||
|
form.um = 'buc'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
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>
|
||||||
29
frontend/src/composables/useSync.js
Normal file
29
frontend/src/composables/useSync.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
import { syncEngine } from '../db/sync.js'
|
||||||
|
import { useAuthStore } from '../stores/auth.js'
|
||||||
|
|
||||||
|
export function useSync() {
|
||||||
|
const auth = useAuthStore()
|
||||||
|
let interval = null
|
||||||
|
|
||||||
|
function onFocus() {
|
||||||
|
if (auth.isAuthenticated && syncEngine.online) {
|
||||||
|
syncEngine.incrementalSync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('focus', onFocus)
|
||||||
|
// Periodic sync every 60s
|
||||||
|
interval = setInterval(() => {
|
||||||
|
if (auth.isAuthenticated && syncEngine.online) {
|
||||||
|
syncEngine.incrementalSync()
|
||||||
|
}
|
||||||
|
}, 60000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('focus', onFocus)
|
||||||
|
if (interval) clearInterval(interval)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,10 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-gray-50">
|
<div class="min-h-screen bg-gray-50">
|
||||||
<!-- Desktop sidebar -->
|
<!-- Mobile sidebar overlay -->
|
||||||
<aside class="hidden md:fixed md:inset-y-0 md:flex md:w-56 md:flex-col">
|
<div
|
||||||
|
v-if="mobileMenuOpen"
|
||||||
|
class="md:hidden fixed inset-0 z-40 bg-black/50"
|
||||||
|
@click="mobileMenuOpen = false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Sidebar (desktop always visible, mobile slide-in) -->
|
||||||
|
<aside
|
||||||
|
class="fixed inset-y-0 z-50 flex w-56 flex-col transition-transform md:translate-x-0"
|
||||||
|
:class="mobileMenuOpen ? 'translate-x-0' : '-translate-x-full'"
|
||||||
|
>
|
||||||
<div class="flex flex-col flex-grow bg-gray-900 overflow-y-auto">
|
<div class="flex flex-col flex-grow bg-gray-900 overflow-y-auto">
|
||||||
<div class="px-4 py-5">
|
<div class="px-4 py-5 flex items-center justify-between">
|
||||||
<h1 class="text-xl font-bold text-white">ROA AUTO</h1>
|
<h1 class="text-xl font-bold text-white">ROA AUTO</h1>
|
||||||
|
<button @click="mobileMenuOpen = false" class="md:hidden text-gray-400 hover:text-white">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<nav class="flex-1 px-2 space-y-1">
|
<nav class="flex-1 px-2 space-y-1">
|
||||||
<router-link
|
<router-link
|
||||||
@@ -13,11 +26,13 @@
|
|||||||
:to="item.path"
|
:to="item.path"
|
||||||
class="flex items-center px-3 py-2 text-sm font-medium rounded-md text-gray-300 hover:bg-gray-800 hover:text-white"
|
class="flex items-center px-3 py-2 text-sm font-medium rounded-md text-gray-300 hover:bg-gray-800 hover:text-white"
|
||||||
active-class="!bg-gray-800 !text-white"
|
active-class="!bg-gray-800 !text-white"
|
||||||
|
@click="mobileMenuOpen = false"
|
||||||
>
|
>
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="px-4 py-3 border-t border-gray-700">
|
<div class="px-4 py-3 border-t border-gray-700">
|
||||||
|
<div class="text-xs text-gray-400 mb-1">{{ auth.plan }} plan</div>
|
||||||
<SyncIndicator />
|
<SyncIndicator />
|
||||||
<button
|
<button
|
||||||
@click="logout"
|
@click="logout"
|
||||||
@@ -33,16 +48,19 @@
|
|||||||
<div class="md:pl-56">
|
<div class="md:pl-56">
|
||||||
<!-- Mobile header -->
|
<!-- Mobile header -->
|
||||||
<header class="md:hidden bg-white border-b border-gray-200 px-4 py-3 flex items-center justify-between">
|
<header class="md:hidden bg-white border-b border-gray-200 px-4 py-3 flex items-center justify-between">
|
||||||
|
<button @click="mobileMenuOpen = true" class="text-gray-600 hover:text-gray-900">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>
|
||||||
|
</button>
|
||||||
<h1 class="text-lg font-bold text-gray-900">ROA AUTO</h1>
|
<h1 class="text-lg font-bold text-gray-900">ROA AUTO</h1>
|
||||||
<SyncIndicator />
|
<SyncIndicator />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="p-4 md:p-6">
|
<main class="p-4 md:p-6 pb-20 md:pb-6">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Mobile bottom nav -->
|
<!-- Mobile bottom nav -->
|
||||||
<nav class="md:hidden fixed bottom-0 inset-x-0 bg-white border-t border-gray-200 flex">
|
<nav class="md:hidden fixed bottom-0 inset-x-0 bg-white border-t border-gray-200 flex z-30">
|
||||||
<router-link
|
<router-link
|
||||||
v-for="item in mobileNavItems"
|
v-for="item in mobileNavItems"
|
||||||
:key="item.path"
|
:key="item.path"
|
||||||
@@ -58,12 +76,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from '../stores/auth.js'
|
import { useAuthStore } from '../stores/auth.js'
|
||||||
import SyncIndicator from '../components/common/SyncIndicator.vue'
|
import SyncIndicator from '../components/common/SyncIndicator.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
const mobileMenuOpen = ref(false)
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ path: '/dashboard', label: 'Dashboard' },
|
{ path: '/dashboard', label: 'Dashboard' },
|
||||||
|
|||||||
173
frontend/src/stores/orders.js
Normal file
173
frontend/src/stores/orders.js
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { execSQL, notifyTableChanged } from '../db/database.js'
|
||||||
|
import { syncEngine } from '../db/sync.js'
|
||||||
|
import { useAuthStore } from './auth.js'
|
||||||
|
|
||||||
|
export const useOrdersStore = defineStore('orders', () => {
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
async function getAll(statusFilter = null) {
|
||||||
|
let sql = `SELECT * FROM orders WHERE tenant_id = ? ORDER BY created_at DESC`
|
||||||
|
const params = [auth.tenantId]
|
||||||
|
if (statusFilter) {
|
||||||
|
sql = `SELECT * FROM orders WHERE tenant_id = ? AND status = ? ORDER BY created_at DESC`
|
||||||
|
params.push(statusFilter)
|
||||||
|
}
|
||||||
|
return execSQL(sql, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getById(id) {
|
||||||
|
const rows = await execSQL(`SELECT * FROM orders WHERE id = ?`, [id])
|
||||||
|
return rows[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLines(orderId) {
|
||||||
|
return execSQL(
|
||||||
|
`SELECT * FROM order_lines WHERE order_id = ? ORDER BY ordine, created_at`,
|
||||||
|
[orderId]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRecentOrders(limit = 5) {
|
||||||
|
return execSQL(
|
||||||
|
`SELECT * FROM orders WHERE tenant_id = ? ORDER BY created_at DESC LIMIT ?`,
|
||||||
|
[auth.tenantId, limit]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create(data) {
|
||||||
|
const id = crypto.randomUUID()
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
const nr = `CMD-${Date.now().toString(36).toUpperCase()}`
|
||||||
|
|
||||||
|
// Lookup vehicle info for denormalized fields
|
||||||
|
let clientNume = '', clientTelefon = '', nrAuto = '', marcaDenumire = '', modelDenumire = ''
|
||||||
|
if (data.vehicle_id) {
|
||||||
|
const [v] = await execSQL(`SELECT * FROM vehicles WHERE id = ?`, [data.vehicle_id])
|
||||||
|
if (v) {
|
||||||
|
clientNume = v.client_nume || ''
|
||||||
|
clientTelefon = v.client_telefon || ''
|
||||||
|
nrAuto = v.nr_inmatriculare || ''
|
||||||
|
const [marca] = await execSQL(`SELECT denumire FROM catalog_marci WHERE id = ?`, [v.marca_id])
|
||||||
|
const [model] = await execSQL(`SELECT denumire FROM catalog_modele WHERE id = ?`, [v.model_id])
|
||||||
|
marcaDenumire = marca?.denumire || ''
|
||||||
|
modelDenumire = model?.denumire || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await execSQL(
|
||||||
|
`INSERT INTO orders (id, tenant_id, nr_comanda, data_comanda, vehicle_id, tip_deviz_id, status, km_intrare, observatii, client_nume, client_telefon, nr_auto, marca_denumire, model_denumire, created_at, updated_at)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||||
|
[id, auth.tenantId, nr, now, data.vehicle_id || null, data.tip_deviz_id || null,
|
||||||
|
'DRAFT', data.km_intrare || 0, data.observatii || '',
|
||||||
|
clientNume, clientTelefon, nrAuto, marcaDenumire, modelDenumire, now, now]
|
||||||
|
)
|
||||||
|
notifyTableChanged('orders')
|
||||||
|
await syncEngine.addToQueue('orders', id, 'INSERT', {
|
||||||
|
id, tenant_id: auth.tenantId, nr_comanda: nr, data_comanda: now,
|
||||||
|
vehicle_id: data.vehicle_id, tip_deviz_id: data.tip_deviz_id,
|
||||||
|
status: 'DRAFT', km_intrare: data.km_intrare || 0, observatii: data.observatii || '',
|
||||||
|
client_nume: clientNume, client_telefon: clientTelefon, nr_auto: nrAuto,
|
||||||
|
marca_denumire: marcaDenumire, model_denumire: modelDenumire
|
||||||
|
})
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addLine(orderId, line) {
|
||||||
|
const id = crypto.randomUUID()
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
const total = line.tip === 'manopera'
|
||||||
|
? (line.ore || 0) * (line.pret_ora || 0)
|
||||||
|
: (line.cantitate || 0) * (line.pret_unitar || 0)
|
||||||
|
|
||||||
|
const maxOrdine = await execSQL(
|
||||||
|
`SELECT MAX(ordine) as mx FROM order_lines WHERE order_id = ?`, [orderId]
|
||||||
|
)
|
||||||
|
const ordine = (maxOrdine[0]?.mx || 0) + 1
|
||||||
|
|
||||||
|
await execSQL(
|
||||||
|
`INSERT INTO order_lines (id, order_id, tenant_id, tip, descriere, norma_id, ore, pret_ora, um, cantitate, pret_unitar, total, mecanic_id, ordine, created_at, updated_at)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||||
|
[id, orderId, auth.tenantId, line.tip, line.descriere || '',
|
||||||
|
line.norma_id || null, line.ore || 0, line.pret_ora || 0,
|
||||||
|
line.um || 'buc', line.cantitate || 0, line.pret_unitar || 0,
|
||||||
|
total, line.mecanic_id || null, ordine, now, now]
|
||||||
|
)
|
||||||
|
notifyTableChanged('order_lines')
|
||||||
|
|
||||||
|
// Recalculate order totals
|
||||||
|
await recalcTotals(orderId)
|
||||||
|
|
||||||
|
await syncEngine.addToQueue('order_lines', id, 'INSERT', {
|
||||||
|
id, order_id: orderId, tenant_id: auth.tenantId,
|
||||||
|
tip: line.tip, descriere: line.descriere || '',
|
||||||
|
ore: line.ore || 0, pret_ora: line.pret_ora || 0,
|
||||||
|
um: line.um || 'buc', cantitate: line.cantitate || 0,
|
||||||
|
pret_unitar: line.pret_unitar || 0, total
|
||||||
|
})
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeLine(lineId, orderId) {
|
||||||
|
await execSQL(`DELETE FROM order_lines WHERE id = ?`, [lineId])
|
||||||
|
notifyTableChanged('order_lines')
|
||||||
|
await recalcTotals(orderId)
|
||||||
|
await syncEngine.addToQueue('order_lines', lineId, 'DELETE', {})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recalcTotals(orderId) {
|
||||||
|
const [man] = await execSQL(
|
||||||
|
`SELECT COALESCE(SUM(total), 0) as s FROM order_lines WHERE order_id = ? AND tip = 'manopera'`, [orderId]
|
||||||
|
)
|
||||||
|
const [mat] = await execSQL(
|
||||||
|
`SELECT COALESCE(SUM(total), 0) as s FROM order_lines WHERE order_id = ? AND tip = 'material'`, [orderId]
|
||||||
|
)
|
||||||
|
const totalManopera = man?.s || 0
|
||||||
|
const totalMateriale = mat?.s || 0
|
||||||
|
const totalGeneral = totalManopera + totalMateriale
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
|
||||||
|
await execSQL(
|
||||||
|
`UPDATE orders SET total_manopera=?, total_materiale=?, total_general=?, updated_at=? WHERE id=?`,
|
||||||
|
[totalManopera, totalMateriale, totalGeneral, now, orderId]
|
||||||
|
)
|
||||||
|
notifyTableChanged('orders')
|
||||||
|
await syncEngine.addToQueue('orders', orderId, 'UPDATE', {
|
||||||
|
total_manopera: totalManopera, total_materiale: totalMateriale, total_general: totalGeneral
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateOrder(orderId) {
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
await execSQL(`UPDATE orders SET status='VALIDAT', updated_at=? WHERE id=?`, [now, orderId])
|
||||||
|
notifyTableChanged('orders')
|
||||||
|
await syncEngine.addToQueue('orders', orderId, 'UPDATE', { status: 'VALIDAT' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStats() {
|
||||||
|
const [total] = await execSQL(
|
||||||
|
`SELECT COUNT(*) as cnt FROM orders WHERE tenant_id = ?`, [auth.tenantId]
|
||||||
|
)
|
||||||
|
const [draft] = await execSQL(
|
||||||
|
`SELECT COUNT(*) as cnt FROM orders WHERE tenant_id = ? AND status = 'DRAFT'`, [auth.tenantId]
|
||||||
|
)
|
||||||
|
const [validat] = await execSQL(
|
||||||
|
`SELECT COUNT(*) as cnt FROM orders WHERE tenant_id = ? AND status = 'VALIDAT'`, [auth.tenantId]
|
||||||
|
)
|
||||||
|
const [totalVehicles] = await execSQL(
|
||||||
|
`SELECT COUNT(*) as cnt FROM vehicles WHERE tenant_id = ?`, [auth.tenantId]
|
||||||
|
)
|
||||||
|
const [revenue] = await execSQL(
|
||||||
|
`SELECT COALESCE(SUM(total_general), 0) as s FROM orders WHERE tenant_id = ? AND status = 'VALIDAT'`, [auth.tenantId]
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
totalOrders: total?.cnt || 0,
|
||||||
|
draftOrders: draft?.cnt || 0,
|
||||||
|
validatedOrders: validat?.cnt || 0,
|
||||||
|
totalVehicles: totalVehicles?.cnt || 0,
|
||||||
|
totalRevenue: revenue?.s || 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getAll, getById, getLines, getRecentOrders, create, addLine, removeLine, validateOrder, getStats }
|
||||||
|
})
|
||||||
104
frontend/src/stores/vehicles.js
Normal file
104
frontend/src/stores/vehicles.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { execSQL, notifyTableChanged } from '../db/database.js'
|
||||||
|
import { syncEngine } from '../db/sync.js'
|
||||||
|
import { useAuthStore } from './auth.js'
|
||||||
|
|
||||||
|
export const useVehiclesStore = defineStore('vehicles', () => {
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
async function getAll(search = '') {
|
||||||
|
if (search) {
|
||||||
|
const like = `%${search}%`
|
||||||
|
return execSQL(
|
||||||
|
`SELECT v.*, m.denumire as marca_denumire, mo.denumire as model_denumire
|
||||||
|
FROM vehicles v
|
||||||
|
LEFT JOIN catalog_marci m ON v.marca_id = m.id
|
||||||
|
LEFT JOIN catalog_modele mo ON v.model_id = mo.id
|
||||||
|
WHERE v.tenant_id = ? AND (v.nr_inmatriculare LIKE ? OR v.client_nume LIKE ? OR v.serie_sasiu LIKE ?)
|
||||||
|
ORDER BY v.created_at DESC`,
|
||||||
|
[auth.tenantId, like, like, like]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return execSQL(
|
||||||
|
`SELECT v.*, m.denumire as marca_denumire, mo.denumire as model_denumire
|
||||||
|
FROM vehicles v
|
||||||
|
LEFT JOIN catalog_marci m ON v.marca_id = m.id
|
||||||
|
LEFT JOIN catalog_modele mo ON v.model_id = mo.id
|
||||||
|
WHERE v.tenant_id = ?
|
||||||
|
ORDER BY v.created_at DESC`,
|
||||||
|
[auth.tenantId]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getById(id) {
|
||||||
|
const rows = await execSQL(
|
||||||
|
`SELECT v.*, m.denumire as marca_denumire, mo.denumire as model_denumire
|
||||||
|
FROM vehicles v
|
||||||
|
LEFT JOIN catalog_marci m ON v.marca_id = m.id
|
||||||
|
LEFT JOIN catalog_modele mo ON v.model_id = mo.id
|
||||||
|
WHERE v.id = ?`,
|
||||||
|
[id]
|
||||||
|
)
|
||||||
|
return rows[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create(data) {
|
||||||
|
const id = crypto.randomUUID()
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
|
||||||
|
await execSQL(
|
||||||
|
`INSERT INTO vehicles (id, tenant_id, client_nume, client_telefon, client_email, client_cod_fiscal, client_adresa, nr_inmatriculare, marca_id, model_id, an_fabricatie, serie_sasiu, tip_motor_id, created_at, updated_at)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||||
|
[id, auth.tenantId, data.client_nume || '', data.client_telefon || '',
|
||||||
|
data.client_email || '', data.client_cod_fiscal || '', data.client_adresa || '',
|
||||||
|
data.nr_inmatriculare || '', data.marca_id || null, data.model_id || null,
|
||||||
|
data.an_fabricatie || null, data.serie_sasiu || '', data.tip_motor_id || null, now, now]
|
||||||
|
)
|
||||||
|
notifyTableChanged('vehicles')
|
||||||
|
await syncEngine.addToQueue('vehicles', id, 'INSERT', {
|
||||||
|
id, tenant_id: auth.tenantId, ...data
|
||||||
|
})
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update(id, data) {
|
||||||
|
const sets = Object.keys(data).map(k => `${k} = ?`).join(', ')
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
await execSQL(
|
||||||
|
`UPDATE vehicles SET ${sets}, updated_at = ? WHERE id = ?`,
|
||||||
|
[...Object.values(data), now, id]
|
||||||
|
)
|
||||||
|
notifyTableChanged('vehicles')
|
||||||
|
await syncEngine.addToQueue('vehicles', id, 'UPDATE', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function search(query) {
|
||||||
|
if (!query || query.length < 2) return []
|
||||||
|
const like = `%${query}%`
|
||||||
|
return execSQL(
|
||||||
|
`SELECT v.*, m.denumire as marca_denumire, mo.denumire as model_denumire
|
||||||
|
FROM vehicles v
|
||||||
|
LEFT JOIN catalog_marci m ON v.marca_id = m.id
|
||||||
|
LEFT JOIN catalog_modele mo ON v.model_id = mo.id
|
||||||
|
WHERE v.tenant_id = ? AND (v.nr_inmatriculare LIKE ? OR v.client_nume LIKE ?)
|
||||||
|
ORDER BY v.nr_inmatriculare LIMIT 10`,
|
||||||
|
[auth.tenantId, like, like]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getMarci() {
|
||||||
|
return execSQL(
|
||||||
|
`SELECT * FROM catalog_marci WHERE tenant_id = ? AND activ = 1 ORDER BY denumire`,
|
||||||
|
[auth.tenantId]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getModele(marcaId) {
|
||||||
|
return execSQL(
|
||||||
|
`SELECT * FROM catalog_modele WHERE marca_id = ? ORDER BY denumire`,
|
||||||
|
[marcaId]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getAll, getById, create, update, search, getMarci, getModele }
|
||||||
|
})
|
||||||
@@ -1,6 +1,93 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold text-gray-900 mb-6">Dashboard</h1>
|
<h1 class="text-2xl font-bold text-gray-900 mb-6">Dashboard</h1>
|
||||||
<p class="text-gray-500">Dashboard - va fi implementat in TASK-006.</p>
|
|
||||||
|
<!-- Stats cards -->
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||||
|
<div class="bg-white rounded-lg shadow p-4">
|
||||||
|
<p class="text-sm text-gray-500">Total comenzi</p>
|
||||||
|
<p class="text-2xl font-bold text-gray-900">{{ stats.totalOrders }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow p-4">
|
||||||
|
<p class="text-sm text-gray-500">In lucru (draft)</p>
|
||||||
|
<p class="text-2xl font-bold text-yellow-600">{{ stats.draftOrders }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow p-4">
|
||||||
|
<p class="text-sm text-gray-500">Validate</p>
|
||||||
|
<p class="text-2xl font-bold text-green-600">{{ stats.validatedOrders }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow p-4">
|
||||||
|
<p class="text-sm text-gray-500">Vehicule</p>
|
||||||
|
<p class="text-2xl font-bold text-blue-600">{{ stats.totalVehicles }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Revenue card -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-4 mb-8">
|
||||||
|
<p class="text-sm text-gray-500">Venituri totale (comenzi validate)</p>
|
||||||
|
<p class="text-3xl font-bold text-gray-900">{{ stats.totalRevenue.toFixed(2) }} RON</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent orders -->
|
||||||
|
<div class="bg-white rounded-lg shadow">
|
||||||
|
<div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900">Comenzi recente</h2>
|
||||||
|
<router-link to="/orders" class="text-sm text-blue-600 hover:underline">Vezi toate</router-link>
|
||||||
|
</div>
|
||||||
|
<div v-if="recentOrders.length === 0" class="p-4 text-sm text-gray-500">
|
||||||
|
Nicio comanda inca.
|
||||||
|
</div>
|
||||||
|
<ul v-else class="divide-y divide-gray-100">
|
||||||
|
<li v-for="o in recentOrders" :key="o.id">
|
||||||
|
<router-link
|
||||||
|
:to="`/orders/${o.id}`"
|
||||||
|
class="flex items-center justify-between px-4 py-3 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-gray-900">{{ o.nr_comanda }}</p>
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
{{ o.nr_auto || 'Fara vehicul' }}
|
||||||
|
<span v-if="o.client_nume"> - {{ o.client_nume }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<span
|
||||||
|
class="inline-block px-2 py-0.5 rounded-full text-xs font-medium"
|
||||||
|
:class="statusClass(o.status)"
|
||||||
|
>
|
||||||
|
{{ o.status }}
|
||||||
|
</span>
|
||||||
|
<p class="text-sm font-medium text-gray-900 mt-1">{{ (o.total_general || 0).toFixed(2) }} RON</p>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useOrdersStore } from '../../stores/orders.js'
|
||||||
|
import { useSync } from '../../composables/useSync.js'
|
||||||
|
|
||||||
|
useSync()
|
||||||
|
|
||||||
|
const ordersStore = useOrdersStore()
|
||||||
|
const stats = ref({ totalOrders: 0, draftOrders: 0, validatedOrders: 0, totalVehicles: 0, totalRevenue: 0 })
|
||||||
|
const recentOrders = ref([])
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
stats.value = await ordersStore.getStats()
|
||||||
|
recentOrders.value = await ordersStore.getRecentOrders(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
function statusClass(status) {
|
||||||
|
switch (status) {
|
||||||
|
case 'DRAFT': return 'bg-yellow-100 text-yellow-800'
|
||||||
|
case 'VALIDAT': return 'bg-green-100 text-green-800'
|
||||||
|
case 'FACTURAT': return 'bg-blue-100 text-blue-800'
|
||||||
|
default: return 'bg-gray-100 text-gray-800'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,139 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold text-gray-900 mb-6">Comanda noua</h1>
|
<div class="flex items-center justify-between mb-6">
|
||||||
<p class="text-gray-500">Creare comanda - va fi implementat in TASK-006.</p>
|
<h1 class="text-2xl font-bold text-gray-900">Comanda noua</h1>
|
||||||
|
<router-link to="/orders" class="text-sm text-gray-500 hover:text-gray-700">Inapoi la comenzi</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleCreate" class="bg-white rounded-lg shadow p-6 space-y-4 max-w-2xl">
|
||||||
|
<!-- Vehicle picker -->
|
||||||
|
<VehiclePicker v-model="form.vehicle_id" @select="onVehicleSelect" />
|
||||||
|
|
||||||
|
<!-- Or create new vehicle inline -->
|
||||||
|
<div v-if="!form.vehicle_id" class="border-t pt-4">
|
||||||
|
<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">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>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-500 mb-1">Telefon client</label>
|
||||||
|
<input v-model="newVehicle.client_telefon" type="tel" 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">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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tip deviz -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Tip deviz</label>
|
||||||
|
<select v-model="form.tip_deviz_id" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm">
|
||||||
|
<option :value="null">-- Selecteaza --</option>
|
||||||
|
<option v-for="t in tipuriDeviz" :key="t.id" :value="t.id">{{ t.denumire }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KM + Observatii -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">KM intrare</label>
|
||||||
|
<input v-model.number="form.km_intrare" type="number" min="0" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Observatii</label>
|
||||||
|
<input v-model="form.observatii" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="error" class="text-sm text-red-600">{{ error }}</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="saving"
|
||||||
|
class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{{ saving ? 'Se creeaza...' : 'Creeaza comanda' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useOrdersStore } from '../../stores/orders.js'
|
||||||
|
import { useVehiclesStore } from '../../stores/vehicles.js'
|
||||||
|
import { execSQL } from '../../db/database.js'
|
||||||
|
import { useAuthStore } from '../../stores/auth.js'
|
||||||
|
import VehiclePicker from '../../components/vehicles/VehiclePicker.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const ordersStore = useOrdersStore()
|
||||||
|
const vehiclesStore = useVehiclesStore()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
vehicle_id: null,
|
||||||
|
tip_deviz_id: null,
|
||||||
|
km_intrare: 0,
|
||||||
|
observatii: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const newVehicle = reactive({
|
||||||
|
nr_inmatriculare: '',
|
||||||
|
client_nume: '',
|
||||||
|
client_telefon: '',
|
||||||
|
serie_sasiu: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const showNewVehicle = ref(false)
|
||||||
|
const tipuriDeviz = ref([])
|
||||||
|
const error = ref('')
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
tipuriDeviz.value = await execSQL(
|
||||||
|
`SELECT * FROM catalog_tipuri_deviz WHERE tenant_id = ? ORDER BY denumire`,
|
||||||
|
[auth.tenantId]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function onVehicleSelect(v) {
|
||||||
|
if (v) showNewVehicle.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreate() {
|
||||||
|
error.value = ''
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
// Create new vehicle if needed
|
||||||
|
if (!form.vehicle_id && showNewVehicle.value && newVehicle.nr_inmatriculare) {
|
||||||
|
form.vehicle_id = await vehiclesStore.create({ ...newVehicle })
|
||||||
|
}
|
||||||
|
const id = await ordersStore.create(form)
|
||||||
|
router.push(`/orders/${id}`)
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e.message
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,176 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div v-if="order">
|
||||||
<h1 class="text-2xl font-bold text-gray-900 mb-6">Detalii comanda</h1>
|
<div class="flex items-center justify-between mb-6">
|
||||||
<p class="text-gray-500">Detalii comanda - va fi implementat in TASK-008.</p>
|
<div>
|
||||||
|
<router-link to="/orders" class="text-sm text-gray-500 hover:text-gray-700">Comenzi</router-link>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900">{{ order.nr_comanda }}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
v-if="order.status === 'DRAFT'"
|
||||||
|
@click="handleValidate"
|
||||||
|
class="px-4 py-2 bg-green-600 text-white text-sm rounded-md hover:bg-green-700"
|
||||||
|
>
|
||||||
|
Valideaza
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium"
|
||||||
|
:class="statusClass(order.status)"
|
||||||
|
>
|
||||||
|
{{ order.status }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Order info -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-4 mb-6">
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-500">Nr. auto</p>
|
||||||
|
<p class="font-medium">{{ order.nr_auto || '-' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-500">Client</p>
|
||||||
|
<p class="font-medium">{{ order.client_nume || '-' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-500">Marca / Model</p>
|
||||||
|
<p class="font-medium">{{ order.marca_denumire || '-' }} {{ order.model_denumire || '' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-500">KM intrare</p>
|
||||||
|
<p class="font-medium">{{ order.km_intrare || '-' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="order.observatii" class="mt-3 text-sm">
|
||||||
|
<p class="text-gray-500">Observatii</p>
|
||||||
|
<p>{{ order.observatii }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Order lines -->
|
||||||
|
<div class="bg-white rounded-lg shadow mb-6">
|
||||||
|
<div class="px-4 py-3 border-b border-gray-200">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900">Linii comanda</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="lines.length === 0" class="p-4 text-sm text-gray-500">
|
||||||
|
Nicio linie adaugata.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table v-else class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Tip</th>
|
||||||
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Descriere</th>
|
||||||
|
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase hidden md:table-cell">Cant/Ore</th>
|
||||||
|
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase hidden md:table-cell">Pret</th>
|
||||||
|
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase">Total</th>
|
||||||
|
<th v-if="order.status === 'DRAFT'" class="px-4 py-2 w-10"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-100">
|
||||||
|
<tr v-for="l in lines" :key="l.id">
|
||||||
|
<td class="px-4 py-2 text-xs">
|
||||||
|
<span
|
||||||
|
class="px-1.5 py-0.5 rounded text-xs font-medium"
|
||||||
|
:class="l.tip === 'manopera' ? 'bg-purple-100 text-purple-700' : 'bg-orange-100 text-orange-700'"
|
||||||
|
>
|
||||||
|
{{ l.tip === 'manopera' ? 'MAN' : 'MAT' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 text-sm text-gray-900">{{ l.descriere }}</td>
|
||||||
|
<td class="px-4 py-2 text-sm text-right text-gray-600 hidden md:table-cell">
|
||||||
|
{{ l.tip === 'manopera' ? `${l.ore}h` : `${l.cantitate} ${l.um}` }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 text-sm text-right text-gray-600 hidden md:table-cell">
|
||||||
|
{{ l.tip === 'manopera' ? `${(l.pret_ora || 0).toFixed(2)}/h` : `${(l.pret_unitar || 0).toFixed(2)}/${l.um}` }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2 text-sm text-right font-medium text-gray-900">
|
||||||
|
{{ (l.total || 0).toFixed(2) }}
|
||||||
|
</td>
|
||||||
|
<td v-if="order.status === 'DRAFT'" class="px-4 py-2">
|
||||||
|
<button @click="handleRemoveLine(l.id)" class="text-red-500 hover:text-red-700 text-xs">X</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add line form (only for draft) -->
|
||||||
|
<OrderLineForm v-if="order.status === 'DRAFT'" @add="handleAddLine" class="mb-6" />
|
||||||
|
|
||||||
|
<!-- Totals -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-4">
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<div class="w-64 space-y-1 text-sm">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-500">Manopera:</span>
|
||||||
|
<span class="font-medium">{{ (order.total_manopera || 0).toFixed(2) }} RON</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-500">Materiale:</span>
|
||||||
|
<span class="font-medium">{{ (order.total_materiale || 0).toFixed(2) }} RON</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between border-t pt-1 text-base">
|
||||||
|
<span class="font-semibold">Total general:</span>
|
||||||
|
<span class="font-bold">{{ (order.total_general || 0).toFixed(2) }} RON</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="text-center py-12 text-gray-500">
|
||||||
|
Se incarca...
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useOrdersStore } from '../../stores/orders.js'
|
||||||
|
import { onTableChange } from '../../db/database.js'
|
||||||
|
import OrderLineForm from '../../components/orders/OrderLineForm.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const ordersStore = useOrdersStore()
|
||||||
|
|
||||||
|
const order = ref(null)
|
||||||
|
const lines = ref([])
|
||||||
|
|
||||||
|
async function loadOrder() {
|
||||||
|
order.value = await ordersStore.getById(route.params.id)
|
||||||
|
lines.value = await ordersStore.getLines(route.params.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadOrder()
|
||||||
|
onTableChange('orders', loadOrder)
|
||||||
|
onTableChange('order_lines', async () => {
|
||||||
|
lines.value = await ordersStore.getLines(route.params.id)
|
||||||
|
order.value = await ordersStore.getById(route.params.id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleAddLine(lineData) {
|
||||||
|
await ordersStore.addLine(route.params.id, lineData)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRemoveLine(lineId) {
|
||||||
|
await ordersStore.removeLine(lineId, route.params.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleValidate() {
|
||||||
|
await ordersStore.validateOrder(route.params.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusClass(status) {
|
||||||
|
switch (status) {
|
||||||
|
case 'DRAFT': return 'bg-yellow-100 text-yellow-800'
|
||||||
|
case 'VALIDAT': return 'bg-green-100 text-green-800'
|
||||||
|
case 'FACTURAT': return 'bg-blue-100 text-blue-800'
|
||||||
|
default: return 'bg-gray-100 text-gray-800'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,110 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold text-gray-900 mb-6">Comenzi</h1>
|
<div class="flex items-center justify-between mb-6">
|
||||||
<p class="text-gray-500">Lista comenzi - va fi implementat in TASK-006.</p>
|
<h1 class="text-2xl font-bold text-gray-900">Comenzi</h1>
|
||||||
|
<router-link
|
||||||
|
to="/orders/new"
|
||||||
|
class="px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
+ Comanda noua
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="flex gap-2 mb-4">
|
||||||
|
<button
|
||||||
|
v-for="f in filters"
|
||||||
|
:key="f.value"
|
||||||
|
@click="activeFilter = f.value"
|
||||||
|
class="px-3 py-1.5 text-sm rounded-md"
|
||||||
|
:class="activeFilter === f.value
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50'"
|
||||||
|
>
|
||||||
|
{{ f.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Orders table -->
|
||||||
|
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||||
|
<div v-if="loading" class="p-4 text-center text-gray-500">Se incarca...</div>
|
||||||
|
<div v-else-if="orders.length === 0" class="p-8 text-center text-gray-500">
|
||||||
|
Nicio comanda gasita.
|
||||||
|
</div>
|
||||||
|
<table v-else class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nr. comanda</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nr. auto</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase hidden md:table-cell">Client</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
||||||
|
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-100">
|
||||||
|
<tr
|
||||||
|
v-for="o in orders"
|
||||||
|
:key="o.id"
|
||||||
|
class="hover:bg-gray-50 cursor-pointer"
|
||||||
|
@click="$router.push(`/orders/${o.id}`)"
|
||||||
|
>
|
||||||
|
<td class="px-4 py-3 text-sm font-medium text-gray-900">{{ o.nr_comanda }}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-gray-600">{{ o.nr_auto || '-' }}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-gray-600 hidden md:table-cell">{{ o.client_nume || '-' }}</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<span
|
||||||
|
class="inline-block px-2 py-0.5 rounded-full text-xs font-medium"
|
||||||
|
:class="statusClass(o.status)"
|
||||||
|
>
|
||||||
|
{{ o.status }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-right font-medium text-gray-900">
|
||||||
|
{{ (o.total_general || 0).toFixed(2) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch, onMounted } from 'vue'
|
||||||
|
import { useOrdersStore } from '../../stores/orders.js'
|
||||||
|
import { onTableChange } from '../../db/database.js'
|
||||||
|
|
||||||
|
const ordersStore = useOrdersStore()
|
||||||
|
const orders = ref([])
|
||||||
|
const loading = ref(true)
|
||||||
|
const activeFilter = ref(null)
|
||||||
|
|
||||||
|
const filters = [
|
||||||
|
{ label: 'Toate', value: null },
|
||||||
|
{ label: 'Draft', value: 'DRAFT' },
|
||||||
|
{ label: 'Validate', value: 'VALIDAT' },
|
||||||
|
{ label: 'Facturate', value: 'FACTURAT' },
|
||||||
|
]
|
||||||
|
|
||||||
|
async function loadOrders() {
|
||||||
|
loading.value = true
|
||||||
|
orders.value = await ordersStore.getAll(activeFilter.value)
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(activeFilter, loadOrders)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadOrders()
|
||||||
|
onTableChange('orders', loadOrders)
|
||||||
|
})
|
||||||
|
|
||||||
|
function statusClass(status) {
|
||||||
|
switch (status) {
|
||||||
|
case 'DRAFT': return 'bg-yellow-100 text-yellow-800'
|
||||||
|
case 'VALIDAT': return 'bg-green-100 text-green-800'
|
||||||
|
case 'FACTURAT': return 'bg-blue-100 text-blue-800'
|
||||||
|
default: return 'bg-gray-100 text-gray-800'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,167 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold text-gray-900 mb-6">Vehicule</h1>
|
<div class="flex items-center justify-between mb-6">
|
||||||
<p class="text-gray-500">Lista vehicule - va fi implementat in TASK-006.</p>
|
<h1 class="text-2xl font-bold text-gray-900">Vehicule</h1>
|
||||||
|
<button
|
||||||
|
@click="showForm = !showForm"
|
||||||
|
class="px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
{{ showForm ? 'Anuleaza' : '+ Vehicul nou' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New vehicle form -->
|
||||||
|
<div v-if="showForm" class="bg-white rounded-lg shadow p-6 mb-6 max-w-2xl">
|
||||||
|
<h2 class="text-lg font-semibold mb-4">Vehicul nou</h2>
|
||||||
|
<form @submit.prevent="handleCreate" class="space-y-4">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Nr. inmatriculare</label>
|
||||||
|
<input v-model="form.nr_inmatriculare" type="text" required 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-sm font-medium text-gray-700 mb-1">Serie sasiu (VIN)</label>
|
||||||
|
<input v-model="form.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-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Marca</label>
|
||||||
|
<select v-model="form.marca_id" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" @change="onMarcaChange">
|
||||||
|
<option :value="null">-- Selecteaza --</option>
|
||||||
|
<option v-for="m in marci" :key="m.id" :value="m.id">{{ m.denumire }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Model</label>
|
||||||
|
<select v-model="form.model_id" class="w-full 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>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">An fabricatie</label>
|
||||||
|
<input v-model.number="form.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-sm font-medium text-gray-700 mb-1">Nume client</label>
|
||||||
|
<input v-model="form.client_nume" 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-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Telefon client</label>
|
||||||
|
<input v-model="form.client_telefon" type="tel" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Email client</label>
|
||||||
|
<input v-model="form.client_email" type="email" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" :disabled="saving" class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50">
|
||||||
|
{{ saving ? 'Se salveaza...' : 'Salveaza vehicul' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
type="text"
|
||||||
|
placeholder="Cauta dupa nr. inmatriculare, client..."
|
||||||
|
class="w-full max-w-md px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
@input="onSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Vehicles table -->
|
||||||
|
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||||
|
<div v-if="loading" class="p-4 text-center text-gray-500">Se incarca...</div>
|
||||||
|
<div v-else-if="vehicles.length === 0" class="p-8 text-center text-gray-500">
|
||||||
|
Niciun vehicul gasit.
|
||||||
|
</div>
|
||||||
|
<table v-else class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nr. inmatriculare</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase hidden md:table-cell">Marca / Model</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Client</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase hidden md:table-cell">Telefon</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase hidden md:table-cell">An</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-100">
|
||||||
|
<tr v-for="v in vehicles" :key="v.id" class="hover:bg-gray-50">
|
||||||
|
<td class="px-4 py-3 text-sm font-medium text-gray-900">{{ v.nr_inmatriculare }}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-gray-600 hidden md:table-cell">
|
||||||
|
{{ v.marca_denumire || '-' }} {{ v.model_denumire || '' }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-gray-600">{{ v.client_nume || '-' }}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-gray-600 hidden md:table-cell">{{ v.client_telefon || '-' }}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-gray-600 hidden md:table-cell">{{ v.an_fabricatie || '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { useVehiclesStore } from '../../stores/vehicles.js'
|
||||||
|
import { onTableChange } from '../../db/database.js'
|
||||||
|
|
||||||
|
const vehiclesStore = useVehiclesStore()
|
||||||
|
|
||||||
|
const vehicles = ref([])
|
||||||
|
const loading = ref(true)
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const showForm = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const marci = ref([])
|
||||||
|
const modele = ref([])
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
nr_inmatriculare: '', serie_sasiu: '', marca_id: null, model_id: null,
|
||||||
|
an_fabricatie: null, client_nume: '', client_telefon: '', client_email: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
let searchTimeout = null
|
||||||
|
|
||||||
|
async function loadVehicles() {
|
||||||
|
loading.value = true
|
||||||
|
vehicles.value = await vehiclesStore.getAll(searchQuery.value)
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearch() {
|
||||||
|
if (searchTimeout) clearTimeout(searchTimeout)
|
||||||
|
searchTimeout = setTimeout(loadVehicles, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onMarcaChange() {
|
||||||
|
form.model_id = null
|
||||||
|
if (form.marca_id) {
|
||||||
|
modele.value = await vehiclesStore.getModele(form.marca_id)
|
||||||
|
} else {
|
||||||
|
modele.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreate() {
|
||||||
|
saving.value = true
|
||||||
|
await vehiclesStore.create({ ...form })
|
||||||
|
showForm.value = false
|
||||||
|
Object.assign(form, { nr_inmatriculare: '', serie_sasiu: '', marca_id: null, model_id: null, an_fabricatie: null, client_nume: '', client_telefon: '', client_email: '' })
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
loadVehicles()
|
||||||
|
onTableChange('vehicles', loadVehicles)
|
||||||
|
marci.value = await vehiclesStore.getMarci()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user