80 lines
4.1 KiB
HTML
80 lines
4.1 KiB
HTML
<!-- front.html – Vue3 CDN + Tailwind, 100 % côté client -->
|
||
<!doctype html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>WEVIA ERP</title>
|
||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
</head>
|
||
<body class="bg-gray-50">
|
||
<div id="app" class="min-h-screen flex flex-col">
|
||
<header class="bg-indigo-700 text-white p-4 shadow">
|
||
<h1 class="text-xl font-bold">WEVIA ERP</h1>
|
||
</header>
|
||
<main class="flex-grow container mx-auto p-4">
|
||
<div v-if="!token">
|
||
<form @submit.prevent="login" class="max-w-sm mx-auto bg-white p-6 rounded shadow">
|
||
<h2 class="mb-4 text-lg font-semibold">Connexion</h2>
|
||
<input v-model="formLogin.username" placeholder="email" class="w-full mb-3 px-3 py-2 border rounded">
|
||
<input v-model="formLogin.password" type="password" placeholder="mot de passe" class="w-full mb-3 px-3 py-2 border rounded">
|
||
<button class="w-full bg-indigo-600 text-white py-2 rounded">Se connecter</button>
|
||
</form>
|
||
</div>
|
||
<div v-else>
|
||
<button @click="logout" class="mb-4 text-sm text-indigo-700">Déconnexion</button>
|
||
<div class="grid gap-6 md:grid-cols-2">
|
||
<section class="bg-white p-4 rounded shadow">
|
||
<h2 class="mb-2 font-semibold">Produits</h2>
|
||
<ul>
|
||
<li v-for="p in products" :key="p.id" class="flex justify-between py-1">
|
||
<span>{{ p.name }}</span>
|
||
<span class="font-medium">{{ p.qty_in_stock }} u. – {{ p.price }} €</span>
|
||
</li>
|
||
</ul>
|
||
</section>
|
||
<section class="bg-white p-4 rounded shadow">
|
||
<h2 class="mb-2 font-semibold">Nouvelle vente</h2>
|
||
<form @submit.prevent="createSale">
|
||
<input v-model.number="saleForm.customer_id" placeholder="ID client" class="w-full mb-2 px-3 py-2 border rounded">
|
||
<div v-for="(l,i) in saleForm.lines" :key="i" class="flex gap-2 mb-2">
|
||
<input v-model.number="l.product_id" placeholder="ID produit" class="w-1/3 px-3 py-2 border rounded">
|
||
<input v-model.number="l.qty" placeholder="Qté" class="w-1/3 px-3 py-2 border rounded">
|
||
<button type="button" @click="saleForm.lines.push({})" class="text-green-600">+</button>
|
||
<button type="button" @click="saleForm.lines.splice(i,1)" class="text-red-600">–</button>
|
||
</div>
|
||
<button class="w-full bg-indigo-600 text-white py-2 rounded">Enregistrer</button>
|
||
</form>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
<script>
|
||
const { createApp, reactive, ref, onMounted } = Vue;
|
||
createApp({
|
||
setup() {
|
||
const token = ref(localStorage.token||"");
|
||
const formLogin = reactive({username:"",password:""});
|
||
const products = ref([]);
|
||
const saleForm = reactive({customer_id:"",lines:[{product_id:"",qty:""}]});
|
||
const api = (path,opts={}) => fetch("http://localhost:8000/api/v1"+path,{headers:{Authorization:"Bearer "+token.value},...opts}).then(r=>r.ok?r.json():Promise.reject(r));
|
||
const login = async () => {
|
||
const body = new URLSearchParams({username:formLogin.username, password:formLogin.password});
|
||
const res = await fetch("http://localhost:8000/api/v1/token",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body});
|
||
const json = await res.json();
|
||
token.value = json.access_token;
|
||
localStorage.token = token.value;
|
||
loadProducts();
|
||
};
|
||
const logout = () => { token.value=""; localStorage.token=""; };
|
||
const loadProducts = () => api("/products").then(r => products.value = r);
|
||
const createSale = () => api("/sales",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(saleForm)})
|
||
.then(() => { alert("Vente créée"); loadProducts(); saleForm.lines=[{}]; });
|
||
onMounted(() => { if(token.value) loadProducts(); });
|
||
return { token, formLogin, login, logout, products, saleForm, createSale };
|
||
}
|
||
}).mount('#app');
|
||
</script>
|
||
</body>
|
||
</html> |