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:
211
frontend/src/components/clients/ClientPicker.vue
Normal file
211
frontend/src/components/clients/ClientPicker.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<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 client display -->
|
||||
<div v-if="selected" class="mt-1 text-sm text-gray-600">
|
||||
{{ displayName(selected) }}
|
||||
<span v-if="selected.cod_fiscal" class="text-gray-400"> ({{ selected.cod_fiscal }})</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="c in results"
|
||||
:key="c.id"
|
||||
class="px-3 py-2 hover:bg-blue-50 cursor-pointer text-sm"
|
||||
@mousedown="selectClient(c)"
|
||||
>
|
||||
<span class="font-medium">{{ displayName(c) }}</span>
|
||||
<span v-if="c.cod_fiscal" class="text-gray-400 ml-2">({{ c.cod_fiscal }})</span>
|
||||
<span v-if="c.telefon" class="text-gray-400 ml-2">{{ c.telefon }}</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 client gasit
|
||||
</div>
|
||||
<!-- Create new client inline -->
|
||||
<div v-if="!selected" class="mt-2">
|
||||
<button
|
||||
type="button"
|
||||
@click="showNewForm = !showNewForm"
|
||||
class="text-sm text-blue-600 hover:underline"
|
||||
>
|
||||
{{ showNewForm ? 'Ascunde formular' : '+ Client nou' }}
|
||||
</button>
|
||||
<div v-if="showNewForm" class="mt-3 space-y-3 bg-gray-50 rounded-lg p-4">
|
||||
<div class="flex gap-4 mb-2">
|
||||
<label class="flex items-center gap-1.5 text-xs">
|
||||
<input v-model="newClient.tip_persoana" type="radio" value="PF" class="text-blue-600" />
|
||||
PF
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 text-xs">
|
||||
<input v-model="newClient.tip_persoana" type="radio" value="PJ" class="text-blue-600" />
|
||||
PJ
|
||||
</label>
|
||||
</div>
|
||||
<template v-if="newClient.tip_persoana === 'PF'">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Nume</label>
|
||||
<input v-model="newClient.nume" type="text" 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">Prenume</label>
|
||||
<input v-model="newClient.prenume" 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">CNP</label>
|
||||
<input v-model="newClient.cod_fiscal" type="text" 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">Telefon</label>
|
||||
<input v-model="newClient.telefon" type="tel" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="newClient.tip_persoana === 'PJ'">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Denumire firma</label>
|
||||
<input v-model="newClient.denumire" type="text" 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">CUI</label>
|
||||
<input v-model="newClient.cod_fiscal" 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</label>
|
||||
<input v-model="newClient.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">Email</label>
|
||||
<input v-model="newClient.email" type="email" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">Adresa</label>
|
||||
<input v-model="newClient.adresa" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="handleCreateClient"
|
||||
:disabled="creatingClient"
|
||||
class="px-4 py-2 bg-green-600 text-white text-sm rounded-md hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
{{ creatingClient ? 'Se salveaza...' : 'Salveaza client' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { useClientsStore } from '../../stores/clients.js'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: null },
|
||||
label: { type: String, default: 'Client' },
|
||||
placeholder: { type: String, default: 'Cauta dupa denumire, CUI, telefon...' },
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'select'])
|
||||
|
||||
const clientsStore = useClientsStore()
|
||||
|
||||
const query = ref('')
|
||||
const results = ref([])
|
||||
const selected = ref(null)
|
||||
const showDropdown = ref(false)
|
||||
const loading = ref(false)
|
||||
const showNewForm = ref(false)
|
||||
const creatingClient = ref(false)
|
||||
|
||||
const newClient = reactive({
|
||||
tip_persoana: 'PF',
|
||||
denumire: '', nume: '', prenume: '',
|
||||
cod_fiscal: '', telefon: '', email: '', adresa: '',
|
||||
})
|
||||
|
||||
let searchTimeout = null
|
||||
|
||||
function displayName(c) {
|
||||
if (c.tip_persoana === 'PJ' && c.denumire) return c.denumire
|
||||
const parts = [c.nume, c.prenume].filter(Boolean)
|
||||
return parts.length > 0 ? parts.join(' ') : (c.denumire || '-')
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
if (searchTimeout) clearTimeout(searchTimeout)
|
||||
searchTimeout = setTimeout(async () => {
|
||||
if (query.value.length < 2) {
|
||||
results.value = []
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
results.value = await clientsStore.search(query.value)
|
||||
loading.value = false
|
||||
}, 200)
|
||||
}
|
||||
|
||||
function selectClient(c) {
|
||||
selected.value = c
|
||||
query.value = displayName(c)
|
||||
showDropdown.value = false
|
||||
showNewForm.value = false
|
||||
emit('update:modelValue', c.id)
|
||||
emit('select', c)
|
||||
}
|
||||
|
||||
function clear() {
|
||||
selected.value = null
|
||||
query.value = ''
|
||||
emit('update:modelValue', null)
|
||||
emit('select', null)
|
||||
}
|
||||
|
||||
async function handleCreateClient() {
|
||||
creatingClient.value = true
|
||||
try {
|
||||
const row = await clientsStore.create({ ...newClient })
|
||||
selectClient(row)
|
||||
showNewForm.value = false
|
||||
Object.assign(newClient, {
|
||||
tip_persoana: 'PF', denumire: '', nume: '', prenume: '',
|
||||
cod_fiscal: '', telefon: '', email: '', adresa: '',
|
||||
})
|
||||
} finally {
|
||||
creatingClient.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Load initial client if modelValue is set
|
||||
watch(() => props.modelValue, async (id) => {
|
||||
if (id && !selected.value) {
|
||||
const c = await clientsStore.getById(id)
|
||||
if (c) {
|
||||
selected.value = c
|
||||
query.value = displayName(c)
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
@@ -14,15 +14,34 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Descriere -->
|
||||
<div>
|
||||
<!-- Descriere with autocomplete -->
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="form.descriere"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Descriere operatiune / material"
|
||||
:placeholder="form.tip === 'manopera' ? 'Cauta norma sau scrie descriere...' : 'Cauta material sau scrie descriere...'"
|
||||
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"
|
||||
@input="onDescriereInput"
|
||||
@focus="showSuggestions = true"
|
||||
/>
|
||||
<!-- Autocomplete dropdown -->
|
||||
<ul
|
||||
v-if="showSuggestions && suggestions.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="s in suggestions"
|
||||
:key="s.id"
|
||||
class="px-3 py-2 hover:bg-blue-50 cursor-pointer text-sm"
|
||||
@mousedown="selectSuggestion(s)"
|
||||
>
|
||||
<span class="font-medium">{{ s.denumire }}</span>
|
||||
<span v-if="s.ansamblu_denumire" class="text-gray-400 ml-2">({{ s.ansamblu_denumire }})</span>
|
||||
<span v-if="s.cod" class="text-gray-400 ml-1">[{{ s.cod }}]</span>
|
||||
<span v-if="s.pret !== undefined" class="text-gray-400 ml-2">{{ (s.pret || 0).toFixed(2) }}/{{ s.um || 'buc' }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Manopera fields -->
|
||||
@@ -99,13 +118,16 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, computed } from 'vue'
|
||||
import { reactive, computed, ref, watch } from 'vue'
|
||||
import { useOrdersStore } from '../../stores/orders.js'
|
||||
|
||||
const emit = defineEmits(['add'])
|
||||
const ordersStore = useOrdersStore()
|
||||
|
||||
const form = reactive({
|
||||
tip: 'manopera',
|
||||
descriere: '',
|
||||
norma_id: null,
|
||||
ore: 0,
|
||||
pret_ora: 0,
|
||||
cantitate: 0,
|
||||
@@ -113,16 +135,57 @@ const form = reactive({
|
||||
um: 'buc',
|
||||
})
|
||||
|
||||
const suggestions = ref([])
|
||||
const showSuggestions = ref(false)
|
||||
let searchTimeout = null
|
||||
|
||||
const computedTotal = computed(() => {
|
||||
if (form.tip === 'manopera') return (form.ore || 0) * (form.pret_ora || 0)
|
||||
return (form.cantitate || 0) * (form.pret_unitar || 0)
|
||||
})
|
||||
|
||||
function onDescriereInput() {
|
||||
if (searchTimeout) clearTimeout(searchTimeout)
|
||||
searchTimeout = setTimeout(async () => {
|
||||
if (form.descriere.length < 2) {
|
||||
suggestions.value = []
|
||||
return
|
||||
}
|
||||
if (form.tip === 'manopera') {
|
||||
suggestions.value = await ordersStore.searchNorme(form.descriere)
|
||||
} else {
|
||||
suggestions.value = await ordersStore.searchPreturi(form.descriere)
|
||||
}
|
||||
showSuggestions.value = true
|
||||
}, 200)
|
||||
}
|
||||
|
||||
function selectSuggestion(s) {
|
||||
showSuggestions.value = false
|
||||
if (form.tip === 'manopera') {
|
||||
form.descriere = s.denumire
|
||||
form.norma_id = s.id
|
||||
form.ore = s.ore_normate || 0
|
||||
} else {
|
||||
form.descriere = s.denumire
|
||||
form.pret_unitar = s.pret || 0
|
||||
form.um = s.um || 'buc'
|
||||
form.cantitate = 1
|
||||
}
|
||||
}
|
||||
|
||||
// Clear suggestions when switching tip
|
||||
watch(() => form.tip, () => {
|
||||
suggestions.value = []
|
||||
showSuggestions.value = false
|
||||
})
|
||||
|
||||
function handleSubmit() {
|
||||
if (!form.descriere) return
|
||||
emit('add', { ...form })
|
||||
// Reset
|
||||
form.descriere = ''
|
||||
form.norma_id = null
|
||||
form.ore = 0
|
||||
form.pret_ora = 0
|
||||
form.cantitate = 0
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,23 +4,50 @@ export const SCHEMA_SQL = `
|
||||
adresa TEXT, telefon TEXT, email TEXT, iban TEXT, banca TEXT,
|
||||
plan TEXT DEFAULT 'free', trial_expires_at TEXT, created_at TEXT, updated_at TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS clients (
|
||||
id TEXT PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
tip_persoana TEXT DEFAULT 'PF',
|
||||
denumire TEXT,
|
||||
nume TEXT,
|
||||
prenume TEXT,
|
||||
cod_fiscal TEXT,
|
||||
reg_com TEXT,
|
||||
telefon TEXT,
|
||||
email TEXT,
|
||||
adresa TEXT,
|
||||
judet TEXT,
|
||||
oras TEXT,
|
||||
cod_postal TEXT,
|
||||
tara TEXT DEFAULT 'RO',
|
||||
cont_iban TEXT,
|
||||
banca TEXT,
|
||||
activ INTEGER DEFAULT 1,
|
||||
oracle_id INTEGER,
|
||||
created_at TEXT,
|
||||
updated_at TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS vehicles (
|
||||
id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL,
|
||||
client_id TEXT,
|
||||
client_nume TEXT, client_telefon TEXT, client_email TEXT,
|
||||
client_cod_fiscal TEXT, client_adresa TEXT,
|
||||
client_cod_fiscal TEXT, client_cui TEXT, client_adresa TEXT,
|
||||
nr_inmatriculare TEXT, marca_id TEXT, model_id TEXT,
|
||||
an_fabricatie INTEGER, serie_sasiu TEXT, tip_motor_id TEXT,
|
||||
an_fabricatie INTEGER, vin TEXT, serie_sasiu TEXT, tip_motor_id TEXT,
|
||||
capacitate_motor TEXT, putere_kw TEXT,
|
||||
oracle_id INTEGER, created_at TEXT, updated_at TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL,
|
||||
nr_comanda TEXT, data_comanda TEXT, vehicle_id TEXT,
|
||||
client_id TEXT,
|
||||
tip_deviz_id TEXT, status TEXT DEFAULT 'DRAFT',
|
||||
km_intrare INTEGER, observatii TEXT,
|
||||
mecanic_id TEXT,
|
||||
client_nume TEXT, client_telefon TEXT, nr_auto TEXT,
|
||||
marca_denumire TEXT, model_denumire TEXT,
|
||||
total_manopera REAL DEFAULT 0, total_materiale REAL DEFAULT 0, total_general REAL DEFAULT 0,
|
||||
token_client TEXT, created_by TEXT, oracle_id INTEGER, created_at TEXT, updated_at TEXT
|
||||
token_client TEXT, status_client TEXT, created_by TEXT, oracle_id INTEGER, created_at TEXT, updated_at TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS order_lines (
|
||||
id TEXT PRIMARY KEY, order_id TEXT NOT NULL, tenant_id TEXT NOT NULL,
|
||||
@@ -32,9 +59,12 @@ export const SCHEMA_SQL = `
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS invoices (
|
||||
id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, order_id TEXT,
|
||||
client_id TEXT,
|
||||
nr_factura TEXT, serie_factura TEXT, data_factura TEXT,
|
||||
tip_document TEXT DEFAULT 'FACTURA',
|
||||
modalitate_plata TEXT, client_nume TEXT, client_cod_fiscal TEXT, nr_auto TEXT,
|
||||
total_fara_tva REAL, tva REAL, total_general REAL,
|
||||
total REAL DEFAULT 0, status TEXT DEFAULT 'EMISA',
|
||||
oracle_id INTEGER, created_at TEXT, updated_at TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS appointments (
|
||||
@@ -46,31 +76,31 @@ export const SCHEMA_SQL = `
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS catalog_marci (
|
||||
id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, denumire TEXT, activ INTEGER DEFAULT 1,
|
||||
oracle_id INTEGER, updated_at TEXT
|
||||
oracle_id INTEGER, created_at TEXT, updated_at TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS catalog_modele (
|
||||
id TEXT PRIMARY KEY, marca_id TEXT, denumire TEXT, oracle_id INTEGER, updated_at TEXT
|
||||
id TEXT PRIMARY KEY, marca_id TEXT, denumire TEXT, oracle_id INTEGER, created_at TEXT, updated_at TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS catalog_ansamble (
|
||||
id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, denumire TEXT, oracle_id INTEGER, updated_at TEXT
|
||||
id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, denumire TEXT, oracle_id INTEGER, created_at TEXT, updated_at TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS catalog_norme (
|
||||
id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, cod TEXT, denumire TEXT,
|
||||
ore_normate REAL, ansamblu_id TEXT, oracle_id INTEGER, updated_at TEXT
|
||||
ore_normate REAL, ansamblu_id TEXT, oracle_id INTEGER, created_at TEXT, updated_at TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS catalog_preturi (
|
||||
id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, denumire TEXT, pret REAL, um TEXT,
|
||||
oracle_id INTEGER, updated_at TEXT
|
||||
oracle_id INTEGER, created_at TEXT, updated_at TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS catalog_tipuri_deviz (
|
||||
id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, denumire TEXT, oracle_id INTEGER, updated_at TEXT
|
||||
id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, denumire TEXT, oracle_id INTEGER, created_at TEXT, updated_at TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS catalog_tipuri_motoare (
|
||||
id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, denumire TEXT, oracle_id INTEGER, updated_at TEXT
|
||||
id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, denumire TEXT, oracle_id INTEGER, created_at TEXT, updated_at TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS mecanici (
|
||||
id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, user_id TEXT,
|
||||
nume TEXT, prenume TEXT, activ INTEGER DEFAULT 1, oracle_id INTEGER, updated_at TEXT
|
||||
nume TEXT, prenume TEXT, activ INTEGER DEFAULT 1, oracle_id INTEGER, created_at TEXT, updated_at TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS _sync_queue (
|
||||
id TEXT PRIMARY KEY, table_name TEXT, row_id TEXT,
|
||||
@@ -82,7 +112,7 @@ export const SCHEMA_SQL = `
|
||||
`;
|
||||
|
||||
export const SYNC_TABLES = [
|
||||
'vehicles', 'orders', 'order_lines', 'invoices', 'appointments',
|
||||
'clients', 'vehicles', 'orders', 'order_lines', 'invoices', 'appointments',
|
||||
'catalog_marci', 'catalog_modele', 'catalog_ansamble', 'catalog_norme',
|
||||
'catalog_preturi', 'catalog_tipuri_deviz', 'catalog_tipuri_motoare', 'mecanici'
|
||||
];
|
||||
|
||||
@@ -87,8 +87,8 @@ const mobileMenuOpen = ref(false)
|
||||
|
||||
const navItems = [
|
||||
{ path: '/dashboard', label: 'Dashboard' },
|
||||
{ path: '/orders', label: 'Comenzi' },
|
||||
{ path: '/invoices', label: 'Facturi' },
|
||||
{ path: '/clients', label: 'Clienti' },
|
||||
{ path: '/vehicles', label: 'Vehicule' },
|
||||
{ path: '/appointments', label: 'Programari' },
|
||||
{ path: '/catalog', label: 'Catalog' },
|
||||
@@ -97,7 +97,7 @@ const navItems = [
|
||||
|
||||
const mobileNavItems = [
|
||||
{ path: '/dashboard', label: 'Acasa' },
|
||||
{ path: '/orders', label: 'Comenzi' },
|
||||
{ path: '/clients', label: 'Clienti' },
|
||||
{ path: '/vehicles', label: 'Vehicule' },
|
||||
{ path: '/appointments', label: 'Programari' },
|
||||
{ path: '/settings', label: 'Setari' },
|
||||
|
||||
@@ -7,9 +7,10 @@ const router = createRouter({
|
||||
{ path: '/login', component: () => import('../views/auth/LoginView.vue'), meta: { layout: 'auth' } },
|
||||
{ path: '/register', component: () => import('../views/auth/RegisterView.vue'), meta: { layout: 'auth' } },
|
||||
{ path: '/dashboard', component: () => import('../views/dashboard/DashboardView.vue'), meta: { requiresAuth: true } },
|
||||
{ path: '/orders', component: () => import('../views/orders/OrdersListView.vue'), meta: { requiresAuth: true } },
|
||||
{ path: '/orders', redirect: '/dashboard' },
|
||||
{ path: '/orders/new', component: () => import('../views/orders/OrderCreateView.vue'), meta: { requiresAuth: true } },
|
||||
{ path: '/orders/:id', component: () => import('../views/orders/OrderDetailView.vue'), meta: { requiresAuth: true } },
|
||||
{ path: '/clients', component: () => import('../views/clients/ClientsListView.vue'), meta: { requiresAuth: true } },
|
||||
{ path: '/vehicles', component: () => import('../views/vehicles/VehiclesListView.vue'), meta: { requiresAuth: true } },
|
||||
{ path: '/appointments', component: () => import('../views/appointments/AppointmentsView.vue'), meta: { requiresAuth: true } },
|
||||
{ path: '/catalog', component: () => import('../views/catalog/CatalogView.vue'), meta: { requiresAuth: true } },
|
||||
|
||||
91
frontend/src/stores/clients.js
Normal file
91
frontend/src/stores/clients.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { execSQL, notifyTableChanged } from '../db/database.js'
|
||||
import { syncEngine } from '../db/sync.js'
|
||||
import { useAuthStore } from './auth.js'
|
||||
|
||||
export const useClientsStore = defineStore('clients', () => {
|
||||
const auth = useAuthStore()
|
||||
|
||||
async function getAll(search = '') {
|
||||
let sql = `SELECT * FROM clients WHERE tenant_id = ?`
|
||||
const params = [auth.tenantId]
|
||||
if (search && search.length >= 2) {
|
||||
sql += ` AND (denumire LIKE ? OR cod_fiscal LIKE ? OR telefon LIKE ? OR nume LIKE ? OR prenume LIKE ?)`
|
||||
const like = `%${search}%`
|
||||
params.push(like, like, like, like, like)
|
||||
}
|
||||
sql += ` ORDER BY denumire ASC`
|
||||
return await execSQL(sql, params)
|
||||
}
|
||||
|
||||
async function getById(id) {
|
||||
const rows = await execSQL(`SELECT * FROM clients WHERE id = ? AND tenant_id = ?`, [id, auth.tenantId])
|
||||
return rows[0] || null
|
||||
}
|
||||
|
||||
async function create(data) {
|
||||
const id = data.id || crypto.randomUUID()
|
||||
const now = new Date().toISOString()
|
||||
const row = {
|
||||
id, tenant_id: auth.tenantId,
|
||||
tip_persoana: data.tip_persoana || 'PF',
|
||||
denumire: data.denumire || null,
|
||||
nume: data.nume || null,
|
||||
prenume: data.prenume || null,
|
||||
cod_fiscal: data.cod_fiscal || null,
|
||||
reg_com: data.reg_com || null,
|
||||
telefon: data.telefon || null,
|
||||
email: data.email || null,
|
||||
adresa: data.adresa || null,
|
||||
judet: data.judet || null,
|
||||
oras: data.oras || null,
|
||||
cod_postal: data.cod_postal || null,
|
||||
tara: data.tara || 'RO',
|
||||
cont_iban: data.cont_iban || null,
|
||||
banca: data.banca || null,
|
||||
activ: data.activ ?? 1,
|
||||
oracle_id: data.oracle_id || null,
|
||||
created_at: now, updated_at: now
|
||||
}
|
||||
await execSQL(
|
||||
`INSERT INTO clients (id, tenant_id, tip_persoana, denumire, nume, prenume, cod_fiscal, reg_com, telefon, email, adresa, judet, oras, cod_postal, tara, cont_iban, banca, activ, oracle_id, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[row.id, row.tenant_id, row.tip_persoana, row.denumire, row.nume, row.prenume, row.cod_fiscal, row.reg_com, row.telefon, row.email, row.adresa, row.judet, row.oras, row.cod_postal, row.tara, row.cont_iban, row.banca, row.activ, row.oracle_id, row.created_at, row.updated_at]
|
||||
)
|
||||
await syncEngine.addToQueue('clients', id, 'INSERT', row)
|
||||
notifyTableChanged('clients')
|
||||
return row
|
||||
}
|
||||
|
||||
async function update(id, data) {
|
||||
const fields = []
|
||||
const values = []
|
||||
for (const [key, val] of Object.entries(data)) {
|
||||
if (val !== undefined && key !== 'id' && key !== 'tenant_id') {
|
||||
fields.push(`${key} = ?`)
|
||||
values.push(val)
|
||||
}
|
||||
}
|
||||
if (fields.length === 0) return
|
||||
const now = new Date().toISOString()
|
||||
fields.push('updated_at = ?')
|
||||
values.push(now)
|
||||
values.push(id, auth.tenantId)
|
||||
await execSQL(`UPDATE clients SET ${fields.join(', ')} WHERE id = ? AND tenant_id = ?`, values)
|
||||
const updated = await getById(id)
|
||||
await syncEngine.addToQueue('clients', id, 'UPDATE', updated)
|
||||
notifyTableChanged('clients')
|
||||
return updated
|
||||
}
|
||||
|
||||
async function search(query) {
|
||||
if (!query || query.length < 2) return []
|
||||
const like = `%${query}%`
|
||||
return await execSQL(
|
||||
`SELECT * FROM clients WHERE tenant_id = ? AND (denumire LIKE ? OR cod_fiscal LIKE ? OR telefon LIKE ? OR nume LIKE ? OR prenume LIKE ?) LIMIT 10`,
|
||||
[auth.tenantId, like, like, like, like, like]
|
||||
)
|
||||
}
|
||||
|
||||
return { getAll, getById, create, update, search }
|
||||
})
|
||||
@@ -6,13 +6,19 @@ 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`
|
||||
async function getAll(statusFilter = null, search = '') {
|
||||
let sql = `SELECT * FROM orders WHERE tenant_id = ?`
|
||||
const params = [auth.tenantId]
|
||||
if (statusFilter) {
|
||||
sql = `SELECT * FROM orders WHERE tenant_id = ? AND status = ? ORDER BY created_at DESC`
|
||||
sql += ` AND status = ?`
|
||||
params.push(statusFilter)
|
||||
}
|
||||
if (search && search.length >= 2) {
|
||||
sql += ` AND (nr_auto LIKE ? OR client_nume LIKE ? OR nr_comanda LIKE ?)`
|
||||
const like = `%${search}%`
|
||||
params.push(like, like, like)
|
||||
}
|
||||
sql += ` ORDER BY created_at DESC`
|
||||
return execSQL(sql, params)
|
||||
}
|
||||
|
||||
@@ -40,13 +46,23 @@ export const useOrdersStore = defineStore('orders', () => {
|
||||
const now = new Date().toISOString()
|
||||
const nr = `CMD-${Date.now().toString(36).toUpperCase()}`
|
||||
|
||||
// Lookup client info for denormalized fields
|
||||
let clientNume = '', clientTelefon = ''
|
||||
if (data.client_id) {
|
||||
const [c] = await execSQL(`SELECT * FROM clients WHERE id = ?`, [data.client_id])
|
||||
if (c) {
|
||||
clientNume = c.tip_persoana === 'PJ' ? (c.denumire || '') : [c.nume, c.prenume].filter(Boolean).join(' ')
|
||||
clientTelefon = c.telefon || ''
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup vehicle info for denormalized fields
|
||||
let clientNume = '', clientTelefon = '', nrAuto = '', marcaDenumire = '', modelDenumire = ''
|
||||
let 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 || ''
|
||||
if (!clientNume) clientNume = v.client_nume || ''
|
||||
if (!clientTelefon) 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])
|
||||
@@ -56,16 +72,18 @@ export const useOrdersStore = defineStore('orders', () => {
|
||||
}
|
||||
|
||||
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,
|
||||
`INSERT INTO orders (id, tenant_id, nr_comanda, data_comanda, vehicle_id, client_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.client_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,
|
||||
vehicle_id: data.vehicle_id, client_id: data.client_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
|
||||
@@ -137,6 +155,49 @@ export const useOrdersStore = defineStore('orders', () => {
|
||||
})
|
||||
}
|
||||
|
||||
async function updateHeader(orderId, data) {
|
||||
const now = new Date().toISOString()
|
||||
|
||||
// Re-denormalize client and vehicle info if IDs changed
|
||||
let clientNume = data.client_nume, clientTelefon = data.client_telefon
|
||||
let nrAuto = data.nr_auto, marcaDenumire = data.marca_denumire, modelDenumire = data.model_denumire
|
||||
|
||||
if (data.client_id) {
|
||||
const [c] = await execSQL(`SELECT * FROM clients WHERE id = ?`, [data.client_id])
|
||||
if (c) {
|
||||
clientNume = c.tip_persoana === 'PJ' ? (c.denumire || '') : [c.nume, c.prenume].filter(Boolean).join(' ')
|
||||
clientTelefon = c.telefon || ''
|
||||
}
|
||||
}
|
||||
if (data.vehicle_id) {
|
||||
const [v] = await execSQL(`SELECT * FROM vehicles WHERE id = ?`, [data.vehicle_id])
|
||||
if (v) {
|
||||
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(
|
||||
`UPDATE orders SET client_id=?, vehicle_id=?, tip_deviz_id=?, km_intrare=?, observatii=?,
|
||||
client_nume=?, client_telefon=?, nr_auto=?, marca_denumire=?, model_denumire=?, updated_at=?
|
||||
WHERE id=?`,
|
||||
[data.client_id || null, data.vehicle_id || null, data.tip_deviz_id || null,
|
||||
data.km_intrare || 0, data.observatii || '',
|
||||
clientNume || '', clientTelefon || '', nrAuto || '', marcaDenumire || '', modelDenumire || '',
|
||||
now, orderId]
|
||||
)
|
||||
notifyTableChanged('orders')
|
||||
await syncEngine.addToQueue('orders', orderId, 'UPDATE', {
|
||||
client_id: data.client_id, vehicle_id: data.vehicle_id, tip_deviz_id: data.tip_deviz_id,
|
||||
km_intrare: data.km_intrare, observatii: data.observatii,
|
||||
client_nume: clientNume, client_telefon: clientTelefon, nr_auto: nrAuto,
|
||||
marca_denumire: marcaDenumire, model_denumire: modelDenumire
|
||||
})
|
||||
}
|
||||
|
||||
async function validateOrder(orderId) {
|
||||
const now = new Date().toISOString()
|
||||
await execSQL(`UPDATE orders SET status='VALIDAT', updated_at=? WHERE id=?`, [now, orderId])
|
||||
@@ -144,6 +205,59 @@ export const useOrdersStore = defineStore('orders', () => {
|
||||
await syncEngine.addToQueue('orders', orderId, 'UPDATE', { status: 'VALIDAT' })
|
||||
}
|
||||
|
||||
async function devalidateOrder(orderId) {
|
||||
const now = new Date().toISOString()
|
||||
await execSQL(`UPDATE orders SET status='DRAFT', updated_at=? WHERE id=?`, [now, orderId])
|
||||
notifyTableChanged('orders')
|
||||
await syncEngine.addToQueue('orders', orderId, 'UPDATE', { status: 'DRAFT' })
|
||||
}
|
||||
|
||||
async function deleteOrder(orderId) {
|
||||
await execSQL(`DELETE FROM order_lines WHERE order_id = ?`, [orderId])
|
||||
await execSQL(`DELETE FROM orders WHERE id = ?`, [orderId])
|
||||
notifyTableChanged('order_lines')
|
||||
notifyTableChanged('orders')
|
||||
await syncEngine.addToQueue('orders', orderId, 'DELETE', {})
|
||||
}
|
||||
|
||||
async function deleteInvoice(invoiceId) {
|
||||
const [inv] = await execSQL(`SELECT * FROM invoices WHERE id = ?`, [invoiceId])
|
||||
if (!inv) return
|
||||
// Set order back to VALIDAT
|
||||
if (inv.order_id) {
|
||||
const now = new Date().toISOString()
|
||||
await execSQL(`UPDATE orders SET status='VALIDAT', updated_at=? WHERE id=?`, [now, inv.order_id])
|
||||
notifyTableChanged('orders')
|
||||
await syncEngine.addToQueue('orders', inv.order_id, 'UPDATE', { status: 'VALIDAT' })
|
||||
}
|
||||
await execSQL(`DELETE FROM invoices WHERE id = ?`, [invoiceId])
|
||||
notifyTableChanged('invoices')
|
||||
await syncEngine.addToQueue('invoices', invoiceId, 'DELETE', {})
|
||||
}
|
||||
|
||||
async function searchNorme(query) {
|
||||
if (!query || query.length < 2) return []
|
||||
const like = `%${query}%`
|
||||
return execSQL(
|
||||
`SELECT n.*, a.denumire as ansamblu_denumire
|
||||
FROM catalog_norme n
|
||||
LEFT JOIN catalog_ansamble a ON n.ansamblu_id = a.id
|
||||
WHERE n.tenant_id = ? AND (n.denumire LIKE ? OR n.cod LIKE ?)
|
||||
ORDER BY n.denumire LIMIT 10`,
|
||||
[auth.tenantId, like, like]
|
||||
)
|
||||
}
|
||||
|
||||
async function searchPreturi(query) {
|
||||
if (!query || query.length < 2) return []
|
||||
const like = `%${query}%`
|
||||
return execSQL(
|
||||
`SELECT * FROM catalog_preturi WHERE tenant_id = ? AND denumire LIKE ?
|
||||
ORDER BY denumire LIMIT 10`,
|
||||
[auth.tenantId, like]
|
||||
)
|
||||
}
|
||||
|
||||
async function getStats() {
|
||||
const [total] = await execSQL(
|
||||
`SELECT COUNT(*) as cnt FROM orders WHERE tenant_id = ?`, [auth.tenantId]
|
||||
@@ -154,20 +268,28 @@ export const useOrdersStore = defineStore('orders', () => {
|
||||
const [validat] = await execSQL(
|
||||
`SELECT COUNT(*) as cnt FROM orders WHERE tenant_id = ? AND status = 'VALIDAT'`, [auth.tenantId]
|
||||
)
|
||||
const [facturat] = await execSQL(
|
||||
`SELECT COUNT(*) as cnt FROM orders WHERE tenant_id = ? AND status = 'FACTURAT'`, [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]
|
||||
`SELECT COALESCE(SUM(total_general), 0) as s FROM orders WHERE tenant_id = ? AND status IN ('VALIDAT', 'FACTURAT')`, [auth.tenantId]
|
||||
)
|
||||
return {
|
||||
totalOrders: total?.cnt || 0,
|
||||
draftOrders: draft?.cnt || 0,
|
||||
validatedOrders: validat?.cnt || 0,
|
||||
facturatedOrders: facturat?.cnt || 0,
|
||||
totalVehicles: totalVehicles?.cnt || 0,
|
||||
totalRevenue: revenue?.s || 0,
|
||||
}
|
||||
}
|
||||
|
||||
return { getAll, getById, getLines, getRecentOrders, create, addLine, removeLine, validateOrder, getStats }
|
||||
return {
|
||||
getAll, getById, getLines, getRecentOrders, create, addLine, removeLine,
|
||||
updateHeader, validateOrder, devalidateOrder, deleteOrder, deleteInvoice,
|
||||
searchNorme, searchPreturi, getStats
|
||||
}
|
||||
})
|
||||
|
||||
@@ -42,14 +42,26 @@ export const useVehiclesStore = defineStore('vehicles', () => {
|
||||
return rows[0] || null
|
||||
}
|
||||
|
||||
async function getByClient(clientId) {
|
||||
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.client_id = ? AND v.tenant_id = ?
|
||||
ORDER BY v.nr_inmatriculare`,
|
||||
[clientId, auth.tenantId]
|
||||
)
|
||||
}
|
||||
|
||||
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 || '',
|
||||
`INSERT INTO vehicles (id, tenant_id, client_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_id || null, 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]
|
||||
@@ -72,9 +84,24 @@ export const useVehiclesStore = defineStore('vehicles', () => {
|
||||
await syncEngine.addToQueue('vehicles', id, 'UPDATE', data)
|
||||
}
|
||||
|
||||
async function search(query) {
|
||||
if (!query || query.length < 2) return []
|
||||
async function search(query, clientId = null) {
|
||||
if (!query || query.length < 2) {
|
||||
// If no query but clientId, return client vehicles
|
||||
if (clientId) return getByClient(clientId)
|
||||
return []
|
||||
}
|
||||
const like = `%${query}%`
|
||||
if (clientId) {
|
||||
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.client_id = ? AND (v.nr_inmatriculare LIKE ? OR v.client_nume LIKE ?)
|
||||
ORDER BY v.nr_inmatriculare LIMIT 10`,
|
||||
[auth.tenantId, clientId, like, like]
|
||||
)
|
||||
}
|
||||
return execSQL(
|
||||
`SELECT v.*, m.denumire as marca_denumire, mo.denumire as model_denumire
|
||||
FROM vehicles v
|
||||
@@ -100,5 +127,5 @@ export const useVehiclesStore = defineStore('vehicles', () => {
|
||||
)
|
||||
}
|
||||
|
||||
return { getAll, getById, create, update, search, getMarci, getModele }
|
||||
return { getAll, getById, getByClient, create, update, search, getMarci, getModele }
|
||||
})
|
||||
|
||||
@@ -128,6 +128,51 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Norme -->
|
||||
<div v-if="activeTab === 'norme'">
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden mb-6">
|
||||
<div v-if="norme.length === 0" class="p-8 text-center text-gray-500">Nicio norma 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">Cod</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Denumire</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase hidden md:table-cell">Ansamblu</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Ore normate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
<tr v-for="n in norme" :key="n.id" class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3 text-sm text-gray-500">{{ n.cod || '-' }}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-900">{{ n.denumire }}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-500 hidden md:table-cell">{{ n.ansamblu_denumire || '-' }}</td>
|
||||
<td class="px-4 py-3 text-sm text-right font-medium text-gray-900">{{ n.ore_normate || '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Add Norma form -->
|
||||
<div class="bg-white rounded-lg shadow p-4 max-w-lg">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-3">Adauga norma</h3>
|
||||
<form @submit.prevent="addNorma" class="space-y-2">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<input v-model="newNorma.cod" type="text" placeholder="Cod" class="px-3 py-2 border border-gray-300 rounded-md text-sm" />
|
||||
<select v-model="newNorma.ansamblu_id" class="px-3 py-2 border border-gray-300 rounded-md text-sm">
|
||||
<option :value="null">-- Ansamblu --</option>
|
||||
<option v-for="a in ansamble" :key="a.id" :value="a.id">{{ a.denumire }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input v-model="newNorma.denumire" type="text" required placeholder="Denumire norma" class="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm" />
|
||||
<input v-model.number="newNorma.ore_normate" type="number" step="0.1" min="0" placeholder="Ore" class="w-20 px-3 py-2 border border-gray-300 rounded-md text-sm" />
|
||||
<button type="submit" :disabled="savingNorma" class="px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700 disabled:opacity-50">
|
||||
{{ savingNorma ? '...' : 'Adauga' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Preturi -->
|
||||
<div v-if="activeTab === 'preturi'">
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden mb-6">
|
||||
@@ -208,6 +253,7 @@ const tabs = [
|
||||
{ key: 'marci', label: 'Marci' },
|
||||
{ key: 'modele', label: 'Modele' },
|
||||
{ key: 'ansamble', label: 'Ansamble' },
|
||||
{ key: 'norme', label: 'Norme' },
|
||||
{ key: 'preturi', label: 'Preturi' },
|
||||
{ key: 'tipuri_deviz', label: 'Tipuri deviz' },
|
||||
]
|
||||
@@ -327,6 +373,43 @@ async function addAnsamblu() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Norme ----
|
||||
const norme = ref([])
|
||||
const newNorma = reactive({ cod: '', denumire: '', ore_normate: null, ansamblu_id: null })
|
||||
const savingNorma = ref(false)
|
||||
|
||||
async function loadNorme() {
|
||||
norme.value = await execSQL(
|
||||
`SELECT n.*, a.denumire AS ansamblu_denumire
|
||||
FROM catalog_norme n
|
||||
LEFT JOIN catalog_ansamble a ON a.id = n.ansamblu_id
|
||||
WHERE n.tenant_id=?
|
||||
ORDER BY n.denumire`,
|
||||
[auth.tenantId]
|
||||
)
|
||||
}
|
||||
|
||||
async function addNorma() {
|
||||
savingNorma.value = true
|
||||
try {
|
||||
const id = crypto.randomUUID()
|
||||
const data = {
|
||||
id, tenant_id: auth.tenantId, cod: newNorma.cod || null,
|
||||
denumire: newNorma.denumire, ore_normate: newNorma.ore_normate || 0,
|
||||
ansamblu_id: newNorma.ansamblu_id || null
|
||||
}
|
||||
await execSQL(
|
||||
`INSERT INTO catalog_norme (id, tenant_id, cod, denumire, ore_normate, ansamblu_id) VALUES (?,?,?,?,?,?)`,
|
||||
[id, auth.tenantId, data.cod, data.denumire, data.ore_normate, data.ansamblu_id]
|
||||
)
|
||||
notifyTableChanged('catalog_norme')
|
||||
syncEngine.addToQueue('catalog_norme', id, 'INSERT', data)
|
||||
Object.assign(newNorma, { cod: '', denumire: '', ore_normate: null, ansamblu_id: null })
|
||||
} finally {
|
||||
savingNorma.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Preturi ----
|
||||
const preturi = ref([])
|
||||
const newPret = reactive({ denumire: '', pret: null, um: 'ora' })
|
||||
@@ -390,6 +473,7 @@ async function addTipDeviz() {
|
||||
onTableChange('catalog_marci', loadMarci)
|
||||
onTableChange('catalog_modele', loadModele)
|
||||
onTableChange('catalog_ansamble', loadAnsamble)
|
||||
onTableChange('catalog_norme', loadNorme)
|
||||
onTableChange('catalog_preturi', loadPreturi)
|
||||
onTableChange('catalog_tipuri_deviz', loadTipuriDeviz)
|
||||
|
||||
@@ -398,6 +482,7 @@ watch(activeTab, (tab) => {
|
||||
if (tab === 'marci') loadMarci()
|
||||
else if (tab === 'modele') { loadMarci(); loadModele() }
|
||||
else if (tab === 'ansamble') loadAnsamble()
|
||||
else if (tab === 'norme') { loadAnsamble(); loadNorme() }
|
||||
else if (tab === 'preturi') loadPreturi()
|
||||
else if (tab === 'tipuri_deviz') loadTipuriDeviz()
|
||||
})
|
||||
|
||||
287
frontend/src/views/clients/ClientsListView.vue
Normal file
287
frontend/src/views/clients/ClientsListView.vue
Normal file
@@ -0,0 +1,287 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Clienti</h1>
|
||||
<button
|
||||
@click="showForm = !showForm; editingId = null"
|
||||
class="px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700"
|
||||
>
|
||||
{{ showForm ? 'Anuleaza' : '+ Adauga client' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- New / Edit client 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">{{ editingId ? 'Editeaza client' : 'Client nou' }}</h2>
|
||||
<form @submit.prevent="handleSave" class="space-y-4">
|
||||
<!-- Tip persoana toggle -->
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center gap-1.5 text-sm">
|
||||
<input v-model="form.tip_persoana" type="radio" value="PF" class="text-blue-600" />
|
||||
Persoana fizica (PF)
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 text-sm">
|
||||
<input v-model="form.tip_persoana" type="radio" value="PJ" class="text-blue-600" />
|
||||
Persoana juridica (PJ)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- PF fields -->
|
||||
<template v-if="form.tip_persoana === 'PF'">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Nume</label>
|
||||
<input v-model="form.nume" type="text" 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">Prenume</label>
|
||||
<input v-model="form.prenume" 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">CNP</label>
|
||||
<input v-model="form.cod_fiscal" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" placeholder="CNP" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Telefon</label>
|
||||
<input v-model="form.telefon" type="tel" 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">Email</label>
|
||||
<input v-model="form.email" type="email" 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">Adresa</label>
|
||||
<input v-model="form.adresa" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- PJ fields -->
|
||||
<template v-if="form.tip_persoana === 'PJ'">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Denumire firma</label>
|
||||
<input v-model="form.denumire" type="text" 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">CUI</label>
|
||||
<input v-model="form.cod_fiscal" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" placeholder="RO12345678" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Reg. comertului</label>
|
||||
<input v-model="form.reg_com" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" placeholder="J40/1234/2020" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Telefon</label>
|
||||
<input v-model="form.telefon" type="tel" 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">Email</label>
|
||||
<input v-model="form.email" type="email" 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">Adresa</label>
|
||||
<input v-model="form.adresa" 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">IBAN</label>
|
||||
<input v-model="form.cont_iban" type="text" 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">Banca</label>
|
||||
<input v-model="form.banca" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<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...' : (editingId ? 'Salveaza modificarile' : 'Salveaza client') }}
|
||||
</button>
|
||||
<button v-if="editingId" type="button" @click="cancelEdit" class="px-4 py-2 text-gray-600 hover:text-gray-800 text-sm">
|
||||
Anuleaza
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="mb-4">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Cauta dupa denumire, CUI, telefon..."
|
||||
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>
|
||||
|
||||
<!-- Clients 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="clients.length === 0" class="p-8 text-center text-gray-500">
|
||||
Niciun client 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">Denumire</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase hidden md:table-cell">CUI/CNP</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Telefon</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase hidden md:table-cell">Email</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Tip</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
<tr
|
||||
v-for="c in clients"
|
||||
:key="c.id"
|
||||
class="hover:bg-gray-50 cursor-pointer"
|
||||
@click="startEdit(c)"
|
||||
>
|
||||
<td class="px-4 py-3 text-sm font-medium text-gray-900">
|
||||
{{ clientDisplayName(c) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-600 hidden md:table-cell">{{ c.cod_fiscal || '-' }}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-600">{{ c.telefon || '-' }}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-600 hidden md:table-cell">{{ c.email || '-' }}</td>
|
||||
<td class="px-4 py-3">
|
||||
<span
|
||||
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium"
|
||||
:class="c.tip_persoana === 'PJ' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'"
|
||||
>
|
||||
{{ c.tip_persoana || 'PF' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { useClientsStore } from '../../stores/clients.js'
|
||||
import { onTableChange } from '../../db/database.js'
|
||||
|
||||
const clientsStore = useClientsStore()
|
||||
|
||||
const clients = ref([])
|
||||
const loading = ref(true)
|
||||
const searchQuery = ref('')
|
||||
const showForm = ref(false)
|
||||
const saving = ref(false)
|
||||
const editingId = ref(null)
|
||||
|
||||
const form = reactive({
|
||||
tip_persoana: 'PF',
|
||||
denumire: '',
|
||||
nume: '',
|
||||
prenume: '',
|
||||
cod_fiscal: '',
|
||||
reg_com: '',
|
||||
telefon: '',
|
||||
email: '',
|
||||
adresa: '',
|
||||
judet: '',
|
||||
oras: '',
|
||||
cod_postal: '',
|
||||
tara: 'RO',
|
||||
cont_iban: '',
|
||||
banca: '',
|
||||
})
|
||||
|
||||
let searchTimeout = null
|
||||
|
||||
async function loadClients() {
|
||||
loading.value = true
|
||||
clients.value = await clientsStore.getAll(searchQuery.value)
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
if (searchTimeout) clearTimeout(searchTimeout)
|
||||
searchTimeout = setTimeout(loadClients, 300)
|
||||
}
|
||||
|
||||
function clientDisplayName(c) {
|
||||
if (c.tip_persoana === 'PJ' && c.denumire) return c.denumire
|
||||
const parts = [c.nume, c.prenume].filter(Boolean)
|
||||
return parts.length > 0 ? parts.join(' ') : (c.denumire || '-')
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
Object.assign(form, {
|
||||
tip_persoana: 'PF', denumire: '', nume: '', prenume: '',
|
||||
cod_fiscal: '', reg_com: '', telefon: '', email: '',
|
||||
adresa: '', judet: '', oras: '', cod_postal: '', tara: 'RO',
|
||||
cont_iban: '', banca: '',
|
||||
})
|
||||
}
|
||||
|
||||
function startEdit(client) {
|
||||
editingId.value = client.id
|
||||
showForm.value = true
|
||||
Object.assign(form, {
|
||||
tip_persoana: client.tip_persoana || 'PF',
|
||||
denumire: client.denumire || '',
|
||||
nume: client.nume || '',
|
||||
prenume: client.prenume || '',
|
||||
cod_fiscal: client.cod_fiscal || '',
|
||||
reg_com: client.reg_com || '',
|
||||
telefon: client.telefon || '',
|
||||
email: client.email || '',
|
||||
adresa: client.adresa || '',
|
||||
judet: client.judet || '',
|
||||
oras: client.oras || '',
|
||||
cod_postal: client.cod_postal || '',
|
||||
tara: client.tara || 'RO',
|
||||
cont_iban: client.cont_iban || '',
|
||||
banca: client.banca || '',
|
||||
})
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editingId.value = null
|
||||
showForm.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
saving.value = true
|
||||
try {
|
||||
if (editingId.value) {
|
||||
await clientsStore.update(editingId.value, { ...form })
|
||||
editingId.value = null
|
||||
} else {
|
||||
await clientsStore.create({ ...form })
|
||||
}
|
||||
showForm.value = false
|
||||
resetForm()
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
let unsubscribe = null
|
||||
|
||||
onMounted(() => {
|
||||
loadClients()
|
||||
unsubscribe = onTableChange('clients', loadClients)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (unsubscribe) unsubscribe()
|
||||
})
|
||||
</script>
|
||||
@@ -1,88 +1,163 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-6">Dashboard</h1>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Dashboard</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>
|
||||
|
||||
<UpgradeBanner />
|
||||
|
||||
<!-- 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">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-white rounded-lg shadow p-4 cursor-pointer hover:ring-2 hover:ring-blue-300" @click="activeFilter = null">
|
||||
<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">
|
||||
<div class="bg-white rounded-lg shadow p-4 cursor-pointer hover:ring-2 hover:ring-yellow-300" @click="activeFilter = 'DRAFT'">
|
||||
<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">
|
||||
<div class="bg-white rounded-lg shadow p-4 cursor-pointer hover:ring-2 hover:ring-green-300" @click="activeFilter = 'VALIDAT'">
|
||||
<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 class="bg-white rounded-lg shadow p-4 cursor-pointer hover:ring-2 hover:ring-blue-300" @click="activeFilter = 'FACTURAT'">
|
||||
<p class="text-sm text-gray-500">Facturate</p>
|
||||
<p class="text-2xl font-bold text-blue-600">{{ stats.facturatedOrders }}</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>
|
||||
<!-- Filter tabs + Search -->
|
||||
<div class="flex flex-col md:flex-row md:items-center gap-3 mb-4">
|
||||
<div class="flex gap-2">
|
||||
<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>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Cauta dupa nr. auto, client, nr. comanda..."
|
||||
class="flex-1 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>
|
||||
|
||||
<!-- 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>
|
||||
<!-- 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>
|
||||
<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"
|
||||
<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 hidden md:table-cell">Marca / Model</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}`)"
|
||||
>
|
||||
<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">
|
||||
<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 text-sm text-gray-600 hidden md:table-cell">
|
||||
{{ [o.marca_denumire, o.model_denumire].filter(Boolean).join(' ') || '-' }}
|
||||
</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>
|
||||
<p class="text-sm font-medium text-gray-900 mt-1">{{ (o.total_general || 0).toFixed(2) }} RON</p>
|
||||
</div>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { useOrdersStore } from '../../stores/orders.js'
|
||||
import { useSync } from '../../composables/useSync.js'
|
||||
import { onTableChange } from '../../db/database.js'
|
||||
import UpgradeBanner from '../../components/common/UpgradeBanner.vue'
|
||||
|
||||
useSync()
|
||||
|
||||
const ordersStore = useOrdersStore()
|
||||
const stats = ref({ totalOrders: 0, draftOrders: 0, validatedOrders: 0, totalVehicles: 0, totalRevenue: 0 })
|
||||
const recentOrders = ref([])
|
||||
const stats = ref({ totalOrders: 0, draftOrders: 0, validatedOrders: 0, facturatedOrders: 0, totalVehicles: 0, totalRevenue: 0 })
|
||||
const orders = ref([])
|
||||
const loading = ref(true)
|
||||
const activeFilter = ref(null)
|
||||
const searchQuery = ref('')
|
||||
|
||||
const filters = [
|
||||
{ label: 'Toate', value: null },
|
||||
{ label: 'Draft', value: 'DRAFT' },
|
||||
{ label: 'Validate', value: 'VALIDAT' },
|
||||
{ label: 'Facturate', value: 'FACTURAT' },
|
||||
]
|
||||
|
||||
let searchTimeout = null
|
||||
|
||||
async function loadOrders() {
|
||||
loading.value = true
|
||||
orders.value = await ordersStore.getAll(activeFilter.value, searchQuery.value)
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
stats.value = await ordersStore.getStats()
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
if (searchTimeout) clearTimeout(searchTimeout)
|
||||
searchTimeout = setTimeout(loadOrders, 300)
|
||||
}
|
||||
|
||||
watch(activeFilter, loadOrders)
|
||||
|
||||
let unsubOrders = null
|
||||
|
||||
onMounted(async () => {
|
||||
stats.value = await ordersStore.getStats()
|
||||
recentOrders.value = await ordersStore.getRecentOrders(5)
|
||||
await Promise.all([loadStats(), loadOrders()])
|
||||
unsubOrders = onTableChange('orders', () => {
|
||||
loadOrders()
|
||||
loadStats()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (unsubOrders) unsubOrders()
|
||||
})
|
||||
|
||||
function statusClass(status) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nr. factura</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Tip</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Data</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 hidden md:table-cell">Nr. auto</th>
|
||||
@@ -23,6 +24,14 @@
|
||||
<td class="px-4 py-3 text-sm font-medium text-gray-900">
|
||||
{{ inv.serie_factura }}{{ inv.nr_factura }}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span
|
||||
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium"
|
||||
:class="inv.tip_document === 'BON_FISCAL' ? 'bg-gray-100 text-gray-700' : 'bg-blue-100 text-blue-700'"
|
||||
>
|
||||
{{ inv.tip_document === 'BON_FISCAL' ? 'BON FISCAL' : 'FACTURA' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-600">{{ formatDate(inv.data_factura) }}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-600 hidden md:table-cell">{{ inv.client_nume || '-' }}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-600 hidden md:table-cell">{{ inv.nr_auto || '-' }}</td>
|
||||
|
||||
@@ -2,77 +2,69 @@
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<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>
|
||||
<router-link to="/dashboard" class="text-sm text-gray-500 hover:text-gray-700">Inapoi</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 class="bg-white rounded-lg shadow p-6 space-y-6 max-w-2xl">
|
||||
<!-- Step 1: Select Client -->
|
||||
<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 class="flex items-center gap-2 mb-3">
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-600 text-white text-xs font-bold">1</span>
|
||||
<h2 class="text-lg font-semibold text-gray-900">Selecteaza client</h2>
|
||||
</div>
|
||||
<ClientPicker v-model="form.client_id" @select="onClientSelect" />
|
||||
</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" />
|
||||
<!-- Step 2: Select Vehicle (visible after client is selected) -->
|
||||
<div v-if="form.client_id" class="border-t pt-6">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-600 text-white text-xs font-bold">2</span>
|
||||
<h2 class="text-lg font-semibold text-gray-900">Selecteaza vehicul</h2>
|
||||
</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" />
|
||||
<VehiclePicker v-model="form.vehicle_id" :client-id="form.client_id" @select="onVehicleSelect" />
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Order Details (visible after vehicle is selected) -->
|
||||
<div v-if="form.vehicle_id" class="border-t pt-6">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-600 text-white text-xs font-bold">3</span>
|
||||
<h2 class="text-lg font-semibold text-gray-900">Detalii comanda</h2>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<!-- 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="text-sm text-red-600">{{ error }}</p>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
v-if="form.vehicle_id"
|
||||
@click="handleCreate"
|
||||
: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>
|
||||
|
||||
@@ -80,31 +72,23 @@
|
||||
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 ClientPicker from '../../components/clients/ClientPicker.vue'
|
||||
import VehiclePicker from '../../components/vehicles/VehiclePicker.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const ordersStore = useOrdersStore()
|
||||
const vehiclesStore = useVehiclesStore()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const form = reactive({
|
||||
client_id: null,
|
||||
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)
|
||||
@@ -116,18 +100,20 @@ onMounted(async () => {
|
||||
)
|
||||
})
|
||||
|
||||
function onClientSelect(client) {
|
||||
if (!client) {
|
||||
form.vehicle_id = null
|
||||
}
|
||||
}
|
||||
|
||||
function onVehicleSelect(v) {
|
||||
if (v) showNewVehicle.value = false
|
||||
// vehicle selected
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -1,33 +1,11 @@
|
||||
<template>
|
||||
<div v-if="order">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<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">
|
||||
<PdfDownloadButton
|
||||
v-if="order.status !== 'DRAFT'"
|
||||
type="deviz"
|
||||
:order-id="order.id"
|
||||
:nr-comanda="order.nr_comanda"
|
||||
label="PDF Deviz"
|
||||
/>
|
||||
<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>
|
||||
<button
|
||||
v-if="order.status === 'VALIDAT'"
|
||||
@click="handleFactureaza"
|
||||
:disabled="facturand"
|
||||
class="px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{{ facturand ? 'Se proceseaza...' : 'Factureaza' }}
|
||||
</button>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div>
|
||||
<router-link to="/dashboard" 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>
|
||||
<span
|
||||
class="inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium"
|
||||
:class="statusClass(order.status)"
|
||||
@@ -35,10 +13,105 @@
|
||||
{{ order.status }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<PdfDownloadButton
|
||||
v-if="order.status !== 'DRAFT'"
|
||||
type="deviz"
|
||||
:order-id="order.id"
|
||||
:nr-comanda="order.nr_comanda"
|
||||
label="PDF Deviz"
|
||||
/>
|
||||
<!-- Edit button (DRAFT only) -->
|
||||
<button
|
||||
v-if="order.status === 'DRAFT' && !editing"
|
||||
@click="startEdit"
|
||||
class="px-4 py-2 bg-gray-600 text-white text-sm rounded-md hover:bg-gray-700"
|
||||
>
|
||||
Editeaza
|
||||
</button>
|
||||
<!-- Validate button (DRAFT only) -->
|
||||
<button
|
||||
v-if="order.status === 'DRAFT' && !editing"
|
||||
@click="handleValidate"
|
||||
class="px-4 py-2 bg-green-600 text-white text-sm rounded-md hover:bg-green-700"
|
||||
>
|
||||
Valideaza
|
||||
</button>
|
||||
<!-- Devalidate button (VALIDAT, no invoice) -->
|
||||
<button
|
||||
v-if="order.status === 'VALIDAT'"
|
||||
@click="confirmDevalidate = true"
|
||||
class="px-4 py-2 bg-yellow-600 text-white text-sm rounded-md hover:bg-yellow-700"
|
||||
>
|
||||
Devalideaza
|
||||
</button>
|
||||
<!-- Facturare button (VALIDAT) -->
|
||||
<button
|
||||
v-if="order.status === 'VALIDAT'"
|
||||
@click="showFacturareDialog = true"
|
||||
class="px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700"
|
||||
>
|
||||
Factureaza
|
||||
</button>
|
||||
<!-- Delete invoice button (FACTURAT) -->
|
||||
<button
|
||||
v-if="order.status === 'FACTURAT' && invoice"
|
||||
@click="confirmDeleteInvoice = true"
|
||||
class="px-4 py-2 bg-red-600 text-white text-sm rounded-md hover:bg-red-700"
|
||||
>
|
||||
Sterge factura
|
||||
</button>
|
||||
<!-- Delete order button (DRAFT or VALIDAT) -->
|
||||
<button
|
||||
v-if="order.status === 'DRAFT' || order.status === 'VALIDAT'"
|
||||
@click="confirmDelete = true"
|
||||
class="px-4 py-2 bg-red-600 text-white text-sm rounded-md hover:bg-red-700"
|
||||
>
|
||||
Sterge
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order info -->
|
||||
<div class="bg-white rounded-lg shadow p-4 mb-6">
|
||||
<!-- Edit form (DRAFT only) -->
|
||||
<div v-if="editing" class="bg-white rounded-lg shadow p-6 mb-6 max-w-2xl">
|
||||
<h2 class="text-lg font-semibold mb-4">Editeaza comanda</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Client</label>
|
||||
<ClientPicker v-model="editForm.client_id" @select="onEditClientSelect" />
|
||||
</div>
|
||||
<div v-if="editForm.client_id">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Vehicul</label>
|
||||
<VehiclePicker v-model="editForm.vehicle_id" :client-id="editForm.client_id" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Tip deviz</label>
|
||||
<select v-model="editForm.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>
|
||||
<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="editForm.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="editForm.observatii" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-4">
|
||||
<button @click="handleSaveEdit" :disabled="savingEdit" class="px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700 disabled:opacity-50">
|
||||
{{ savingEdit ? 'Se salveaza...' : 'Salveaza' }}
|
||||
</button>
|
||||
<button @click="cancelEdit" class="px-4 py-2 text-gray-600 hover:text-gray-800 text-sm">Anuleaza</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order info (read mode) -->
|
||||
<div v-if="!editing" 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>
|
||||
@@ -134,6 +207,65 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Devalidate dialog -->
|
||||
<div v-if="confirmDevalidate" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click.self="confirmDevalidate = false">
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 max-w-sm w-full mx-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Devalideaza comanda?</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Comanda va reveni la status DRAFT si va putea fi editata.</p>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button @click="confirmDevalidate = false" class="px-4 py-2 text-gray-600 text-sm hover:text-gray-800">Anuleaza</button>
|
||||
<button @click="handleDevalidate" class="px-4 py-2 bg-yellow-600 text-white text-sm rounded-md hover:bg-yellow-700">Da, devalideaza</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Delete dialog -->
|
||||
<div v-if="confirmDelete" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click.self="confirmDelete = false">
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 max-w-sm w-full mx-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Sterge comanda?</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Comanda si toate liniile aferente vor fi sterse definitiv.</p>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button @click="confirmDelete = false" class="px-4 py-2 text-gray-600 text-sm hover:text-gray-800">Anuleaza</button>
|
||||
<button @click="handleDelete" class="px-4 py-2 bg-red-600 text-white text-sm rounded-md hover:bg-red-700">Da, sterge</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Delete Invoice dialog -->
|
||||
<div v-if="confirmDeleteInvoice" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click.self="confirmDeleteInvoice = false">
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 max-w-sm w-full mx-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Sterge factura?</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Factura va fi stearsa si comanda va reveni la status VALIDAT.</p>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button @click="confirmDeleteInvoice = false" class="px-4 py-2 text-gray-600 text-sm hover:text-gray-800">Anuleaza</button>
|
||||
<button @click="handleDeleteInvoice" class="px-4 py-2 bg-red-600 text-white text-sm rounded-md hover:bg-red-700">Da, sterge</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Facturare dialog -->
|
||||
<div v-if="showFacturareDialog" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click.self="showFacturareDialog = false">
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 max-w-sm w-full mx-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Tip document</h3>
|
||||
<div class="space-y-3 mb-4">
|
||||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input v-model="facturaTip" type="radio" value="FACTURA" class="text-blue-600" />
|
||||
<span>Factura</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input v-model="facturaTip" type="radio" value="BON_FISCAL" class="text-blue-600" />
|
||||
<span>Bon fiscal</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button @click="showFacturareDialog = false" class="px-4 py-2 text-gray-600 text-sm hover:text-gray-800">Anuleaza</button>
|
||||
<button @click="handleFactureaza" :disabled="facturand" class="px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700 disabled:opacity-50">
|
||||
{{ facturand ? 'Se proceseaza...' : 'Creeaza' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center py-12 text-gray-500">
|
||||
@@ -142,30 +274,64 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useOrdersStore } from '../../stores/orders.js'
|
||||
import { execSQL, onTableChange, notifyTableChanged } from '../../db/database.js'
|
||||
import { syncEngine } from '../../db/sync.js'
|
||||
import { useAuthStore } from '../../stores/auth.js'
|
||||
import OrderLineForm from '../../components/orders/OrderLineForm.vue'
|
||||
import PdfDownloadButton from '../../components/orders/PdfDownloadButton.vue'
|
||||
import ClientPicker from '../../components/clients/ClientPicker.vue'
|
||||
import VehiclePicker from '../../components/vehicles/VehiclePicker.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const ordersStore = useOrdersStore()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const order = ref(null)
|
||||
const lines = ref([])
|
||||
const invoice = ref(null)
|
||||
const facturand = ref(false)
|
||||
const editing = ref(false)
|
||||
const savingEdit = ref(false)
|
||||
const confirmDevalidate = ref(false)
|
||||
const confirmDelete = ref(false)
|
||||
const confirmDeleteInvoice = ref(false)
|
||||
const showFacturareDialog = ref(false)
|
||||
const facturaTip = ref('FACTURA')
|
||||
const tipuriDeviz = ref([])
|
||||
|
||||
const editForm = reactive({
|
||||
client_id: null,
|
||||
vehicle_id: null,
|
||||
tip_deviz_id: null,
|
||||
km_intrare: 0,
|
||||
observatii: '',
|
||||
})
|
||||
|
||||
async function loadOrder() {
|
||||
order.value = await ordersStore.getById(route.params.id)
|
||||
lines.value = await ordersStore.getLines(route.params.id)
|
||||
// Load invoice for FACTURAT orders
|
||||
if (order.value?.status === 'FACTURAT') {
|
||||
const invoices = await execSQL(
|
||||
`SELECT * FROM invoices WHERE order_id = ? AND tenant_id = ?`,
|
||||
[route.params.id, auth.tenantId]
|
||||
)
|
||||
invoice.value = invoices[0] || null
|
||||
} else {
|
||||
invoice.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadOrder()
|
||||
onMounted(async () => {
|
||||
await loadOrder()
|
||||
tipuriDeviz.value = await execSQL(
|
||||
`SELECT * FROM catalog_tipuri_deviz WHERE tenant_id = ? ORDER BY denumire`,
|
||||
[auth.tenantId]
|
||||
)
|
||||
onTableChange('orders', loadOrder)
|
||||
onTableChange('order_lines', async () => {
|
||||
lines.value = await ordersStore.getLines(route.params.id)
|
||||
@@ -173,6 +339,35 @@ onMounted(() => {
|
||||
})
|
||||
})
|
||||
|
||||
function startEdit() {
|
||||
editForm.client_id = order.value.client_id || null
|
||||
editForm.vehicle_id = order.value.vehicle_id || null
|
||||
editForm.tip_deviz_id = order.value.tip_deviz_id || null
|
||||
editForm.km_intrare = order.value.km_intrare || 0
|
||||
editForm.observatii = order.value.observatii || ''
|
||||
editing.value = true
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editing.value = false
|
||||
}
|
||||
|
||||
function onEditClientSelect(client) {
|
||||
if (!client) {
|
||||
editForm.vehicle_id = null
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveEdit() {
|
||||
savingEdit.value = true
|
||||
try {
|
||||
await ordersStore.updateHeader(route.params.id, { ...editForm })
|
||||
editing.value = false
|
||||
} finally {
|
||||
savingEdit.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddLine(lineData) {
|
||||
await ordersStore.addLine(route.params.id, lineData)
|
||||
}
|
||||
@@ -185,18 +380,42 @@ async function handleValidate() {
|
||||
await ordersStore.validateOrder(route.params.id)
|
||||
}
|
||||
|
||||
async function handleDevalidate() {
|
||||
await ordersStore.devalidateOrder(route.params.id)
|
||||
confirmDevalidate.value = false
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
await ordersStore.deleteOrder(route.params.id)
|
||||
confirmDelete.value = false
|
||||
router.push('/dashboard')
|
||||
}
|
||||
|
||||
async function handleDeleteInvoice() {
|
||||
if (invoice.value) {
|
||||
await ordersStore.deleteInvoice(invoice.value.id)
|
||||
}
|
||||
confirmDeleteInvoice.value = false
|
||||
}
|
||||
|
||||
async function handleFactureaza() {
|
||||
if (!order.value) return
|
||||
facturand.value = true
|
||||
try {
|
||||
const id = crypto.randomUUID()
|
||||
const now = new Date().toISOString()
|
||||
const nrFactura = `F${Date.now().toString().slice(-6)}`
|
||||
const year = new Date().getFullYear()
|
||||
const seq = Date.now().toString().slice(-4)
|
||||
const nrFactura = facturaTip.value === 'FACTURA'
|
||||
? `F-${year}-${seq}`
|
||||
: `BF-${year}-${seq}`
|
||||
|
||||
await execSQL(
|
||||
`INSERT INTO invoices (id, tenant_id, order_id, nr_factura, serie_factura, data_factura, client_nume, nr_auto, total_fara_tva, tva, total_general, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
[id, auth.tenantId, order.value.id, nrFactura, 'ROA', now.slice(0,10),
|
||||
order.value.client_nume, order.value.nr_auto,
|
||||
`INSERT INTO invoices (id, tenant_id, order_id, client_id, nr_factura, serie_factura, data_factura, tip_document, modalitate_plata, client_nume, client_cod_fiscal, nr_auto, total_fara_tva, tva, total_general, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
[id, auth.tenantId, order.value.id, order.value.client_id || null,
|
||||
nrFactura, 'ROA', now.slice(0, 10), facturaTip.value, null,
|
||||
order.value.client_nume, null, order.value.nr_auto,
|
||||
(order.value.total_general || 0) / 1.19,
|
||||
(order.value.total_general || 0) - (order.value.total_general || 0) / 1.19,
|
||||
order.value.total_general || 0, now, now]
|
||||
@@ -204,7 +423,13 @@ async function handleFactureaza() {
|
||||
await execSQL(`UPDATE orders SET status='FACTURAT', updated_at=? WHERE id=?`, [now, order.value.id])
|
||||
notifyTableChanged('invoices')
|
||||
notifyTableChanged('orders')
|
||||
syncEngine.addToQueue('invoices', id, 'INSERT', { id, tenant_id: auth.tenantId, order_id: order.value.id, nr_factura: nrFactura })
|
||||
await syncEngine.addToQueue('invoices', id, 'INSERT', {
|
||||
id, tenant_id: auth.tenantId, order_id: order.value.id,
|
||||
client_id: order.value.client_id, nr_factura: nrFactura,
|
||||
tip_document: facturaTip.value
|
||||
})
|
||||
await syncEngine.addToQueue('orders', order.value.id, 'UPDATE', { status: 'FACTURAT' })
|
||||
showFacturareDialog.value = false
|
||||
} finally {
|
||||
facturand.value = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user