feat(frontend): Vue 3 + wa-sqlite + sync engine + auth + layouts

- package.json with Vue 3, Pinia, vue-router, wa-sqlite, Tailwind CSS 4, Vite
- wa-sqlite database layer with IDBBatchAtomicVFS (offline-first)
- Full schema mirroring backend tables (vehicles, orders, invoices, etc.)
- SyncEngine: fullSync, incrementalSync, pushQueue for offline queue
- Auth store with JWT parsing, login/register, plan tier detection
- Router with all routes and auth navigation guards
- AppLayout (sidebar desktop / bottom nav mobile) + AuthLayout
- Login/Register views connected to API contract
- SyncIndicator component (online/offline status)
- Reactive SQL query composable (useSqlQuery)
- Placeholder views for dashboard, orders, vehicles, appointments, catalog, settings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 17:22:50 +02:00
parent a16d01a669
commit c3482bba8d
27 changed files with 7614 additions and 0 deletions

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ROA AUTO</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

6861
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
frontend/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "roaauto-frontend",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5",
"vue-router": "^4.4",
"pinia": "^2.2",
"@journeyapps/wa-sqlite": "^1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2",
"vite": "^6.0",
"tailwindcss": "^4.0",
"@tailwindcss/vite": "^4.0",
"vite-plugin-pwa": "^0.21"
}
}

17
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,17 @@
<template>
<component :is="layout">
<router-view />
</component>
</template>
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import AppLayout from './layouts/AppLayout.vue'
import AuthLayout from './layouts/AuthLayout.vue'
const route = useRoute()
const layouts = { app: AppLayout, auth: AuthLayout }
const layout = computed(() => layouts[route.meta.layout] || AppLayout)
</script>

View File

@@ -0,0 +1 @@
@import "tailwindcss";

View File

@@ -0,0 +1,29 @@
<template>
<div class="flex items-center gap-1.5 text-xs">
<span
class="inline-block w-2 h-2 rounded-full"
:class="online ? 'bg-green-400' : 'bg-red-400'"
/>
<span :class="online ? 'text-green-300' : 'text-red-300'">
{{ online ? 'Online' : 'Offline' }}
</span>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const online = ref(navigator.onLine)
function setOnline() { online.value = true }
function setOffline() { online.value = false }
onMounted(() => {
window.addEventListener('online', setOnline)
window.addEventListener('offline', setOffline)
})
onUnmounted(() => {
window.removeEventListener('online', setOnline)
window.removeEventListener('offline', setOffline)
})
</script>

View File

@@ -0,0 +1,22 @@
import { ref, onMounted, onUnmounted } from 'vue'
import { execSQL, onTableChange } from '../db/database.js'
export function useSqlQuery(sql, params = [], watchTables = []) {
const rows = ref([])
const loading = ref(true)
async function refresh() {
loading.value = true
try { rows.value = await execSQL(sql, params) }
finally { loading.value = false }
}
const unsubs = []
onMounted(() => {
refresh()
watchTables.forEach(t => unsubs.push(onTableChange(t, refresh)))
})
onUnmounted(() => unsubs.forEach(fn => fn()))
return { rows, loading, refresh }
}

View File

@@ -0,0 +1,43 @@
import SQLiteESMFactory from '@journeyapps/wa-sqlite/dist/wa-sqlite-async.mjs'
import { IDBBatchAtomicVFS } from '@journeyapps/wa-sqlite/src/examples/IDBBatchAtomicVFS.js'
import * as SQLite from '@journeyapps/wa-sqlite'
import { SCHEMA_SQL } from './schema.js'
let db = null
let sqlite3 = null
const tableListeners = new Map()
export async function initDatabase() {
if (db) return db
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')
for (const sql of SCHEMA_SQL.split(';').filter(s => s.trim())) {
await sqlite3.exec(db, sql)
}
return db
}
export function notifyTableChanged(table) {
tableListeners.get(table)?.forEach(cb => cb())
}
export function onTableChange(table, cb) {
if (!tableListeners.has(table)) tableListeners.set(table, new Set())
tableListeners.get(table).add(cb)
return () => tableListeners.get(table).delete(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)
})
return results
}

88
frontend/src/db/schema.js Normal file
View File

@@ -0,0 +1,88 @@
export const SCHEMA_SQL = `
CREATE TABLE IF NOT EXISTS tenants (
id TEXT PRIMARY KEY, tenant_id TEXT, nume TEXT, cui TEXT, reg_com TEXT,
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 vehicles (
id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL,
client_nume TEXT, client_telefon TEXT, client_email TEXT,
client_cod_fiscal TEXT, client_adresa TEXT,
nr_inmatriculare TEXT, marca_id TEXT, model_id TEXT,
an_fabricatie INTEGER, serie_sasiu TEXT, tip_motor_id 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,
tip_deviz_id TEXT, status TEXT DEFAULT 'DRAFT',
km_intrare INTEGER, observatii 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
);
CREATE TABLE IF NOT EXISTS order_lines (
id TEXT PRIMARY KEY, order_id TEXT NOT NULL, tenant_id TEXT NOT NULL,
tip TEXT, descriere TEXT,
norma_id TEXT, ore REAL, pret_ora REAL,
um TEXT, cantitate REAL, pret_unitar REAL,
total REAL, mecanic_id TEXT, ordine INTEGER,
oracle_id INTEGER, created_at TEXT, updated_at TEXT
);
CREATE TABLE IF NOT EXISTS invoices (
id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, order_id TEXT,
nr_factura TEXT, serie_factura TEXT, data_factura TEXT,
modalitate_plata TEXT, client_nume TEXT, client_cod_fiscal TEXT, nr_auto TEXT,
total_fara_tva REAL, tva REAL, total_general REAL,
oracle_id INTEGER, created_at TEXT, updated_at TEXT
);
CREATE TABLE IF NOT EXISTS appointments (
id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, vehicle_id TEXT,
client_nume TEXT, client_telefon TEXT, data_ora TEXT,
durata_minute INTEGER DEFAULT 60, observatii TEXT,
status TEXT DEFAULT 'PROGRAMAT', order_id TEXT,
oracle_id INTEGER, created_at TEXT, updated_at TEXT
);
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
);
CREATE TABLE IF NOT EXISTS catalog_modele (
id TEXT PRIMARY KEY, marca_id TEXT, denumire TEXT, oracle_id INTEGER, 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
);
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
);
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
);
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
);
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
);
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
);
CREATE TABLE IF NOT EXISTS _sync_queue (
id TEXT PRIMARY KEY, table_name TEXT, row_id TEXT,
operation TEXT, data_json TEXT, created_at TEXT, synced_at TEXT
);
CREATE TABLE IF NOT EXISTS _sync_state (table_name TEXT PRIMARY KEY, last_sync_at TEXT);
CREATE TABLE IF NOT EXISTS _local_settings (key TEXT PRIMARY KEY, value TEXT);
PRAGMA journal_mode=WAL;
`;
export const SYNC_TABLES = [
'vehicles', 'orders', 'order_lines', 'invoices', 'appointments',
'catalog_marci', 'catalog_modele', 'catalog_ansamble', 'catalog_norme',
'catalog_preturi', 'catalog_tipuri_deviz', 'catalog_tipuri_motoare', 'mecanici'
];

88
frontend/src/db/sync.js Normal file
View File

@@ -0,0 +1,88 @@
import { execSQL, notifyTableChanged } from './database.js'
const API_URL = import.meta.env.VITE_API_URL || '/api'
export class SyncEngine {
constructor() {
this.syncing = false
this.online = navigator.onLine
window.addEventListener('online', () => { this.online = true; this.pushQueue() })
window.addEventListener('offline', () => { this.online = false })
}
getToken() { return localStorage.getItem('token') }
async fullSync() {
const token = this.getToken()
if (!token) return
const res = await fetch(`${API_URL}/sync/full`, { headers: { Authorization: `Bearer ${token}` } })
if (!res.ok) return
const { tables, synced_at } = await res.json()
for (const [tableName, rows] of Object.entries(tables)) {
for (const row of rows) {
const cols = Object.keys(row).join(', ')
const ph = Object.keys(row).map(() => '?').join(', ')
await execSQL(`INSERT OR REPLACE INTO ${tableName} (${cols}) VALUES (${ph})`, Object.values(row))
}
notifyTableChanged(tableName)
await execSQL(`INSERT OR REPLACE INTO _sync_state VALUES (?, ?)`, [tableName, synced_at])
}
}
async incrementalSync() {
const token = this.getToken()
if (!token || !this.online) return
const [state] = await execSQL(`SELECT MIN(last_sync_at) as since FROM _sync_state`)
if (!state?.since) return this.fullSync()
const res = await fetch(`${API_URL}/sync/changes?since=${encodeURIComponent(state.since)}`,
{ headers: { Authorization: `Bearer ${token}` } })
if (!res.ok) return
const { tables, synced_at } = await res.json()
for (const [tableName, rows] of Object.entries(tables)) {
for (const row of rows) {
const cols = Object.keys(row).join(', ')
const ph = Object.keys(row).map(() => '?').join(', ')
await execSQL(`INSERT OR REPLACE INTO ${tableName} (${cols}) VALUES (${ph})`, Object.values(row))
}
if (rows.length) notifyTableChanged(tableName)
await execSQL(`INSERT OR REPLACE INTO _sync_state VALUES (?, ?)`, [tableName, synced_at])
}
}
async addToQueue(tableName, rowId, operation, data) {
const id = crypto.randomUUID()
await execSQL(
`INSERT INTO _sync_queue (id, table_name, row_id, operation, data_json, created_at) VALUES (?,?,?,?,?,?)`,
[id, tableName, rowId, operation, JSON.stringify(data), new Date().toISOString()]
)
if (this.online) this.pushQueue()
}
async pushQueue() {
if (this.syncing) return
const token = this.getToken()
if (!token) return
this.syncing = true
try {
const queue = await execSQL(`SELECT * FROM _sync_queue WHERE synced_at IS NULL ORDER BY created_at`)
if (!queue.length) return
const res = await fetch(`${API_URL}/sync/push`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ operations: queue.map(q => ({
table: q.table_name, id: q.row_id, operation: q.operation,
data: JSON.parse(q.data_json), timestamp: q.created_at
})) })
})
if (res.ok) {
const ids = queue.map(() => '?').join(',')
await execSQL(`UPDATE _sync_queue SET synced_at=? WHERE id IN (${ids})`,
[new Date().toISOString(), ...queue.map(q => q.id)])
}
} finally {
this.syncing = false
}
}
}
export const syncEngine = new SyncEngine()

View File

@@ -0,0 +1,89 @@
<template>
<div class="min-h-screen bg-gray-50">
<!-- Desktop sidebar -->
<aside class="hidden md:fixed md:inset-y-0 md:flex md:w-56 md:flex-col">
<div class="flex flex-col flex-grow bg-gray-900 overflow-y-auto">
<div class="px-4 py-5">
<h1 class="text-xl font-bold text-white">ROA AUTO</h1>
</div>
<nav class="flex-1 px-2 space-y-1">
<router-link
v-for="item in navItems"
:key="item.path"
:to="item.path"
class="flex items-center px-3 py-2 text-sm font-medium rounded-md text-gray-300 hover:bg-gray-800 hover:text-white"
active-class="!bg-gray-800 !text-white"
>
{{ item.label }}
</router-link>
</nav>
<div class="px-4 py-3 border-t border-gray-700">
<SyncIndicator />
<button
@click="logout"
class="mt-2 w-full text-left text-sm text-gray-400 hover:text-white"
>
Deconectare
</button>
</div>
</div>
</aside>
<!-- Main content -->
<div class="md:pl-56">
<!-- Mobile header -->
<header class="md:hidden bg-white border-b border-gray-200 px-4 py-3 flex items-center justify-between">
<h1 class="text-lg font-bold text-gray-900">ROA AUTO</h1>
<SyncIndicator />
</header>
<main class="p-4 md:p-6">
<slot />
</main>
<!-- Mobile bottom nav -->
<nav class="md:hidden fixed bottom-0 inset-x-0 bg-white border-t border-gray-200 flex">
<router-link
v-for="item in mobileNavItems"
:key="item.path"
:to="item.path"
class="flex-1 flex flex-col items-center py-2 text-xs text-gray-500"
active-class="!text-blue-600"
>
{{ item.label }}
</router-link>
</nav>
</div>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth.js'
import SyncIndicator from '../components/common/SyncIndicator.vue'
const router = useRouter()
const auth = useAuthStore()
const navItems = [
{ path: '/dashboard', label: 'Dashboard' },
{ path: '/orders', label: 'Comenzi' },
{ path: '/vehicles', label: 'Vehicule' },
{ path: '/appointments', label: 'Programari' },
{ path: '/catalog', label: 'Catalog' },
{ path: '/settings', label: 'Setari' },
]
const mobileNavItems = [
{ path: '/dashboard', label: 'Acasa' },
{ path: '/orders', label: 'Comenzi' },
{ path: '/vehicles', label: 'Vehicule' },
{ path: '/appointments', label: 'Programari' },
{ path: '/settings', label: 'Setari' },
]
function logout() {
auth.logout()
router.push('/login')
}
</script>

View File

@@ -0,0 +1,11 @@
<template>
<div class="min-h-screen bg-gray-100 flex items-center justify-center p-4">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900">ROA AUTO</h1>
<p class="text-gray-500 mt-1">Management Service Auto</p>
</div>
<slot />
</div>
</div>
</template>

21
frontend/src/main.js Normal file
View File

@@ -0,0 +1,21 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router/index.js'
import { initDatabase } from './db/database.js'
import { syncEngine } from './db/sync.js'
import { useAuthStore } from './stores/auth.js'
import './assets/css/main.css'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
const auth = useAuthStore()
if (auth.isAuthenticated) {
initDatabase().then(() => syncEngine.fullSync())
}
app.mount('#app')

View File

@@ -0,0 +1,27 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '../stores/auth.js'
const router = createRouter({
history: createWebHistory(),
routes: [
{ 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/new', component: () => import('../views/orders/OrderCreateView.vue'), meta: { requiresAuth: true } },
{ path: '/orders/:id', component: () => import('../views/orders/OrderDetailView.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 } },
{ path: '/settings', component: () => import('../views/settings/SettingsView.vue'), meta: { requiresAuth: true } },
{ path: '/p/:token', component: () => import('../views/client/DevizPublicView.vue') },
{ path: '/', redirect: '/dashboard' },
],
scrollBehavior: () => ({ top: 0 })
})
router.beforeEach((to) => {
if (to.meta.requiresAuth && !useAuthStore().isAuthenticated) return '/login'
})
export default router

View File

@@ -0,0 +1,43 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
const API_URL = import.meta.env.VITE_API_URL || '/api'
export const useAuthStore = defineStore('auth', () => {
const token = ref(localStorage.getItem('token'))
const payload = computed(() => {
if (!token.value) return null
try { return JSON.parse(atob(token.value.split('.')[1])) } catch { return null }
})
const isAuthenticated = computed(() => !!token.value && payload.value?.exp * 1000 > Date.now())
const tenantId = computed(() => payload.value?.tenant_id)
const plan = computed(() => payload.value?.plan || 'free')
async function login(email, password) {
const res = await fetch(`${API_URL}/auth/login`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
})
if (!res.ok) throw new Error('Credentiale invalide')
const data = await res.json()
token.value = data.access_token
localStorage.setItem('token', data.access_token)
return data
}
async function register(email, password, tenant_name, telefon) {
const res = await fetch(`${API_URL}/auth/register`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, tenant_name, telefon })
})
if (!res.ok) throw new Error('Inregistrare esuata')
const data = await res.json()
token.value = data.access_token
localStorage.setItem('token', data.access_token)
return data
}
function logout() { token.value = null; localStorage.removeItem('token') }
return { token, isAuthenticated, tenantId, plan, login, register, logout }
})

View File

@@ -0,0 +1,6 @@
<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>
</template>

View File

@@ -0,0 +1,70 @@
<template>
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-xl font-semibold mb-6">Autentificare</h2>
<form @submit.prevent="handleLogin" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Email</label>
<input
v-model="email"
type="email"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="email@exemplu.ro"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Parola</label>
<input
v-model="password"
type="password"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Parola"
/>
</div>
<p v-if="error" class="text-sm text-red-600">{{ error }}</p>
<button
type="submit"
:disabled="loading"
class="w-full py-2 px-4 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{{ loading ? 'Se autentifica...' : 'Autentificare' }}
</button>
</form>
<p class="mt-4 text-center text-sm text-gray-500">
Nu ai cont?
<router-link to="/register" class="text-blue-600 hover:underline">Inregistreaza-te</router-link>
</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '../../stores/auth.js'
import { initDatabase } from '../../db/database.js'
import { syncEngine } from '../../db/sync.js'
const router = useRouter()
const auth = useAuthStore()
const email = ref('')
const password = ref('')
const error = ref('')
const loading = ref(false)
async function handleLogin() {
error.value = ''
loading.value = true
try {
await auth.login(email.value, password.value)
await initDatabase()
syncEngine.fullSync()
router.push('/dashboard')
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
</script>

View File

@@ -0,0 +1,93 @@
<template>
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-xl font-semibold mb-6">Inregistrare</h2>
<form @submit.prevent="handleRegister" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Numele service-ului</label>
<input
v-model="tenantName"
type="text"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Service Auto Ionescu"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Telefon</label>
<input
v-model="telefon"
type="tel"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="0722 000 000"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Email</label>
<input
v-model="email"
type="email"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="email@exemplu.ro"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Parola</label>
<input
v-model="password"
type="password"
required
minlength="6"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Minim 6 caractere"
/>
</div>
<p v-if="error" class="text-sm text-red-600">{{ error }}</p>
<button
type="submit"
:disabled="loading"
class="w-full py-2 px-4 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{{ loading ? 'Se creeaza contul...' : 'Creeaza cont (trial 30 zile)' }}
</button>
</form>
<p class="mt-4 text-center text-sm text-gray-500">
Ai deja cont?
<router-link to="/login" class="text-blue-600 hover:underline">Autentificare</router-link>
</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '../../stores/auth.js'
import { initDatabase } from '../../db/database.js'
import { syncEngine } from '../../db/sync.js'
const router = useRouter()
const auth = useAuthStore()
const tenantName = ref('')
const telefon = ref('')
const email = ref('')
const password = ref('')
const error = ref('')
const loading = ref(false)
async function handleRegister() {
error.value = ''
loading.value = true
try {
await auth.register(email.value, password.value, tenantName.value, telefon.value)
await initDatabase()
syncEngine.fullSync()
router.push('/dashboard')
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
</script>

View File

@@ -0,0 +1,6 @@
<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>
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<div>
<h1 class="text-2xl font-bold text-gray-900 mb-6">Deviz</h1>
<p class="text-gray-500">Portal public client - va fi implementat in TASK-008.</p>
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<div>
<h1 class="text-2xl font-bold text-gray-900 mb-6">Dashboard</h1>
<p class="text-gray-500">Dashboard - va fi implementat in TASK-006.</p>
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<div>
<h1 class="text-2xl font-bold text-gray-900 mb-6">Comanda noua</h1>
<p class="text-gray-500">Creare comanda - va fi implementat in TASK-006.</p>
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<div>
<h1 class="text-2xl font-bold text-gray-900 mb-6">Detalii comanda</h1>
<p class="text-gray-500">Detalii comanda - va fi implementat in TASK-008.</p>
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<div>
<h1 class="text-2xl font-bold text-gray-900 mb-6">Comenzi</h1>
<p class="text-gray-500">Lista comenzi - va fi implementat in TASK-006.</p>
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<div>
<h1 class="text-2xl font-bold text-gray-900 mb-6">Setari</h1>
<p class="text-gray-500">Setari - va fi implementat in TASK-011.</p>
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<div>
<h1 class="text-2xl font-bold text-gray-900 mb-6">Vehicule</h1>
<p class="text-gray-500">Lista vehicule - va fi implementat in TASK-006.</p>
</div>
</template>

21
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,21 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
vue(),
tailwindcss(),
],
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
},
},
},
optimizeDeps: {
exclude: ['@journeyapps/wa-sqlite'],
},
})