2026.03.30 front és garázs logika
This commit is contained in:
@@ -2613,9 +2613,9 @@ function publicAssetsURL(...path) {
|
||||
const APP_ROOT_OPEN_TAG = `<${appRootTag}${propsToString(appRootAttrs)}>`;
|
||||
const APP_ROOT_CLOSE_TAG = `</${appRootTag}>`;
|
||||
// @ts-expect-error file will be produced after app build
|
||||
const getServerEntry = () => import('file:///app/.nuxt//dist/server/server.mjs').then((r) => r.default || r);
|
||||
const getServerEntry = () => import('file:///app/.nuxt/dist/server/server.mjs').then((r) => r.default || r);
|
||||
// @ts-expect-error file will be produced after app build
|
||||
const getClientManifest = () => import('file:///app/.nuxt//dist/server/client.manifest.mjs').then((r) => r.default || r).then((r) => typeof r === "function" ? r() : r);
|
||||
const getClientManifest = () => import('file:///app/.nuxt/dist/server/client.manifest.mjs').then((r) => r.default || r).then((r) => typeof r === "function" ? r() : r);
|
||||
// -- SSR Renderer --
|
||||
const getSSRRenderer = lazyCachedFunction(async () => {
|
||||
// Load server bundle
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"id":"dev","timestamp":1774557833950}
|
||||
{"id":"dev","timestamp":1774810059036}
|
||||
@@ -1 +1 @@
|
||||
{"id":"dev","timestamp":1774557833950,"prerendered":[]}
|
||||
{"id":"dev","timestamp":1774810059036,"prerendered":[]}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"date": "2026-03-26T20:43:59.681Z",
|
||||
"date": "2026-03-29T18:47:42.746Z",
|
||||
"preset": "nitro-dev",
|
||||
"framework": {
|
||||
"name": "nuxt",
|
||||
@@ -11,7 +11,7 @@
|
||||
"dev": {
|
||||
"pid": 19,
|
||||
"workerAddress": {
|
||||
"socketPath": "\u0000nitro-worker-19-1-1-9144.sock"
|
||||
"socketPath": "\u0000nitro-worker-19-4-4-8465.sock"
|
||||
}
|
||||
}
|
||||
}
|
||||
6
frontend/admin/.nuxt/nuxt.d.ts
vendored
6
frontend/admin/.nuxt/nuxt.d.ts
vendored
@@ -1,8 +1,8 @@
|
||||
/// <reference types="vuetify-nuxt-module" />
|
||||
/// <reference types="@nuxtjs/i18n" />
|
||||
/// <reference types="@pinia/nuxt" />
|
||||
/// <reference types="@nuxt/telemetry" />
|
||||
/// <reference types="@nuxtjs/tailwindcss" />
|
||||
/// <reference types="@nuxtjs/i18n" />
|
||||
/// <reference types="vuetify-nuxt-module" />
|
||||
/// <reference types="@nuxt/telemetry" />
|
||||
/// <reference path="types/nitro-layouts.d.ts" />
|
||||
/// <reference path="types/builder-env.d.ts" />
|
||||
/// <reference types="nuxt" />
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"_hash": "86WsHSzrghegd85QlSfb0tmyVB8WGKoWBHcdl2r1_DE",
|
||||
"project": {
|
||||
"rootDir": "/app"
|
||||
},
|
||||
"versions": {
|
||||
"nuxt": "3.21.2"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 3/27/2026, 9:42:29 AM
|
||||
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 3/29/2026, 6:47:41 PM
|
||||
import "@nuxtjs/tailwindcss/config-ctx"
|
||||
import configMerger from "@nuxtjs/tailwindcss/merger";
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
"./imports"
|
||||
],
|
||||
"#app-manifest": [
|
||||
"./manifest/meta/dev"
|
||||
"./manifest/meta/dev.json"
|
||||
],
|
||||
"#components": [
|
||||
"./components"
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "=== Testing Epic 10 Admin Frontend Structure ==="
|
||||
echo "Date: $(date)"
|
||||
echo ""
|
||||
|
||||
# Check essential files
|
||||
echo "1. Checking essential files..."
|
||||
essential_files=(
|
||||
"package.json"
|
||||
"nuxt.config.ts"
|
||||
"tsconfig.json"
|
||||
"Dockerfile"
|
||||
"app.vue"
|
||||
"pages/dashboard.vue"
|
||||
"pages/login.vue"
|
||||
"components/TileCard.vue"
|
||||
"stores/auth.ts"
|
||||
"stores/tiles.ts"
|
||||
"composables/useRBAC.ts"
|
||||
"middleware/auth.global.ts"
|
||||
"development_log.md"
|
||||
)
|
||||
|
||||
missing_files=0
|
||||
for file in "${essential_files[@]}"; do
|
||||
if [ -f "$file" ]; then
|
||||
echo " ✓ $file"
|
||||
else
|
||||
echo " ✗ $file (MISSING)"
|
||||
((missing_files++))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "2. Checking directory structure..."
|
||||
directories=(
|
||||
"components"
|
||||
"composables"
|
||||
"middleware"
|
||||
"pages"
|
||||
"stores"
|
||||
)
|
||||
|
||||
for dir in "${directories[@]}"; do
|
||||
if [ -d "$dir" ]; then
|
||||
echo " ✓ $dir/"
|
||||
else
|
||||
echo " ✗ $dir/ (MISSING)"
|
||||
((missing_files++))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "3. Checking package.json dependencies..."
|
||||
if [ -f "package.json" ]; then
|
||||
echo " ✓ package.json exists"
|
||||
# Check for key dependencies
|
||||
if grep -q '"nuxt"' package.json; then
|
||||
echo " ✓ nuxt dependency found"
|
||||
else
|
||||
echo " ✗ nuxt dependency missing"
|
||||
((missing_files++))
|
||||
fi
|
||||
|
||||
if grep -q '"vuetify"' package.json; then
|
||||
echo " ✓ vuetify dependency found"
|
||||
else
|
||||
echo " ✗ vuetify dependency missing"
|
||||
((missing_files++))
|
||||
fi
|
||||
|
||||
if grep -q '"pinia"' package.json; then
|
||||
echo " ✓ pinia dependency found"
|
||||
else
|
||||
echo " ✗ pinia dependency missing"
|
||||
((missing_files++))
|
||||
fi
|
||||
else
|
||||
echo " ✗ package.json missing"
|
||||
((missing_files++))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "4. Checking Docker configuration..."
|
||||
if [ -f "Dockerfile" ]; then
|
||||
echo " ✓ Dockerfile exists"
|
||||
if grep -q "node:20" Dockerfile; then
|
||||
echo " ✓ Node 20 base image"
|
||||
else
|
||||
echo " ✗ Node version not specified or incorrect"
|
||||
fi
|
||||
|
||||
if grep -q "EXPOSE 3000" Dockerfile; then
|
||||
echo " ✓ Port 3000 exposed"
|
||||
else
|
||||
echo " ✗ Port not exposed"
|
||||
fi
|
||||
else
|
||||
echo " ✗ Dockerfile missing"
|
||||
((missing_files++))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Summary ==="
|
||||
if [ $missing_files -eq 0 ]; then
|
||||
echo "✅ All essential files and directories are present."
|
||||
echo "✅ Project structure is valid for Epic 10 Admin Frontend."
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Run 'npm install' to install dependencies"
|
||||
echo "2. Run 'npm run dev' to start development server"
|
||||
echo "3. Build Docker image: 'docker build -t sf-admin-frontend .'"
|
||||
echo "4. Test with docker-compose: 'docker compose up sf_admin_frontend'"
|
||||
else
|
||||
echo "⚠️ Found $missing_files missing essential items."
|
||||
echo "Please check the missing files above."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== RBAC Implementation Check ==="
|
||||
echo "The following RBAC features are implemented:"
|
||||
echo "✓ JWT token parsing with role/rank/scope extraction"
|
||||
echo "✓ Pinia auth store with permission checking"
|
||||
echo "✓ Global authentication middleware"
|
||||
echo "✓ Role-based tile filtering (7 tiles defined)"
|
||||
echo "✓ Geographical scope validation"
|
||||
echo "✓ User preference persistence"
|
||||
echo "✓ Demo login with 4 role types"
|
||||
|
||||
echo ""
|
||||
echo "=== Phase 1 & 2 Completion Status ==="
|
||||
echo "✅ Project initialization complete"
|
||||
echo "✅ Docker configuration complete"
|
||||
echo "✅ Authentication system complete"
|
||||
echo "✅ RBAC integration complete"
|
||||
echo "✅ Launchpad UI complete"
|
||||
echo "✅ Dynamic tile system complete"
|
||||
echo "✅ Development documentation complete"
|
||||
echo ""
|
||||
echo "Ready for integration testing and Phase 3 development."
|
||||
@@ -13,7 +13,12 @@ const props = defineProps({
|
||||
const statusColors = {
|
||||
'OK': 'bg-emerald-50 text-emerald-700 border border-emerald-200',
|
||||
'Service Due': 'bg-blue-50 text-blue-900 border border-blue-200 animate-pulse',
|
||||
'Warning': 'bg-rose-50 text-rose-700 border border-rose-200'
|
||||
'Warning': 'bg-rose-50 text-rose-700 border border-rose-200',
|
||||
'draft': 'bg-yellow-50 text-yellow-800 border border-yellow-300',
|
||||
'verified': 'bg-green-50 text-green-800 border border-green-300',
|
||||
'active': 'bg-blue-50 text-blue-800 border border-blue-300',
|
||||
'pending': 'bg-gray-50 text-gray-800 border border-gray-300',
|
||||
'incomplete': 'bg-orange-50 text-orange-800 border border-orange-300'
|
||||
}
|
||||
|
||||
const sortedVehicles = computed(() => {
|
||||
@@ -103,7 +108,7 @@ const getCountryFlag = (make) => {
|
||||
</th>
|
||||
<th scope="col" class="px-8 py-4 text-left text-xs font-semibold text-slate-700 uppercase tracking-wider">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2">🔧</span> Status
|
||||
<span class="mr-2">🔧</span> Data Status
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="px-8 py-4 text-left text-xs font-semibold text-slate-700 uppercase tracking-wider">
|
||||
@@ -178,15 +183,42 @@ const getCountryFlag = (make) => {
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-8 py-5 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
:class="['px-4 py-2 rounded-full text-sm font-semibold flex items-center', statusColors[vehicle.status] || 'bg-slate-100 text-slate-800 border border-slate-300']"
|
||||
>
|
||||
<span v-if="vehicle.status === 'OK'" class="w-2 h-2 bg-emerald-500 rounded-full mr-2"></span>
|
||||
<span v-else-if="vehicle.status === 'Service Due'" class="w-2 h-2 bg-blue-500 rounded-full mr-2 animate-pulse"></span>
|
||||
<span v-else class="w-2 h-2 bg-rose-500 rounded-full mr-2"></span>
|
||||
{{ vehicle.status }}
|
||||
</span>
|
||||
<div class="space-y-2">
|
||||
<!-- Data Status Badge -->
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
:class="['px-3 py-1.5 rounded-full text-xs font-semibold flex items-center', statusColors[vehicle.data_status || vehicle.status] || 'bg-slate-100 text-slate-800 border border-slate-300']"
|
||||
>
|
||||
<span
|
||||
class="w-2 h-2 rounded-full mr-2"
|
||||
:class="{
|
||||
'bg-emerald-500': (vehicle.data_status || vehicle.status) === 'OK' || (vehicle.data_status || vehicle.status) === 'verified',
|
||||
'bg-blue-500': (vehicle.data_status || vehicle.status) === 'Service Due' || (vehicle.data_status || vehicle.status) === 'active',
|
||||
'bg-rose-500': (vehicle.data_status || vehicle.status) === 'Warning',
|
||||
'bg-yellow-500': (vehicle.data_status || vehicle.status) === 'draft',
|
||||
'bg-gray-500': (vehicle.data_status || vehicle.status) === 'pending',
|
||||
'bg-orange-500': (vehicle.data_status || vehicle.status) === 'incomplete'
|
||||
}"
|
||||
></span>
|
||||
{{ vehicle.data_status || vehicle.status }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Profile Completion Mini Progress Bar -->
|
||||
<div v-if="vehicle.profile_completion_percentage !== undefined" class="flex items-center space-x-2">
|
||||
<div class="w-16 bg-gray-200 rounded-full h-1.5">
|
||||
<div
|
||||
class="h-1.5 rounded-full transition-all duration-300"
|
||||
:class="{
|
||||
'bg-yellow-500': (vehicle.profile_completion_percentage || 0) < 50,
|
||||
'bg-blue-500': (vehicle.profile_completion_percentage || 0) >= 50 && (vehicle.profile_completion_percentage || 0) < 80,
|
||||
'bg-green-500': (vehicle.profile_completion_percentage || 0) >= 80
|
||||
}"
|
||||
:style="{ width: (vehicle.profile_completion_percentage || 0) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-600 font-medium">{{ vehicle.profile_completion_percentage || 0 }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-8 py-5 whitespace-nowrap">
|
||||
|
||||
@@ -15,7 +15,11 @@ const statusColors = {
|
||||
'OK': 'bg-green-100 text-green-800',
|
||||
'Service Due': 'bg-blue-100 text-blue-900',
|
||||
'Warning': 'bg-orange-100 text-orange-800',
|
||||
'draft': 'bg-yellow-100 text-yellow-800'
|
||||
'draft': 'bg-yellow-100 text-yellow-800',
|
||||
'verified': 'bg-green-100 text-green-800',
|
||||
'active': 'bg-blue-100 text-blue-800',
|
||||
'pending': 'bg-gray-100 text-gray-800',
|
||||
'incomplete': 'bg-orange-100 text-orange-800'
|
||||
}
|
||||
|
||||
const brandLogoUrl = (make) => {
|
||||
@@ -67,9 +71,9 @@ const getCountryFlag = (make) => {
|
||||
</div>
|
||||
<div class="absolute top-3 right-3">
|
||||
<span
|
||||
:class="['px-3 py-1 rounded-full text-xs font-semibold', statusColors[vehicle.status] || 'bg-gray-100 text-gray-800']"
|
||||
:class="['px-3 py-1 rounded-full text-xs font-semibold', statusColors[vehicle.data_status || vehicle.status] || 'bg-gray-100 text-gray-800']"
|
||||
>
|
||||
{{ vehicle.status }}
|
||||
{{ vehicle.data_status || vehicle.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,18 +103,32 @@ const getCountryFlag = (make) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Draft Status & Profile Completion -->
|
||||
<div v-if="vehicle.status === 'draft'" class="mb-4">
|
||||
<!-- Profile Completion Progress Bar (shown for all vehicles with <100% completion) -->
|
||||
<div v-if="(vehicle.profile_completion_percentage || 0) < 100" class="mb-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="px-2 py-1 text-xs font-semibold rounded-full bg-yellow-100 text-yellow-800">DRAFT</span>
|
||||
<span
|
||||
v-if="vehicle.data_status === 'draft'"
|
||||
class="px-2 py-1 text-xs font-semibold rounded-full bg-yellow-100 text-yellow-800"
|
||||
>
|
||||
DRAFT
|
||||
</span>
|
||||
<div class="flex-1">
|
||||
<div class="text-xs text-gray-600 mb-1">Profile: {{ vehicle.profile_completion_percentage || 0 }}% Complete</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-yellow-500 h-2 rounded-full" :style="{ width: (vehicle.profile_completion_percentage || 0) + '%' }"></div>
|
||||
<div
|
||||
class="h-2 rounded-full transition-all duration-500"
|
||||
:class="{
|
||||
'bg-yellow-500': (vehicle.profile_completion_percentage || 0) < 50,
|
||||
'bg-blue-500': (vehicle.profile_completion_percentage || 0) >= 50 && (vehicle.profile_completion_percentage || 0) < 80,
|
||||
'bg-green-500': (vehicle.profile_completion_percentage || 0) >= 80
|
||||
}"
|
||||
:style="{ width: (vehicle.profile_completion_percentage || 0) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-1">Edit vehicle to provide VIN or Catalog ID</p>
|
||||
<p v-if="vehicle.data_status === 'draft'" class="text-xs text-gray-500 mt-1">Edit vehicle to provide VIN or Catalog ID</p>
|
||||
<p v-else class="text-xs text-gray-500 mt-1">Complete your vehicle profile for better service recommendations</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
<script setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { computed, ref, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { useGarageStore } from '@/stores/garageStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import VehicleCard from './VehicleCard.vue'
|
||||
import FleetTable from './FleetTable.vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const appModeStore = useAppModeStore()
|
||||
const garageStore = useGarageStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Animation state
|
||||
const isMounted = ref(false)
|
||||
const organizations = ref([])
|
||||
const activeOrganizationName = ref('')
|
||||
const loadingOrganizations = ref(false)
|
||||
|
||||
// Fetch vehicles on component mount (simulated)
|
||||
onMounted(() => {
|
||||
@@ -21,8 +27,53 @@ onMounted(() => {
|
||||
setTimeout(() => {
|
||||
isMounted.value = true
|
||||
}, 100)
|
||||
|
||||
// Fetch organizations if in fleet mode
|
||||
if (appModeStore.isCorporateFleet) {
|
||||
fetchOrganizations()
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for mode changes
|
||||
watch(() => appModeStore.mode, (newMode) => {
|
||||
if (newMode === 'fleet') {
|
||||
fetchOrganizations()
|
||||
} else {
|
||||
activeOrganizationName.value = ''
|
||||
}
|
||||
})
|
||||
|
||||
// Fetch user's organizations
|
||||
const fetchOrganizations = async () => {
|
||||
if (!authStore.token) return
|
||||
|
||||
try {
|
||||
loadingOrganizations.value = true
|
||||
const response = await api.get('/organizations/my')
|
||||
organizations.value = response.data || []
|
||||
|
||||
// Find active organization name
|
||||
const activeOrgId = authStore.activeOrgId || authStore.userProfile?.active_organization_id
|
||||
if (activeOrgId && organizations.value.length > 0) {
|
||||
// Try to find organization by ID
|
||||
// Note: The current endpoint returns limited data, we may need to enhance it
|
||||
const org = organizations.value.find(o => o.organization_id === parseInt(activeOrgId))
|
||||
if (org) {
|
||||
activeOrganizationName.value = org.display_name || org.name || org.full_name || `Organization #${activeOrgId}`
|
||||
} else {
|
||||
activeOrganizationName.value = 'Corporate Fleet'
|
||||
}
|
||||
} else {
|
||||
activeOrganizationName.value = 'Corporate Fleet'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch organizations:', error)
|
||||
activeOrganizationName.value = 'Corporate Fleet'
|
||||
} finally {
|
||||
loadingOrganizations.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const stats = computed(() => ({
|
||||
totalVehicles: garageStore.totalVehicles,
|
||||
totalMonthlyExpense: garageStore.totalMonthlyExpense,
|
||||
@@ -36,6 +87,30 @@ const formatCurrency = (amount) => {
|
||||
minimumFractionDigits: 0
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
// Computed for header title
|
||||
const headerTitle = computed(() => {
|
||||
if (appModeStore.isPrivateGarage) {
|
||||
return 'Saját Garázs'
|
||||
} else {
|
||||
if (activeOrganizationName.value && activeOrganizationName.value !== 'Corporate Fleet') {
|
||||
return `Céges Flotta: ${activeOrganizationName.value}`
|
||||
}
|
||||
return 'Céges Flotta'
|
||||
}
|
||||
})
|
||||
|
||||
// Computed for header description
|
||||
const headerDescription = computed(() => {
|
||||
if (appModeStore.isPrivateGarage) {
|
||||
return 'Your personal vehicle collection and maintenance tracker'
|
||||
} else {
|
||||
if (activeOrganizationName.value && activeOrganizationName.value !== 'Corporate Fleet') {
|
||||
return `Company-wide vehicle management and cost analytics for ${activeOrganizationName.value}`
|
||||
}
|
||||
return 'Company-wide vehicle management and cost analytics'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -44,18 +119,29 @@ const formatCurrency = (amount) => {
|
||||
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-2xl p-6 border border-blue-100">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">
|
||||
{{ appModeStore.isPrivateGarage ? 'My Garage' : 'Corporate Fleet' }}
|
||||
</h1>
|
||||
<div class="flex items-center gap-3">
|
||||
<h1 class="text-3xl font-bold text-gray-900">
|
||||
{{ headerTitle }}
|
||||
</h1>
|
||||
<span
|
||||
v-if="appModeStore.isPrivateGarage"
|
||||
class="px-3 py-1 bg-gradient-to-r from-green-500 to-emerald-600 text-white text-xs font-semibold rounded-full shadow-sm"
|
||||
>
|
||||
B2C
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="px-3 py-1 bg-gradient-to-r from-blue-500 to-indigo-600 text-white text-xs font-semibold rounded-full shadow-sm"
|
||||
>
|
||||
B2B
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-gray-600 mt-2">
|
||||
{{ appModeStore.isPrivateGarage
|
||||
? 'Your personal vehicle collection and maintenance tracker'
|
||||
: 'Company-wide vehicle management and cost analytics'
|
||||
}}
|
||||
{{ headerDescription }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button
|
||||
<button
|
||||
@click="appModeStore.toggleMode"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-lg font-medium text-gray-700 hover:bg-gray-50 transition-all duration-300 flex items-center active:scale-95"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import api from '@/services/api'
|
||||
import { useGarageStore } from './garageStore'
|
||||
|
||||
export const useAppModeStore = defineStore('appMode', () => {
|
||||
// State
|
||||
@@ -80,6 +81,37 @@ export const useAppModeStore = defineStore('appMode', () => {
|
||||
await api.patch('/users/me/preferences', {
|
||||
ui_mode: newMode
|
||||
})
|
||||
|
||||
// Also update active organization based on mode
|
||||
// When switching to fleet mode, we need to set an active organization
|
||||
// When switching to personal mode, we should clear the active organization
|
||||
if (newMode === 'fleet') {
|
||||
// Try to get the user's active organization from auth store
|
||||
// For now, we'll just make sure the backend knows we're in fleet mode
|
||||
// The actual organization selection should happen elsewhere
|
||||
console.log('Switched to fleet mode - organization selection may be needed')
|
||||
} else if (newMode === 'personal') {
|
||||
// Clear active organization when switching to personal mode
|
||||
try {
|
||||
await api.patch('/users/me/active-organization', {
|
||||
organization_id: null
|
||||
})
|
||||
console.log('Cleared active organization for personal mode')
|
||||
} catch (orgError) {
|
||||
console.warn('Failed to clear active organization', orgError)
|
||||
// Non-critical error, continue
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh the garage store to reflect the new scope
|
||||
try {
|
||||
const garageStore = useGarageStore()
|
||||
await garageStore.fetchVehicles()
|
||||
console.log('Garage store refreshed after mode change')
|
||||
} catch (refreshError) {
|
||||
console.warn('Failed to refresh garage store', refreshError)
|
||||
// Non-critical error, continue
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save UI mode to backend', error)
|
||||
// Optionally revert local state? For now just log.
|
||||
|
||||
@@ -188,6 +188,47 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Update active organization
|
||||
const updateActiveOrganization = async (organizationId) => {
|
||||
if (!token.value) {
|
||||
throw new Error('Not authenticated')
|
||||
}
|
||||
|
||||
try {
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu';
|
||||
const response = await fetch(`${apiBaseUrl}/users/me/active-organization`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token.value}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
organization_id: organizationId
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to update active organization: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log('AuthStore: Updated active organization', data)
|
||||
|
||||
// Update local state
|
||||
activeOrgId.value = organizationId
|
||||
localStorage.setItem('active_org_id', organizationId || '')
|
||||
|
||||
// Also update user profile to reflect changes
|
||||
await fetchUserProfile()
|
||||
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('AuthStore: Error updating active organization', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
const checkAuth = () => {
|
||||
// Sync with localStorage on page load
|
||||
const storedToken = localStorage.getItem('token') || ''
|
||||
@@ -236,6 +277,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
userEmail,
|
||||
userRole,
|
||||
userProfile,
|
||||
activeOrgId,
|
||||
|
||||
// Getters
|
||||
isLoggedIn,
|
||||
@@ -246,6 +288,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
login,
|
||||
logout,
|
||||
checkAuth,
|
||||
fetchUserProfile
|
||||
fetchUserProfile,
|
||||
updateActiveOrganization
|
||||
}
|
||||
})
|
||||
@@ -201,6 +201,9 @@ export const useGarageStore = defineStore('garage', () => {
|
||||
item.profile_completion ||
|
||||
(item.profile_status === 'complete' ? 100 : 0) || 0
|
||||
|
||||
// Extract data_status from API (new field for vehicle data completeness)
|
||||
const dataStatus = item.data_status || item.status || (item.is_active ? 'active' : 'draft')
|
||||
|
||||
return {
|
||||
id: item.id || item.asset_id || 0,
|
||||
make: item.make || item.brand || item.vehicle_make || catalogMake || 'Unknown',
|
||||
@@ -208,6 +211,7 @@ export const useGarageStore = defineStore('garage', () => {
|
||||
year: item.year || item.year_of_manufacture || item.manufacture_year || catalogYear || 2023,
|
||||
licensePlate: item.license_plate || item.registration_number || 'N/A',
|
||||
status: item.status || (item.is_active ? 'active' : 'draft'),
|
||||
data_status: dataStatus,
|
||||
profile_completion_percentage: profileCompletion,
|
||||
monthlyExpense: item.monthly_expense || item.average_monthly_cost || 0,
|
||||
fuelType: item.fuel_type || item.fuel || catalogFuelType || 'Unknown',
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"failedTests": []
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Automated E2E Flow Test for Service Finder Frontend
|
||||
*
|
||||
* This script simulates:
|
||||
* 1. Logging in and getting a token
|
||||
* 2. Setting the Profile Mode (Personal/Fleet)
|
||||
* 3. Fetching the User's Garage
|
||||
* 4. Fetching Gamification stats
|
||||
*
|
||||
* Usage: node automated_flow_test.js
|
||||
*/
|
||||
|
||||
import axios from 'axios'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { dirname } from 'path'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
// Configuration
|
||||
const API_BASE_URL = process.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'
|
||||
const TEST_USER_EMAIL = process.env.TEST_USER_EMAIL || 'test@example.com'
|
||||
const TEST_USER_PASSWORD = process.env.TEST_USER_PASSWORD || 'password123'
|
||||
|
||||
// Create axios instance
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
// Test state
|
||||
let authToken = null
|
||||
let userId = null
|
||||
|
||||
async function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
async function testLogin() {
|
||||
console.log('🔐 Testing login...')
|
||||
|
||||
try {
|
||||
// First, check if we need to register a test user
|
||||
// For now, we'll try to login with provided credentials
|
||||
const response = await api.post('/api/v2/auth/login', {
|
||||
email: TEST_USER_EMAIL,
|
||||
password: TEST_USER_PASSWORD,
|
||||
})
|
||||
|
||||
if (response.data.access_token) {
|
||||
authToken = response.data.access_token
|
||||
userId = response.data.user_id
|
||||
console.log('✅ Login successful')
|
||||
console.log(` Token: ${authToken.substring(0, 20)}...`)
|
||||
console.log(` User ID: ${userId}`)
|
||||
|
||||
// Set auth header for subsequent requests
|
||||
api.defaults.headers.common['Authorization'] = `Bearer ${authToken}`
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Login failed:', error.response?.data || error.message)
|
||||
|
||||
// If login fails due to invalid credentials, try to register
|
||||
if (error.response?.status === 401 || error.response?.status === 404) {
|
||||
console.log('⚠️ Attempting to register test user...')
|
||||
try {
|
||||
const registerResponse = await api.post('/api/v2/auth/register', null, {
|
||||
params: {
|
||||
email: TEST_USER_EMAIL,
|
||||
password: TEST_USER_PASSWORD,
|
||||
first_name: 'Test',
|
||||
last_name: 'User',
|
||||
phone: '+36123456789',
|
||||
}
|
||||
})
|
||||
|
||||
if (registerResponse.data.access_token) {
|
||||
authToken = registerResponse.data.access_token
|
||||
userId = registerResponse.data.user_id
|
||||
console.log('✅ Test user registered and logged in')
|
||||
api.defaults.headers.common['Authorization'] = `Bearer ${authToken}`
|
||||
return true
|
||||
}
|
||||
} catch (registerError) {
|
||||
console.error('❌ Registration failed:', registerError.response?.data || registerError.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async function testSetProfileMode() {
|
||||
console.log('\n🎯 Testing profile mode setting...')
|
||||
|
||||
try {
|
||||
// First, get current user to check existing mode
|
||||
const userResponse = await api.get('/api/v1/users/me')
|
||||
console.log(` Current UI mode: ${userResponse.data.ui_mode || 'not set'}`)
|
||||
|
||||
// Set mode to 'personal' (private_garage)
|
||||
const modeToSet = 'personal'
|
||||
const response = await api.patch('/api/v1/users/me/preferences', {
|
||||
ui_mode: modeToSet
|
||||
})
|
||||
|
||||
console.log(`✅ Profile mode set to: ${modeToSet}`)
|
||||
|
||||
// Verify the mode was set
|
||||
const verifyResponse = await api.get('/api/v1/users/me')
|
||||
if (verifyResponse.data.ui_mode === modeToSet) {
|
||||
console.log(`✅ Mode verified: ${verifyResponse.data.ui_mode}`)
|
||||
return true
|
||||
} else {
|
||||
console.error(`❌ Mode mismatch: expected ${modeToSet}, got ${verifyResponse.data.ui_mode}`)
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to set profile mode:', error.response?.data || error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function testFetchGarage() {
|
||||
console.log('\n🚗 Testing garage fetch...')
|
||||
|
||||
try {
|
||||
const response = await api.get('/api/v1/vehicles/my-garage')
|
||||
|
||||
if (Array.isArray(response.data)) {
|
||||
console.log(`✅ Garage fetched successfully: ${response.data.length} vehicle(s)`)
|
||||
if (response.data.length > 0) {
|
||||
console.log(' Sample vehicle:', {
|
||||
id: response.data[0].id,
|
||||
make: response.data[0].make,
|
||||
model: response.data[0].model,
|
||||
license_plate: response.data[0].license_plate,
|
||||
})
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
console.error('❌ Unexpected garage response format:', response.data)
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
// Garage might be empty (404) or other error
|
||||
if (error.response?.status === 404) {
|
||||
console.log('✅ Garage is empty (expected for new user)')
|
||||
return true
|
||||
}
|
||||
console.error('❌ Failed to fetch garage:', error.response?.data || error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function testFetchGamification() {
|
||||
console.log('\n🏆 Testing gamification fetch...')
|
||||
|
||||
try {
|
||||
// Test achievements endpoint
|
||||
const achievementsResponse = await api.get('/api/v1/gamification/achievements')
|
||||
console.log(`✅ Achievements fetched: ${achievementsResponse.data.achievements?.length || 0} total`)
|
||||
|
||||
// Test user stats
|
||||
const statsResponse = await api.get('/api/v1/gamification/me')
|
||||
console.log('✅ User stats fetched:', {
|
||||
xp: statsResponse.data.xp,
|
||||
level: statsResponse.data.level,
|
||||
rank: statsResponse.data.rank,
|
||||
})
|
||||
|
||||
// Test badges
|
||||
const badgesResponse = await api.get('/api/v1/gamification/my-badges')
|
||||
console.log(`✅ Badges fetched: ${badgesResponse.data?.length || 0} earned`)
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
// Gamification might not be fully implemented
|
||||
if (error.response?.status === 404 || error.response?.status === 501) {
|
||||
console.log('⚠️ Gamification endpoints not fully implemented (expected during development)')
|
||||
return true
|
||||
}
|
||||
console.error('❌ Failed to fetch gamification:', error.response?.data || error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function runAllTests() {
|
||||
console.log('🚀 Starting Service Finder E2E Flow Test')
|
||||
console.log('=========================================')
|
||||
console.log(`API Base URL: ${API_BASE_URL}`)
|
||||
console.log(`Test User: ${TEST_USER_EMAIL}`)
|
||||
console.log('')
|
||||
|
||||
const results = {
|
||||
login: false,
|
||||
profileMode: false,
|
||||
garage: false,
|
||||
gamification: false,
|
||||
}
|
||||
|
||||
// Run tests sequentially
|
||||
results.login = await testLogin()
|
||||
if (!results.login) {
|
||||
console.error('\n❌ Login failed, aborting further tests')
|
||||
return results
|
||||
}
|
||||
|
||||
await sleep(1000) // Small delay between tests
|
||||
|
||||
results.profileMode = await testSetProfileMode()
|
||||
|
||||
await sleep(500)
|
||||
|
||||
results.garage = await testFetchGarage()
|
||||
|
||||
await sleep(500)
|
||||
|
||||
results.gamification = await testFetchGamification()
|
||||
|
||||
// Summary
|
||||
console.log('\n📊 Test Summary')
|
||||
console.log('===============')
|
||||
console.log(`Login: ${results.login ? '✅ PASS' : '❌ FAIL'}`)
|
||||
console.log(`Profile Mode: ${results.profileMode ? '✅ PASS' : '❌ FAIL'}`)
|
||||
console.log(`Garage Fetch: ${results.garage ? '✅ PASS' : '❌ FAIL'}`)
|
||||
console.log(`Gamification: ${results.gamification ? '✅ PASS' : '❌ FAIL'}`)
|
||||
|
||||
const allPassed = Object.values(results).every(r => r)
|
||||
console.log(`\n${allPassed ? '🎉 ALL TESTS PASSED' : '⚠️ SOME TESTS FAILED'}`)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// Run tests if script is executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
runAllTests().then(results => {
|
||||
const allPassed = Object.values(results).every(r => r)
|
||||
process.exit(allPassed ? 0 : 1)
|
||||
}).catch(error => {
|
||||
console.error('💥 Unhandled error in test runner:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
||||
export { runAllTests }
|
||||
Reference in New Issue
Block a user