feat(frontend): wa-sqlite memory-only + Factureaza + Vite polling + Appointments + Catalog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 18:37:08 +02:00
parent 78d2a77b0d
commit 9aef3d6933
6 changed files with 673 additions and 20 deletions

View File

@@ -1,5 +1,4 @@
import SQLiteESMFactory from '@journeyapps/wa-sqlite/dist/wa-sqlite-async.mjs'
import { IDBBatchAtomicVFS } from '@journeyapps/wa-sqlite/src/examples/IDBBatchAtomicVFS.js'
import SQLiteESMFactory from '@journeyapps/wa-sqlite/dist/wa-sqlite.mjs'
import * as SQLite from '@journeyapps/wa-sqlite'
import { SCHEMA_SQL } from './schema.js'
@@ -7,14 +6,19 @@ let db = null
let sqlite3 = null
const tableListeners = new Map()
export async function initDatabase() {
if (db) return db
let initPromise = null
export function initDatabase() {
if (initPromise) return initPromise
initPromise = _init()
return initPromise
}
async function _init() {
const module = await SQLiteESMFactory()
sqlite3 = SQLite.Factory(module)
const vfs = await IDBBatchAtomicVFS.create('roaauto', module)
sqlite3.vfs_register(vfs, true)
db = await sqlite3.open_v2('roaauto.db',
SQLite.SQLITE_OPEN_READWRITE | SQLite.SQLITE_OPEN_CREATE, 'roaauto')
db = await sqlite3.open_v2(':memory:')
for (const sql of SCHEMA_SQL.split(';').filter(s => s.trim())) {
await sqlite3.exec(db, sql)
}
@@ -34,10 +38,20 @@ export function onTableChange(table, cb) {
export async function execSQL(sql, params = []) {
if (!db) throw new Error('DB not initialized')
const results = []
await sqlite3.exec(db, sql, (row, cols) => {
const obj = {}
cols.forEach((c, i) => { obj[c] = row[i] })
results.push(obj)
})
if (params.length === 0) {
await sqlite3.exec(db, sql, (row, cols) => {
results.push(Object.fromEntries(cols.map((c, i) => [c, row[i]])))
})
} else {
for await (const stmt of sqlite3.statements(db, sql)) {
sqlite3.bind_collection(stmt, params)
const cols = sqlite3.column_names(stmt)
while ((await sqlite3.step(stmt)) === SQLite.SQLITE_ROW) {
results.push(Object.fromEntries(cols.map((c, i) => [c, sqlite3.column(stmt, i)])))
}
}
}
return results
}

View File

@@ -14,8 +14,13 @@ app.use(pinia)
app.use(router)
const auth = useAuthStore()
if (auth.isAuthenticated) {
initDatabase().then(() => syncEngine.fullSync())
async function bootstrap() {
if (auth.isAuthenticated) {
await initDatabase()
syncEngine.fullSync().catch(() => {})
}
app.mount('#app')
}
app.mount('#app')
bootstrap()

View File

@@ -1,6 +1,197 @@
<template>
<div>
<h1 class="text-2xl font-bold text-gray-900 mb-6">Programari</h1>
<p class="text-gray-500">Programari - va fi implementat in TASK-006.</p>
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-gray-900">Programari</h1>
<button
@click="showForm = !showForm"
class="px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700"
>
{{ showForm ? 'Anuleaza' : '+ Programare noua' }}
</button>
</div>
<!-- New appointment 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">Programare noua</h2>
<form @submit.prevent="handleCreate" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Nume client</label>
<input v-model="form.client_nume" type="text" required class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" placeholder="Ion Popescu" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Telefon client</label>
<input v-model="form.client_telefon" type="tel" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" placeholder="07xx xxx xxx" />
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Data si ora</label>
<input v-model="form.data_ora" type="datetime-local" required 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">Durata (minute)</label>
<input v-model.number="form.durata_minute" type="number" min="15" step="15" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" />
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Observatii</label>
<textarea v-model="form.observatii" rows="2" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" placeholder="Detalii programare..."></textarea>
</div>
<button type="submit" :disabled="saving" class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50">
{{ saving ? 'Se salveaza...' : 'Salveaza programarea' }}
</button>
</form>
</div>
<!-- Appointments 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="appointments.length === 0" class="p-8 text-center text-gray-500">
Nicio programare 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">Data / Ora</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Client</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase hidden md:table-cell">Telefon</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase hidden md:table-cell">Durata</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-left text-xs font-medium text-gray-500 uppercase hidden md:table-cell">Observatii</th>
<th class="px-4 py-3 w-40"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr v-for="a in appointments" :key="a.id" class="hover:bg-gray-50">
<td class="px-4 py-3 text-sm font-medium text-gray-900">{{ formatDataOra(a.data_ora) }}</td>
<td class="px-4 py-3 text-sm text-gray-700">{{ a.client_nume || '-' }}</td>
<td class="px-4 py-3 text-sm text-gray-600 hidden md:table-cell">{{ a.client_telefon || '-' }}</td>
<td class="px-4 py-3 text-sm text-gray-600 hidden md:table-cell">{{ a.durata_minute || 60 }} min</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="statusClass(a.status)"
>
{{ a.status || 'PROGRAMAT' }}
</span>
</td>
<td class="px-4 py-3 text-sm text-gray-500 hidden md:table-cell max-w-xs truncate">{{ a.observatii || '-' }}</td>
<td class="px-4 py-3 text-sm">
<div class="flex gap-2">
<button
v-if="a.status !== 'FINALIZAT' && a.status !== 'ANULAT'"
@click="handleStatusChange(a.id, 'FINALIZAT')"
class="px-2 py-1 bg-green-100 text-green-700 text-xs rounded hover:bg-green-200"
>
Finalizeaza
</button>
<button
v-if="a.status !== 'ANULAT' && a.status !== 'FINALIZAT'"
@click="handleStatusChange(a.id, 'ANULAT')"
class="px-2 py-1 bg-red-100 text-red-700 text-xs rounded hover:bg-red-200"
>
Anuleaza
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { execSQL, notifyTableChanged, onTableChange } from '../../db/database.js'
import { syncEngine } from '../../db/sync.js'
import { useAuthStore } from '../../stores/auth.js'
const auth = useAuthStore()
const appointments = ref([])
const loading = ref(true)
const showForm = ref(false)
const saving = ref(false)
const form = reactive({
client_nume: '',
client_telefon: '',
data_ora: '',
durata_minute: 60,
observatii: '',
})
async function loadAppointments() {
loading.value = true
const rows = await execSQL(
`SELECT * FROM appointments WHERE tenant_id=? ORDER BY data_ora DESC`,
[auth.tenantId]
)
appointments.value = rows
loading.value = false
}
async function handleCreate() {
saving.value = true
try {
const id = crypto.randomUUID()
const now = new Date().toISOString()
const data = {
id,
tenant_id: auth.tenantId,
client_nume: form.client_nume,
client_telefon: form.client_telefon,
data_ora: form.data_ora,
durata_minute: form.durata_minute,
observatii: form.observatii,
status: 'PROGRAMAT',
created_at: now,
updated_at: now,
}
await execSQL(
`INSERT INTO appointments (id, tenant_id, client_nume, client_telefon, data_ora, durata_minute, observatii, status, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?)`,
[data.id, data.tenant_id, data.client_nume, data.client_telefon, data.data_ora, data.durata_minute, data.observatii, data.status, data.created_at, data.updated_at]
)
notifyTableChanged('appointments')
syncEngine.addToQueue('appointments', id, 'INSERT', data)
showForm.value = false
Object.assign(form, { client_nume: '', client_telefon: '', data_ora: '', durata_minute: 60, observatii: '' })
} finally {
saving.value = false
}
}
async function handleStatusChange(id, newStatus) {
const now = new Date().toISOString()
await execSQL('UPDATE appointments SET status=?, updated_at=? WHERE id=?', [newStatus, now, id])
notifyTableChanged('appointments')
syncEngine.addToQueue('appointments', id, 'UPDATE', { status: newStatus, updated_at: now })
}
function formatDataOra(dataOra) {
if (!dataOra) return '-'
try {
const d = new Date(dataOra)
return d.toLocaleString('ro-RO', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
} catch {
return dataOra
}
}
function statusClass(status) {
switch (status) {
case 'PROGRAMAT': return 'bg-blue-100 text-blue-700'
case 'FINALIZAT': return 'bg-green-100 text-green-700'
case 'ANULAT': return 'bg-red-100 text-red-700'
default: return 'bg-gray-100 text-gray-700'
}
}
onMounted(() => {
loadAppointments()
onTableChange('appointments', loadAppointments)
})
</script>

View File

@@ -1,6 +1,408 @@
<template>
<div>
<h1 class="text-2xl font-bold text-gray-900 mb-6">Catalog</h1>
<p class="text-gray-500">Catalog - va fi implementat in TASK-006.</p>
<!-- Tabs -->
<div class="flex border-b border-gray-200 mb-6">
<button
v-for="tab in tabs"
:key="tab.key"
@click="activeTab = tab.key"
class="px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors"
:class="activeTab === tab.key
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
>
{{ tab.label }}
</button>
</div>
<!-- Tab: Marci -->
<div v-if="activeTab === 'marci'">
<div class="bg-white rounded-lg shadow overflow-hidden mb-6">
<div v-if="marci.length === 0" class="p-8 text-center text-gray-500">Nicio marca 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">Denumire</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Activ</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr v-for="m in marci" :key="m.id" class="hover:bg-gray-50">
<td class="px-4 py-3 text-sm text-gray-900">{{ m.denumire }}</td>
<td class="px-4 py-3">
<button
@click="toggleMarcaActiv(m)"
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium transition-colors"
:class="m.activ ? 'bg-green-100 text-green-700 hover:bg-green-200' : 'bg-gray-100 text-gray-500 hover:bg-gray-200'"
>
{{ m.activ ? 'Activ' : 'Inactiv' }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Add Marca form -->
<div class="bg-white rounded-lg shadow p-4 max-w-md">
<h3 class="text-sm font-semibold text-gray-700 mb-3">Adauga marca</h3>
<form @submit.prevent="addMarca" class="flex gap-2">
<input v-model="newMarca" type="text" required placeholder="Denumire marca" class="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm" />
<button type="submit" :disabled="savingMarca" class="px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700 disabled:opacity-50">
{{ savingMarca ? '...' : 'Adauga' }}
</button>
</form>
</div>
</div>
<!-- Tab: Modele -->
<div v-if="activeTab === 'modele'">
<div class="mb-4">
<select v-model="filterMarcaId" @change="loadModele" class="px-3 py-2 border border-gray-300 rounded-md text-sm">
<option :value="null">-- Toate marcile --</option>
<option v-for="m in marci" :key="m.id" :value="m.id">{{ m.denumire }}</option>
</select>
</div>
<div class="bg-white rounded-lg shadow overflow-hidden mb-6">
<div v-if="modele.length === 0" class="p-8 text-center text-gray-500">Niciun model 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">Marca</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Model</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr v-for="m in modele" :key="m.id" class="hover:bg-gray-50">
<td class="px-4 py-3 text-sm text-gray-500">{{ m.marca_denumire }}</td>
<td class="px-4 py-3 text-sm text-gray-900">{{ m.denumire }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Add Model form -->
<div class="bg-white rounded-lg shadow p-4 max-w-md">
<h3 class="text-sm font-semibold text-gray-700 mb-3">Adauga model</h3>
<form @submit.prevent="addModel" class="space-y-2">
<select v-model="newModel.marca_id" required class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm">
<option :value="null">-- Selecteaza marca --</option>
<option v-for="m in marci" :key="m.id" :value="m.id">{{ m.denumire }}</option>
</select>
<div class="flex gap-2">
<input v-model="newModel.denumire" type="text" required placeholder="Denumire model" class="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm" />
<button type="submit" :disabled="savingModel" class="px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700 disabled:opacity-50">
{{ savingModel ? '...' : 'Adauga' }}
</button>
</div>
</form>
</div>
</div>
<!-- Tab: Ansamble -->
<div v-if="activeTab === 'ansamble'">
<div class="bg-white rounded-lg shadow overflow-hidden mb-6">
<div v-if="ansamble.length === 0" class="p-8 text-center text-gray-500">Niciun ansamblu 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>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr v-for="a in ansamble" :key="a.id" class="hover:bg-gray-50">
<td class="px-4 py-3 text-sm text-gray-900">{{ a.denumire }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Add Ansamblu form -->
<div class="bg-white rounded-lg shadow p-4 max-w-md">
<h3 class="text-sm font-semibold text-gray-700 mb-3">Adauga ansamblu</h3>
<form @submit.prevent="addAnsamblu" class="flex gap-2">
<input v-model="newAnsamblu" type="text" required placeholder="Denumire ansamblu" class="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm" />
<button type="submit" :disabled="savingAnsamblu" class="px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700 disabled:opacity-50">
{{ savingAnsamblu ? '...' : 'Adauga' }}
</button>
</form>
</div>
</div>
<!-- Tab: Preturi -->
<div v-if="activeTab === 'preturi'">
<div class="bg-white rounded-lg shadow overflow-hidden mb-6">
<div v-if="preturi.length === 0" class="p-8 text-center text-gray-500">Niciun pret 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-right text-xs font-medium text-gray-500 uppercase">Pret</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">UM</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr v-for="p in preturi" :key="p.id" class="hover:bg-gray-50">
<td class="px-4 py-3 text-sm text-gray-900">{{ p.denumire }}</td>
<td class="px-4 py-3 text-sm text-right font-medium text-gray-900">{{ (p.pret || 0).toFixed(2) }}</td>
<td class="px-4 py-3 text-sm text-gray-500">{{ p.um || 'ora' }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Add Pret 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 pret</h3>
<form @submit.prevent="addPret" class="grid grid-cols-3 gap-2">
<input v-model="newPret.denumire" type="text" required placeholder="Denumire" class="col-span-1 px-3 py-2 border border-gray-300 rounded-md text-sm" />
<input v-model.number="newPret.pret" type="number" step="0.01" min="0" required placeholder="Pret" class="px-3 py-2 border border-gray-300 rounded-md text-sm" />
<div class="flex gap-2">
<input v-model="newPret.um" type="text" placeholder="UM (ora)" class="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm" />
<button type="submit" :disabled="savingPret" class="px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700 disabled:opacity-50">
{{ savingPret ? '...' : 'Adauga' }}
</button>
</div>
</form>
</div>
</div>
<!-- Tab: Tipuri deviz -->
<div v-if="activeTab === 'tipuri_deviz'">
<div class="bg-white rounded-lg shadow overflow-hidden mb-6">
<div v-if="tipuriDeviz.length === 0" class="p-8 text-center text-gray-500">Niciun tip de deviz 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>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr v-for="t in tipuriDeviz" :key="t.id" class="hover:bg-gray-50">
<td class="px-4 py-3 text-sm text-gray-900">{{ t.denumire }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Add Tip deviz form -->
<div class="bg-white rounded-lg shadow p-4 max-w-md">
<h3 class="text-sm font-semibold text-gray-700 mb-3">Adauga tip deviz</h3>
<form @submit.prevent="addTipDeviz" class="flex gap-2">
<input v-model="newTipDeviz" type="text" required placeholder="Denumire tip deviz" class="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm" />
<button type="submit" :disabled="savingTipDeviz" class="px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700 disabled:opacity-50">
{{ savingTipDeviz ? '...' : 'Adauga' }}
</button>
</form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, watch } from 'vue'
import { execSQL, notifyTableChanged, onTableChange } from '../../db/database.js'
import { syncEngine } from '../../db/sync.js'
import { useAuthStore } from '../../stores/auth.js'
const auth = useAuthStore()
const tabs = [
{ key: 'marci', label: 'Marci' },
{ key: 'modele', label: 'Modele' },
{ key: 'ansamble', label: 'Ansamble' },
{ key: 'preturi', label: 'Preturi' },
{ key: 'tipuri_deviz', label: 'Tipuri deviz' },
]
const activeTab = ref('marci')
// ---- Marci ----
const marci = ref([])
const newMarca = ref('')
const savingMarca = ref(false)
async function loadMarci() {
marci.value = await execSQL(
`SELECT * FROM catalog_marci WHERE tenant_id=? ORDER BY denumire`,
[auth.tenantId]
)
}
async function addMarca() {
savingMarca.value = true
try {
const id = crypto.randomUUID()
const now = new Date().toISOString()
const data = { id, tenant_id: auth.tenantId, denumire: newMarca.value, activ: 1 }
await execSQL(
`INSERT INTO catalog_marci (id, tenant_id, denumire, activ) VALUES (?,?,?,?)`,
[id, auth.tenantId, newMarca.value, 1]
)
notifyTableChanged('catalog_marci')
syncEngine.addToQueue('catalog_marci', id, 'INSERT', data)
newMarca.value = ''
} finally {
savingMarca.value = false
}
}
async function toggleMarcaActiv(marca) {
const newActiv = marca.activ ? 0 : 1
const now = new Date().toISOString()
await execSQL(`UPDATE catalog_marci SET activ=? WHERE id=?`, [newActiv, marca.id])
notifyTableChanged('catalog_marci')
syncEngine.addToQueue('catalog_marci', marca.id, 'UPDATE', { activ: newActiv })
}
// ---- Modele ----
const modele = ref([])
const filterMarcaId = ref(null)
const newModel = reactive({ marca_id: null, denumire: '' })
const savingModel = ref(false)
async function loadModele() {
if (filterMarcaId.value) {
modele.value = await execSQL(
`SELECT cm.*, ma.denumire AS marca_denumire
FROM catalog_modele cm
JOIN catalog_marci ma ON ma.id = cm.marca_id
WHERE cm.marca_id=?
ORDER BY cm.denumire`,
[filterMarcaId.value]
)
} else {
modele.value = await execSQL(
`SELECT cm.*, ma.denumire AS marca_denumire
FROM catalog_modele cm
JOIN catalog_marci ma ON ma.id = cm.marca_id
WHERE ma.tenant_id=?
ORDER BY ma.denumire, cm.denumire`,
[auth.tenantId]
)
}
}
async function addModel() {
if (!newModel.marca_id) return
savingModel.value = true
try {
const id = crypto.randomUUID()
const data = { id, marca_id: newModel.marca_id, denumire: newModel.denumire }
await execSQL(
`INSERT INTO catalog_modele (id, marca_id, denumire) VALUES (?,?,?)`,
[id, newModel.marca_id, newModel.denumire]
)
notifyTableChanged('catalog_modele')
syncEngine.addToQueue('catalog_modele', id, 'INSERT', data)
newModel.marca_id = null
newModel.denumire = ''
} finally {
savingModel.value = false
}
}
// ---- Ansamble ----
const ansamble = ref([])
const newAnsamblu = ref('')
const savingAnsamblu = ref(false)
async function loadAnsamble() {
ansamble.value = await execSQL(
`SELECT * FROM catalog_ansamble WHERE tenant_id=? ORDER BY denumire`,
[auth.tenantId]
)
}
async function addAnsamblu() {
savingAnsamblu.value = true
try {
const id = crypto.randomUUID()
const data = { id, tenant_id: auth.tenantId, denumire: newAnsamblu.value }
await execSQL(
`INSERT INTO catalog_ansamble (id, tenant_id, denumire) VALUES (?,?,?)`,
[id, auth.tenantId, newAnsamblu.value]
)
notifyTableChanged('catalog_ansamble')
syncEngine.addToQueue('catalog_ansamble', id, 'INSERT', data)
newAnsamblu.value = ''
} finally {
savingAnsamblu.value = false
}
}
// ---- Preturi ----
const preturi = ref([])
const newPret = reactive({ denumire: '', pret: null, um: 'ora' })
const savingPret = ref(false)
async function loadPreturi() {
preturi.value = await execSQL(
`SELECT * FROM catalog_preturi WHERE tenant_id=? ORDER BY denumire`,
[auth.tenantId]
)
}
async function addPret() {
savingPret.value = true
try {
const id = crypto.randomUUID()
const um = newPret.um || 'ora'
const data = { id, tenant_id: auth.tenantId, denumire: newPret.denumire, pret: newPret.pret, um }
await execSQL(
`INSERT INTO catalog_preturi (id, tenant_id, denumire, pret, um) VALUES (?,?,?,?,?)`,
[id, auth.tenantId, newPret.denumire, newPret.pret, um]
)
notifyTableChanged('catalog_preturi')
syncEngine.addToQueue('catalog_preturi', id, 'INSERT', data)
Object.assign(newPret, { denumire: '', pret: null, um: 'ora' })
} finally {
savingPret.value = false
}
}
// ---- Tipuri deviz ----
const tipuriDeviz = ref([])
const newTipDeviz = ref('')
const savingTipDeviz = ref(false)
async function loadTipuriDeviz() {
tipuriDeviz.value = await execSQL(
`SELECT * FROM catalog_tipuri_deviz WHERE tenant_id=? ORDER BY denumire`,
[auth.tenantId]
)
}
async function addTipDeviz() {
savingTipDeviz.value = true
try {
const id = crypto.randomUUID()
const data = { id, tenant_id: auth.tenantId, denumire: newTipDeviz.value }
await execSQL(
`INSERT INTO catalog_tipuri_deviz (id, tenant_id, denumire) VALUES (?,?,?)`,
[id, auth.tenantId, newTipDeviz.value]
)
notifyTableChanged('catalog_tipuri_deviz')
syncEngine.addToQueue('catalog_tipuri_deviz', id, 'INSERT', data)
newTipDeviz.value = ''
} finally {
savingTipDeviz.value = false
}
}
// Register table change listeners
onTableChange('catalog_marci', loadMarci)
onTableChange('catalog_modele', loadModele)
onTableChange('catalog_ansamble', loadAnsamble)
onTableChange('catalog_preturi', loadPreturi)
onTableChange('catalog_tipuri_deviz', loadTipuriDeviz)
// Load data for tab when switching
watch(activeTab, (tab) => {
if (tab === 'marci') loadMarci()
else if (tab === 'modele') { loadMarci(); loadModele() }
else if (tab === 'ansamble') loadAnsamble()
else if (tab === 'preturi') loadPreturi()
else if (tab === 'tipuri_deviz') loadTipuriDeviz()
})
onMounted(() => {
loadMarci()
})
</script>

View File

@@ -20,6 +20,14 @@
>
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>
<span
class="inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium"
:class="statusClass(order.status)"
@@ -137,15 +145,19 @@
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { useOrdersStore } from '../../stores/orders.js'
import { onTableChange } from '../../db/database.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'
const route = useRoute()
const ordersStore = useOrdersStore()
const auth = useAuthStore()
const order = ref(null)
const lines = ref([])
const facturand = ref(false)
async function loadOrder() {
order.value = await ordersStore.getById(route.params.id)
@@ -173,6 +185,31 @@ async function handleValidate() {
await ordersStore.validateOrder(route.params.id)
}
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)}`
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,
(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]
)
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 })
} finally {
facturand.value = false
}
}
function statusClass(status) {
switch (status) {
case 'DRAFT': return 'bg-yellow-100 text-yellow-800'

View File

@@ -42,6 +42,10 @@ export default defineConfig({
}),
],
server: {
watch: {
usePolling: true,
interval: 1000,
},
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',