Files
roaauto/frontend/src/views/clients/ClientsListView.vue
Marius Mutu 9db4e746e3 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>
2026-03-14 00:36:40 +02:00

288 lines
11 KiB
Vue

<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>