Initial commit: Robot ökoszisztéma v2.0 - Stabilizált jármű és szerviz robotok

This commit is contained in:
Kincses
2026-03-04 02:03:03 +01:00
commit 250f4f4b8f
7942 changed files with 449625 additions and 0 deletions

24
frontend/.gitignore vendored Executable file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
frontend/.vscode/extensions.json vendored Executable file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

22
frontend/Dockerfile Executable file
View File

@@ -0,0 +1,22 @@
# 1. szakasz: Build (lefordítja a kódot)
FROM node:20-slim as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 2. szakasz: Kiszolgálás (Nginx)
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
# Nginx konfiguráció, hogy kezelje a Vue router-t
RUN echo 'server { \
listen 80; \
location / { \
root /usr/share/nginx/html; \
index index.html; \
try_files $uri $uri/ /index.html; \
} \
}' > /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

5
frontend/README.md Executable file
View File

@@ -0,0 +1,5 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

13
frontend/index.html Executable file
View File

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

29
frontend/nginx.conf Executable file
View File

@@ -0,0 +1,29 @@
worker_processes 4; # Itt korlátozzuk a 108 helyett!
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html; # Itt keresi a fájlokat
index index.html;
location / {
try_files $uri $uri/ /index.html; # SPA (Vue/React) támogatás
}
# API kérések továbbküldése a backendnek (opcionális, ha a frontend kéri)
location /api/ {
proxy_pass http://service_finder_api:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}

2971
frontend/package-lock.json generated Executable file

File diff suppressed because it is too large Load Diff

27
frontend/package.json Executable file
View File

@@ -0,0 +1,27 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/postcss": "^4.1.18",
"axios": "^1.13.4",
"chart.js": "^4.5.1",
"pinia": "^3.0.4",
"vue": "^3.5.24",
"vue-chartjs": "^5.3.3",
"vue-router": "^5.0.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"autoprefixer": "^10.4.23",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"vite": "^7.2.4"
}
}

6
frontend/postcss.config.js Executable file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
}

1
frontend/public/vite.svg Executable file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

99
frontend/src/App.vue Executable file
View File

@@ -0,0 +1,99 @@
<script setup>
import { ref, onMounted, watchEffect } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
const isLoggedIn = ref(false)
const isAdmin = ref(false)
// Figyeljük a bejelentkezési állapotot
watchEffect(() => {
isLoggedIn.value = !!localStorage.getItem('token')
isAdmin.value = localStorage.getItem('is_admin') === 'true'
})
const handleLogout = () => {
localStorage.removeItem('token')
localStorage.removeItem('is_admin')
isLoggedIn.value = false
router.push('/login')
}
</script>
<template>
<div class="min-h-screen flex flex-col bg-gray-50 text-gray-900 font-sans">
<nav class="bg-blue-700 text-white p-4 shadow-lg flex justify-between items-center z-50">
<div class="flex items-center gap-2 cursor-pointer" @click="router.push('/')">
<span class="text-2xl">🚗</span>
<span class="font-bold text-xl tracking-tight">Service Finder</span>
</div>
<div class="space-x-6 hidden md:flex items-center">
<template v-if="isLoggedIn">
<router-link to="/" class="nav-link">Dashboard</router-link>
<router-link to="/expenses" class="nav-link">Költségek</router-link>
<router-link v-if="isAdmin" to="/admin" class="text-amber-400 font-bold hover:text-amber-300"> Admin</router-link>
<button @click="handleLogout" class="bg-blue-800 px-4 py-2 rounded-lg text-sm hover:bg-blue-900 transition">Kijelentkezés</button>
</template>
<router-link v-else to="/login" class="bg-white text-blue-700 px-4 py-2 rounded-lg font-bold shadow-md hover:bg-blue-50 transition">
Belépés
</router-link>
</div>
</nav>
<main class="flex-grow container mx-auto p-4 md:p-8 pb-24 md:pb-8">
<router-view></router-view>
</main>
<footer v-if="isLoggedIn" class="md:hidden bg-white border-t flex justify-around p-3 sticky bottom-0 z-50 shadow-[0_-4px_10px_rgba(0,0,0,0.05)]">
<router-link to="/" class="mobile-nav-link">
<span class="text-2xl">📊</span><span class="text-[10px] font-medium uppercase">Jelentés</span>
</router-link>
<router-link to="/expenses" class="mobile-nav-link">
<span class="text-2xl">💸</span><span class="text-[10px] font-medium uppercase">Költség</span>
</router-link>
<router-link v-if="isAdmin" to="/admin" class="mobile-nav-link">
<span class="text-2xl"></span><span class="text-[10px] font-medium uppercase text-amber-600">Admin</span>
</router-link>
<button @click="handleLogout" class="mobile-nav-link text-red-500">
<span class="text-2xl">🚪</span><span class="text-[10px] font-medium uppercase">Ki</span>
</button>
</footer>
</div>
</template>
<style scoped>
.nav-link {
color: rgba(255, 255, 255, 0.8);
text-decoration: none;
transition: all 0.2s;
padding-bottom: 2px;
}
.nav-link:hover {
color: white;
}
.router-link-active.nav-link {
color: white;
font-weight: 700;
border-bottom: 2px solid white;
}
.mobile-nav-link {
display: flex;
flex-direction: column;
align-items: center;
color: #4b5563;
text-decoration: none;
}
.router-link-active.mobile-nav-link {
color: #1d4ed8;
}
.router-link-active.mobile-nav-link span:last-child {
font-weight: 700;
}
</style>

1
frontend/src/assets/vue.svg Executable file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,43 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

10
frontend/src/main.js Executable file
View File

@@ -0,0 +1,10 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import './style.css'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

57
frontend/src/router/index.js Executable file
View File

@@ -0,0 +1,57 @@
import { createRouter, createWebHistory } from 'vue-router';
// Nézetek importálása
import Dashboard from '../views/Dashboard.vue';
import Expenses from '../views/Expenses.vue';
import AddExpense from '../views/AddExpense.vue';
import Login from '../views/Login.vue';
import Register from '../views/Register.vue';
import ForgotPassword from '../views/ForgotPassword.vue';
import ResetPassword from '../views/ResetPassword.vue';
import AddVehicle from '../views/AddVehicle.vue';
import AdminStats from '../views/admin/AdminStats.vue';
const routes = [
// Védett útvonalak
{ path: '/', name: 'Dashboard', component: Dashboard, meta: { requiresAuth: true } },
{ path: '/expenses', name: 'Expenses', component: Expenses, meta: { requiresAuth: true } },
{ path: '/expenses/add', name: 'AddExpense', component: AddExpense, meta: { requiresAuth: true } },
{ path: '/vehicles/add', name: 'AddVehicle', component: AddVehicle, meta: { requiresAuth: true } },
// ADMIN útvonal
{
path: '/admin',
name: 'Admin',
component: AdminStats,
meta: { requiresAuth: true, requiresAdmin: true }
},
// Nyilvános útvonalak
{ path: '/login', name: 'Login', component: Login },
{ path: '/register', name: 'Register', component: Register },
{ path: '/forgot-password', name: 'ForgotPassword', component: ForgotPassword },
{ path: '/reset-password', name: 'ResetPassword', component: ResetPassword },
];
const router = createRouter({
history: createWebHistory(),
routes
});
// A "SOROMPÓ" (Auth Guard) LOGIKA
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token');
// Egyszerűsített admin check (később a JWT-ből decodoljuk)
const isAdmin = localStorage.getItem('is_admin') === 'true';
if (to.meta.requiresAuth && !token) {
next('/login');
} else if (to.meta.requiresAdmin && !isAdmin) {
// Ha admin oldalra menne, de nem admin, dobja a főoldalra
next('/');
} else {
next();
}
});
export default router;

8
frontend/src/style.css Executable file
View File

@@ -0,0 +1,8 @@
@import "tailwindcss";
/* Tiszta CSS-t használunk a body-hoz a hiba elkerülése végett */
body {
background-color: #f9fafb; /* gray-50 */
margin: 0;
font-family: sans-serif;
}

View File

@@ -0,0 +1,56 @@
<template>
<div class="max-w-md mx-auto bg-white p-8 rounded-2xl shadow-xl border border-gray-100 mt-10">
<h2 class="text-2xl font-bold text-gray-800 mb-6 flex items-center gap-2">
<span>💸</span> Új költség rögzítése
</h2>
<form @submit.prevent="handleSubmit" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">Kategória</label>
<select v-model="form.category" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 p-2.5 border">
<option value="REFUELING"> Tankolás</option>
<option value="SERVICE">🔧 Szerviz</option>
<option value="INSURANCE">🛡 Biztosítás</option>
<option value="TOLL">🛣 Útdíj / Matrica</option>
<option value="FINE">👮 Büntetés</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Összeg (Ft)</label>
<input v-model="form.amount" type="number" required class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 p-2.5 border" placeholder="Pl: 25000">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Km óra állása</label>
<input v-model="form.odometer_value" type="number" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 p-2.5 border" placeholder="Pl: 145200">
</div>
<button type="submit" class="w-full bg-blue-600 text-white font-bold py-3 rounded-lg hover:bg-blue-700 transition-colors shadow-lg shadow-blue-200">
Mentés rögzítése
</button>
</form>
</div>
</template>
<script setup>
import { ref } from 'vue'
import axios from 'axios'
const form = ref({
category: 'REFUELING',
amount: null,
odometer_value: null,
vehicle_id: 'auto-uuid-helye', // Ezt majd a routerből kapjuk
date: new Date().toISOString().split('T')[0]
})
const handleSubmit = async () => {
try {
await axios.post('http://localhost:8000/api/v1/expenses/add', form.value)
alert("Sikeresen mentve!")
} catch (err) {
alert("Hiba történt a mentéskor.")
}
}
</script>

View File

@@ -0,0 +1,68 @@
<template>
<div class="max-w-2xl mx-auto bg-white p-8 rounded-2xl shadow-lg mt-6">
<h2 class="text-2xl font-bold mb-6 flex items-center gap-2">
<span class="text-3xl"></span> Új jármű hozzáadása
</h2>
<div class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700">Márka keresése</label>
<input v-model="searchQuery" @input="searchBrands" type="text" placeholder="Pl: Audi, Toyota..." class="mt-1 block w-full p-3 border rounded-lg shadow-sm" />
<div v-if="brands.length > 0" class="mt-2 border rounded-lg divide-y bg-gray-50">
<div v-for="brand in brands" :key="brand.id" @click="selectBrand(brand)" class="p-3 hover:bg-blue-100 cursor-pointer flex justify-between">
<span class="font-bold">{{ brand.name }}</span>
<span class="text-xs text-gray-400 italic">{{ brand.country_of_origin }}</span>
</div>
</div>
</div>
<div v-if="selectedBrand" class="p-4 bg-blue-50 rounded-lg border border-blue-200 animate-fade-in">
<p class="font-bold text-blue-800">Kiválasztott márka: {{ selectedBrand.name }}</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<input v-model="form.model_name" type="text" placeholder="Modell (Pl: A4, Corolla)" class="p-2 border rounded" />
<input v-model="form.plate" type="text" placeholder="Rendszám" class="p-2 border rounded uppercase" />
<input v-model="form.vin" type="text" placeholder="Alvázszám (opcionális)" class="p-2 border rounded uppercase" />
<input v-model="form.current_odo" type="number" placeholder="Aktuális km állás" class="p-2 border rounded" />
</div>
<button @click="saveVehicle" class="w-full mt-6 bg-blue-700 text-white py-3 rounded-lg font-bold shadow-lg">Jármű mentése a Széfbe</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'
const router = useRouter()
const searchQuery = ref('')
const brands = ref([])
const selectedBrand = ref(null)
const form = ref({ model_name: '', plate: '', vin: '', current_odo: 0 })
const searchBrands = async () => {
if (searchQuery.value.length < 2) { brands.value = []; return; }
const res = await axios.get(`http://192.168.100.43:8000/api/v1/vehicles/search/brands?q=${searchQuery.value}`)
brands.value = res.data.data
}
const selectBrand = (brand) => {
selectedBrand.value = brand
brands.value = []
}
const saveVehicle = async () => {
const token = localStorage.getItem('token')
try {
await axios.post('http://192.168.100.43:8000/api/v1/vehicles/register', {
brand_id: selectedBrand.value.id,
...form.value
}, { headers: { Authorization: `Bearer ${token}` }})
router.push('/')
} catch (err) {
alert("Hiba a mentés során.")
}
}
</script>

View File

@@ -0,0 +1,33 @@
<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
const report = ref(null)
const loading = ref(true)
onMounted(async () => {
const token = localStorage.getItem('token')
try {
const res = await axios.get('http://localhost:8000/api/v1/reports/summary/latest', {
headers: { Authorization: `Bearer ${token}` }
})
report.value = res.data
} catch (err) {
console.log("Még nincs rögzített adat.")
} finally {
loading.value = false
}
})
</script>
<template>
<div v-if="!loading">
<div v-if="report" class="space-y-6">
</div>
<div v-else class="text-center mt-20">
<span class="text-6xl text-gray-300">📭</span>
<h2 class="text-xl font-bold text-gray-500 mt-4">Még nincsenek rögzített költségeid.</h2>
<router-link to="/expenses" class="mt-4 inline-block text-blue-700 font-bold">Kezdd el itt! </router-link>
</div>
</div>
</template>

View File

@@ -0,0 +1,8 @@
<template>
<div class="p-4">
<h2 class="text-2xl font-bold mb-4 text-gray-800">Költségek kezelése</h2>
<div class="bg-blue-50 p-4 rounded-lg border border-blue-200">
<p>Itt tudod majd rögzíteni és listázni a kiadásaidat.</p>
</div>
</div>
</template>

View File

@@ -0,0 +1,33 @@
<template>
<div class="max-w-md mx-auto mt-20 p-8 bg-white rounded-2xl shadow-xl border">
<h2 class="text-2xl font-bold text-center mb-4 text-gray-800">Elfelejtett jelszó</h2>
<p class="text-sm text-gray-600 mb-6 text-center">Add meg az e-mail címed, és küldünk egy linket a jelszó visszaállításához.</p>
<form @submit.prevent="handleForgot" class="space-y-4">
<input v-model="email" type="email" placeholder="E-mail címed" required class="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500" />
<button type="submit" class="w-full bg-blue-700 text-white py-3 rounded-lg font-bold hover:bg-blue-800">Küldés</button>
</form>
<p v-if="message" class="mt-4 text-center font-medium text-blue-600">{{ message }}</p>
<div class="mt-6 text-center">
<router-link to="/login" class="text-sm text-gray-500 hover:underline">Vissza a belépéshez</router-link>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import axios from 'axios'
const email = ref('')
const message = ref('')
const handleForgot = async () => {
try {
await axios.post(`http://192.168.100.43:8000/api/v2/auth/forgot-password?email=${email.value}`)
message.value = "Ha a cím létezik, a helyreállító levelet elküldtük."
} catch (err) {
message.value = "Hiba történt a kérés feldolgozásakor."
}
}
</script>

87
frontend/src/views/Login.vue Executable file
View File

@@ -0,0 +1,87 @@
<template>
<div class="max-w-md mx-auto mt-20 p-8 bg-white rounded-3xl shadow-2xl border border-gray-100">
<div class="text-center mb-10">
<span class="text-5xl">🚗</span>
<h2 class="text-3xl font-black text-gray-900 mt-4 uppercase tracking-tighter">Belépés</h2>
<p class="text-gray-400 text-sm font-medium">Service Finder V2.0</p>
</div>
<form @submit.prevent="handleLogin" class="space-y-6">
<div class="space-y-1">
<label class="text-xs font-bold text-gray-500 uppercase ml-1">E-mail cím</label>
<input v-model="email" type="email" required
class="w-full p-4 bg-gray-50 border-2 border-transparent rounded-2xl focus:border-blue-500 focus:bg-white transition-all outline-none"
placeholder="kincses@gmail.com" />
</div>
<div class="space-y-1">
<div class="flex justify-between items-center">
<label class="text-xs font-bold text-gray-500 uppercase ml-1">Jelszó</label>
<router-link to="/forgot-password" class="text-xs font-bold text-blue-600 hover:text-blue-800">Elfelejtetted?</router-link>
</div>
<input v-model="password" type="password" required
class="w-full p-4 bg-gray-50 border-2 border-transparent rounded-2xl focus:border-blue-500 focus:bg-white transition-all outline-none"
placeholder="••••••••" />
</div>
<div v-if="error" class="p-4 bg-red-50 border-l-4 border-red-500 text-red-700 rounded-lg text-sm font-bold">
{{ error }}
</div>
<button type="submit" :disabled="loading"
class="w-full bg-blue-700 text-white py-4 rounded-2xl font-black text-lg hover:bg-blue-800 transition-all shadow-xl hover:shadow-blue-200 disabled:bg-gray-300">
{{ loading ? 'ELLENŐRZÉS...' : 'BEJELENTKEZÉS' }}
</button>
</form>
<div class="mt-10 pt-6 border-t border-gray-100 text-center">
<p class="text-gray-500 font-medium mb-3">Nincs még fiókod?</p>
<router-link to="/register"
class="inline-block w-full py-3 px-6 border-2 border-blue-700 text-blue-700 rounded-2xl font-bold hover:bg-blue-50 transition-all">
Új Széf létrehozása
</router-link>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import axios from 'axios'
const email = ref('')
const password = ref('')
const error = ref('')
const loading = ref(false)
const router = useRouter()
// URLSearchParams használata - ez pontosan azt a formátumot küldi, amit a Swagger
const params = new URLSearchParams()
params.append('username', email.value)
params.append('password', password.value)
try {
const res = await axios.post('http://192.168.100.43:8000/api/v2/auth/login', params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
const token = res.data.access_token
localStorage.setItem('token', token)
// Profil lekérése a jogosultságok miatt
const userRes = await axios.get('http://192.168.100.43:8000/api/v1/users/me', {
headers: { Authorization: `Bearer ${token}` }
})
localStorage.setItem('is_admin', userRes.data.is_superuser ? 'true' : 'false')
router.push('/')
} catch (err) {
console.error("Belépési hiba részletei:", err.response?.data)
error.value = err.response?.data?.detail || "Hibás e-mail vagy jelszó."
} finally {
loading.value = false
}
</script>

36
frontend/src/views/Register.vue Executable file
View File

@@ -0,0 +1,36 @@
<template>
<div class="max-w-md mx-auto mt-10 p-8 bg-white rounded-2xl shadow-xl">
<h2 class="text-2xl font-bold mb-6 text-center">Új Széf létrehozása</h2>
<form @submit.prevent="handleRegister" class="space-y-4">
<input v-model="form.first_name" type="text" placeholder="Keresztnév" required class="w-full p-3 border rounded-lg" />
<input v-model="form.last_name" type="text" placeholder="Vezetéknév" required class="w-full p-3 border rounded-lg" />
<input v-model="form.email" type="email" placeholder="E-mail" required class="w-full p-3 border rounded-lg" />
<input v-model="form.password" type="password" placeholder="Jelszó" required class="w-full p-3 border rounded-lg" />
<button class="w-full bg-green-600 text-white py-3 rounded-lg font-bold hover:bg-green-700 transition">Fiók létrehozása</button>
</form>
<p v-if="msg" class="mt-4 text-center font-medium text-green-600">{{ msg }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import axios from 'axios'
const form = ref({ email: '', password: '', first_name: '', last_name: '' })
const msg = ref('')
const handleRegister = async () => {
msg.value = ""; // Üzenet alaphelyzetbe
try {
const res = await axios.post(`http://192.168.100.43:8000/api/v2/auth/register`, null, { params: form.value });
msg.value = "Sikeres regisztráció! Kérlek aktiváld az e-mailedet.";
} catch (err) {
// ITT A JAVÍTÁS: kiolvassuk a backend hibaüzenetét
if (err.response && err.response.data && err.response.data.detail) {
msg.value = "Hiba: " + err.response.data.detail;
} else {
msg.value = "Hiba történt a regisztráció során.";
}
}
}
</script>

View File

@@ -0,0 +1,43 @@
<template>
<div class="max-w-md mx-auto mt-20 p-8 bg-white rounded-2xl shadow-xl border">
<h2 class="text-2xl font-bold text-center mb-6">Új jelszó megadása</h2>
<form @submit.prevent="handleReset" class="space-y-4">
<input v-model="password" type="password" placeholder="Új jelszó" required class="w-full p-3 border rounded-lg" />
<input v-model="passwordConfirm" type="password" placeholder="Jelszó megerősítése" required class="w-full p-3 border rounded-lg" />
<button type="submit" class="w-full bg-blue-700 text-white py-3 rounded-lg font-bold">Jelszó mentése</button>
</form>
<p v-if="error" class="mt-4 text-red-600 text-center">{{ error }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import axios from 'axios'
const route = useRoute()
const router = useRouter()
const password = ref('')
const passwordConfirm = ref('')
const error = ref('')
const handleReset = async () => {
if (password.value !== passwordConfirm.value) {
error.value = "A két jelszó nem egyezik!";
return;
}
try {
const token = route.query.token;
await axios.post(`http://192.168.100.43:8000/api/v2/auth/reset-password-confirm`, {
token: token,
new_password: password.value
});
alert("Jelszó sikeresen megváltoztatva!");
router.push('/login');
} catch (err) {
error.value = "Hiba történt. Lehetséges, hogy a link lejárt.";
}
}
</script>

View File

@@ -0,0 +1,48 @@
<template>
<div class="p-6">
<h1 class="text-2xl font-bold mb-6">Rendszer Statisztika (Admin)</h1>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="bg-blue-600 text-white p-6 rounded-lg shadow-lg">
<p class="text-blue-100 uppercase text-xs font-bold">Összes Felhasználó</p>
<p class="text-4xl font-black">1,248</p>
</div>
<div class="bg-emerald-600 text-white p-6 rounded-lg shadow-lg">
<p class="text-emerald-100 uppercase text-xs font-bold">Aktív Voucherek</p>
<p class="text-4xl font-black">42</p>
</div>
<div class="bg-amber-600 text-white p-6 rounded-lg shadow-lg">
<p class="text-amber-100 uppercase text-xs font-bold">Bot Találatok (Új)</p>
<p class="text-4xl font-black">156</p>
</div>
</div>
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="p-4 border-b bg-gray-50 flex justify-between items-center">
<span class="font-bold text-gray-700">Jóváhagyásra váró szervizek</span>
<button class="bg-blue-500 text-white px-3 py-1 rounded text-sm hover:bg-blue-600">Összes frissítése</button>
</div>
<table class="w-full text-left border-collapse">
<thead class="bg-gray-100 text-gray-600 text-xs uppercase">
<tr>
<th class="p-3">Név</th>
<th class="p-3">Város</th>
<th class="p-3">Típus</th>
<th class="p-3 text-right">Művelet</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 text-sm">
<tr v-for="i in 3" :key="i" class="hover:bg-gray-50">
<td class="p-3 font-semibold">Profi Gumiszerviz #{{i}}</td>
<td class="p-3">Budapest</td>
<td class="p-3"><span class="bg-green-100 text-green-700 px-2 py-0.5 rounded-full text-xs">Gumis</span></td>
<td class="p-3 text-right">
<button class="text-blue-600 hover:underline mr-3">Szerkeszt</button>
<button class="text-green-600 hover:underline font-bold text-lg"></button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>

11
frontend/tailwind.config.js Executable file
View File

@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

7
frontend/vite.config.js Executable file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
})