feat: Implement client authentication and pre-fill functionality
- Added AuthController for client login and token management. - Introduced AuthGate for handling authentication state and login UI. - Updated API to support fetching client data for pre-filling forms. - Modified App.vue to pre-fill client information if logged in. - Enhanced client model to include username and password fields. - Created migration for adding username and password to clients table. - Added utility for generating secure usernames and passwords. - Updated frontend to handle client tokens for pre-filling checkout forms. - Removed old CSS file and updated HTML references to new assets. - Added StatsOverview widget for displaying client and quote statistics. - Improved error handling and user feedback in the login process.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
:root{--brand: #1a4b8c;--brand-dark: #143a6b;--bg: #f5f6f8;--card: #ffffff;--border: #e4e7ec;--text: #1f2937;--muted: #6b7280;--danger: #c0392b;--ok: #1e874b}*{box-sizing:border-box}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:var(--bg);color:var(--text)}.app-header{background:var(--brand);color:#fff;padding:18px 20px}.app-header h1{margin:0;font-size:18px;font-weight:600}.container{max-width:780px;margin:0 auto;padding:20px 16px 60px}.card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:16px;margin-bottom:14px}.item{display:flex;gap:14px}.item img{width:90px;height:90px;object-fit:contain;background:#fafafa;border:1px solid var(--border);border-radius:8px;flex:none}.item-body{flex:1;min-width:0}.item-title{font-weight:600;margin:0 0 2px}.item-sub{color:var(--muted);font-size:13px;margin-bottom:6px}.conf{font-size:13px;color:#374151;line-height:1.5}.conf b{color:var(--muted);font-weight:500}.item-foot{display:flex;align-items:center;justify-content:space-between;margin-top:8px}.price{font-weight:700}.totale-row{display:flex;justify-content:space-between;align-items:baseline;font-size:20px;font-weight:700;color:var(--brand);padding-top:8px}.iva-nota{color:var(--muted);font-size:12px;font-weight:400}button{font:inherit;cursor:pointer;border:none;border-radius:8px;padding:12px 18px}.btn-primary{background:var(--brand);color:#fff;font-weight:600;width:100%}.btn-primary:hover{background:var(--brand-dark)}.btn-primary:disabled{opacity:.6;cursor:not-allowed}.btn-link{background:none;color:var(--muted);padding:6px;text-decoration:underline}.btn-remove{background:none;color:var(--danger);padding:4px 8px;font-size:13px}.field{margin-bottom:12px}.field label{display:block;font-size:13px;color:var(--muted);margin-bottom:4px}.field input,.field textarea{width:100%;padding:10px 12px;border:1px solid var(--border);border-radius:8px;font:inherit}.field .err{color:var(--danger);font-size:12px;margin-top:3px}.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:12px}@media(max-width:520px){.grid-2{grid-template-columns:1fr}}.muted{color:var(--muted)}.center{text-align:center}.empty{text-align:center;padding:40px 20px;color:var(--muted)}.success-icon{font-size:48px}.alert{background:#fdecea;color:var(--danger);padding:10px 12px;border-radius:8px;margin-bottom:12px;font-size:14px}
|
||||
Vendored
+4
-3
@@ -3,9 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Il tuo preventivo · Infissionline</title>
|
||||
<script type="module" crossorigin src="/assets/index-BidQ3v5l.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DNK0MLY8.css">
|
||||
<link rel="icon" type="image/jpeg" href="/kreios-logo.jpg" />
|
||||
<title>Il tuo preventivo · Krèios</title>
|
||||
<script type="module" crossorigin src="/assets/index-D5nts1N1.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-vT7pz0E4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { api, resolveToken, resolveReturnUrl, clearToken, formatEuro } from './api.js'
|
||||
import { api, resolveToken, resolveReturnUrl, resolveClienteToken, clearToken, formatEuro } from './api.js'
|
||||
|
||||
const step = ref('cart') // cart | checkout | done
|
||||
const loading = ref(true)
|
||||
@@ -16,9 +16,37 @@ const form = reactive({
|
||||
nome: '', cognome: '', ragione_sociale: '', email: '', telefono: '',
|
||||
indirizzo: '', cap: '', citta: '', provincia: '', note: '',
|
||||
})
|
||||
const anagraficaCaricata = ref(false)
|
||||
|
||||
const isEmpty = computed(() => !carrello.items || carrello.items.length === 0)
|
||||
|
||||
// Se il cliente è loggato nel configuratore, precompila il form con i suoi dati già
|
||||
// salvati (solo i campi ancora vuoti, per non sovrascrivere quanto già digitato).
|
||||
async function precompilaAnagrafica() {
|
||||
if (anagraficaCaricata.value) return
|
||||
anagraficaCaricata.value = true
|
||||
|
||||
const clienteToken = resolveClienteToken()
|
||||
if (!clienteToken) return
|
||||
|
||||
try {
|
||||
const data = await api.me(clienteToken)
|
||||
const campi = ['nome', 'cognome', 'ragione_sociale', 'email', 'telefono', 'indirizzo', 'cap', 'citta', 'provincia', 'note']
|
||||
for (const campo of campi) {
|
||||
if (!form[campo] && data.cliente[campo]) {
|
||||
form[campo] = data.cliente[campo]
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Token scaduto/non valido: l'utente compila il form manualmente.
|
||||
}
|
||||
}
|
||||
|
||||
function vaiAlCheckout() {
|
||||
step.value = 'checkout'
|
||||
precompilaAnagrafica()
|
||||
}
|
||||
|
||||
async function loadCarrello() {
|
||||
loading.value = true
|
||||
errorMsg.value = ''
|
||||
@@ -123,7 +151,7 @@ onMounted(loadCarrello)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-primary" @click="step = 'checkout'">
|
||||
<button class="btn-primary" @click="vaiAlCheckout">
|
||||
Richiedi il preventivo
|
||||
</button>
|
||||
<a v-if="returnUrl" class="btn-link" style="width:100%;margin-top:8px" :href="returnUrl">
|
||||
|
||||
@@ -2,8 +2,12 @@ const BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000/api'
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
})
|
||||
const data = await res.json().catch(() => ({}))
|
||||
if (!res.ok) {
|
||||
@@ -20,23 +24,34 @@ export const api = {
|
||||
rimuoviItem: (token, itemId) => request(`/carrello/${token}/item/${itemId}`, { method: 'DELETE' }),
|
||||
conferma: (token, dati) =>
|
||||
request(`/carrello/${token}/conferma`, { method: 'POST', body: JSON.stringify(dati) }),
|
||||
// Dati anagrafici del cliente loggato nel configuratore, per precompilare il checkout.
|
||||
me: (clienteToken) =>
|
||||
request('/auth/me', { headers: { Authorization: `Bearer ${clienteToken}` } }),
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'kreios_cart_token'
|
||||
const RETURN_URL_KEY = 'kreios_return_url'
|
||||
const CLIENTE_TOKEN_KEY = 'kreios_cliente_token'
|
||||
|
||||
export function resolveToken() {
|
||||
const url = new URL(window.location.href)
|
||||
const fromUrl = url.searchParams.get('token')
|
||||
const ritorno = url.searchParams.get('ritorno')
|
||||
const clienteToken = url.searchParams.get('cliente_token')
|
||||
if (ritorno) {
|
||||
localStorage.setItem(RETURN_URL_KEY, ritorno)
|
||||
}
|
||||
// Token del cliente loggato nel configuratore (vedi AuthGate.getToken() in starter.js),
|
||||
// usato per precompilare l'anagrafica nel checkout. Assente se non era loggato.
|
||||
if (clienteToken) {
|
||||
localStorage.setItem(CLIENTE_TOKEN_KEY, clienteToken)
|
||||
}
|
||||
if (fromUrl) {
|
||||
localStorage.setItem(STORAGE_KEY, fromUrl)
|
||||
// pulisci l'URL mantenendo token/ritorno in localStorage
|
||||
// pulisci l'URL mantenendo token/ritorno/cliente_token in localStorage
|
||||
url.searchParams.delete('token')
|
||||
url.searchParams.delete('ritorno')
|
||||
url.searchParams.delete('cliente_token')
|
||||
window.history.replaceState({}, '', url.pathname + url.search)
|
||||
return fromUrl
|
||||
}
|
||||
@@ -50,6 +65,10 @@ export function resolveReturnUrl() {
|
||||
return localStorage.getItem(RETURN_URL_KEY)
|
||||
}
|
||||
|
||||
export function resolveClienteToken() {
|
||||
return localStorage.getItem(CLIENTE_TOKEN_KEY)
|
||||
}
|
||||
|
||||
export function clearToken() {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user