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