2026.06.04 frontend építés közben

This commit is contained in:
Roo
2026-06-04 07:26:22 +00:00
parent 7adf6cc3e3
commit 59a30ac428
3302 changed files with 24091 additions and 1771 deletions

122
frontend/.gitignore vendored Executable file → Normal file
View File

@@ -7,18 +7,126 @@ yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Vite
dist
dist-ssr
*.local
# Editor directories and files
# Editor
.vscode/*
!.vscode/extensions.json
.idea
*.swp
*.swo
# Environment variables
.env
.env.local
.env.*.local
# macOS
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Docker
docker-compose.override.yml

View File

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

37
frontend/Dockerfile Executable file → Normal file
View File

@@ -1,22 +1,31 @@
# 1. szakasz: Build (lefordítja a kódot)
FROM node:20-slim as build-stage
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy source code
COPY . .
# Build the application
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
# Production stage
FROM nginx:alpine
# Copy nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Copy built assets from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Expose port 80
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

133
frontend/README.md Executable file → Normal file
View File

@@ -1,5 +1,132 @@
# Vue 3 + Vite
# Service Finder Frontend
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.
Modern Vue 3 frontend for the Service Finder platform with dual layout system (B2B/B2C), internationalization, and Docker deployment.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
## 🚀 Tech Stack
- **Vue 3** (Composition API)
- **TypeScript** for type safety
- **Vite** for fast development and builds
- **Pinia** for state management
- **Vue Router** for navigation
- **Vue I18n** for translations
- **Tailwind CSS** for styling
- **Axios** for HTTP requests
- **Docker** (multi-stage) + **Nginx** for production
## 📁 Project Structure
```
frontend/
├── src/
│ ├── assets/ # Styles, images, fonts
│ ├── components/ # Reusable Vue components
│ ├── layouts/ # Layout components (Corporate, Consumer)
│ ├── router/ # Vue Router configuration
│ ├── stores/ # Pinia stores (app mode, translations)
│ ├── services/ # API services (translation, auth)
│ ├── views/ # Page components (Home, About)
│ ├── locales/ # Translation files (future)
│ ├── App.vue # Root component
│ └── main.ts # Application entry point
├── public/ # Static assets
├── Dockerfile # Multi-stage Docker build
├── nginx.conf # Nginx configuration with API proxy
├── vite.config.ts # Vite configuration
├── tailwind.config.js # Tailwind CSS configuration
├── tsconfig.json # TypeScript configuration
└── package.json # Dependencies and scripts
```
## 🎯 Features
### 1. **Dual Layout System**
- **CorporateLayout**: Clean, data-oriented design for B2B users (fleet management)
- **ConsumerLayout**: Playful, bento-grid design for B2C users (individual vehicle owners)
- Dynamic switching via Pinia store
### 2. **Internationalization (i18n)**
- Integrated vue-i18n with backend translation API
- Language switcher component
- Fallback to English
- Translation loading from `/api/v1/translations`
### 3. **App Mode Store (Pinia)**
- Centralized state for app mode (B2B/B2C) and language
- Persistent storage in localStorage
- Reactive computed properties
### 4. **Docker Deployment**
- Multi-stage build (Node 20 + Nginx Alpine)
- Nginx reverse proxy to backend API (`sf_api:8000`)
- Health check endpoint
- Optimized production image (~50MB)
### 5. **Development Proxy**
- Vite dev server proxies `/api` requests to backend
- Hot module replacement (HMR)
- TypeScript support
## 🐳 Docker Commands
### Build the image
```bash
docker build -t service-finder-frontend .
```
### Run locally (with network)
```bash
docker run -p 8080:80 --network sf_net service-finder-frontend
```
### Development (with hot reload)
```bash
cd frontend
npm install
npm run dev
```
## 🔧 Configuration
### Environment Variables
Create `.env` file:
```env
VITE_API_BASE=/api
```
### Nginx Proxy
All `/api/*` requests are proxied to `http://sf_api:8000` (internal Docker network).
### Tailwind CSS
Custom colors for corporate (`corporate-blue`) and consumer (`consumer-orange`, `consumer-teal`, `consumer-purple`) themes.
## 📦 Scripts
- `npm run dev` Start development server
- `npm run build` Build for production
- `npm run preview` Preview production build
- `npm run lint` Lint code with ESLint
## 🔗 Backend Integration
The frontend expects the following backend endpoints:
- `GET /api/v1/translations?lang={code}` Fetch translations
- `GET /api/v1/translations/all` Fetch all translations
- `POST /api/v1/translations` Update translations (admin)
## 🎨 Design System
### Corporate Theme
- Colors: Blue (#1e3a8a), Gray (#6b7280), Light (#f9fafb)
- Font: Inter (sans-serif)
- Layout: Sidebar navigation, data tables, analytics
### Consumer Theme
- Colors: Orange (#f97316), Teal (#0d9488), Purple (#7c3aed)
- Font: Inter (sans-serif) with playful elements
- Layout: Bento grid, cards, gradient backgrounds
## 📄 License
Proprietary Service Finder Platform

View File

@@ -1,18 +0,0 @@
import { _replaceAppConfig } from '#app/config'
import { defuFn } from 'defu'
const inlineConfig = {
"nuxt": {}
}
// Vite - webpack is handled directly in #app/config
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
_replaceAppConfig(newModule.default)
})
}
export default /*@__PURE__*/ defuFn(inlineConfig)

View File

@@ -1,434 +0,0 @@
import type { DefineComponent, SlotsType } from 'vue'
type IslandComponent<T> = DefineComponent<{}, {refresh: () => Promise<void>}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, SlotsType<{ fallback: { error: unknown } }>> & T
type HydrationStrategies = {
hydrateOnVisible?: IntersectionObserverInit | true
hydrateOnIdle?: number | true
hydrateOnInteraction?: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap> | true
hydrateOnMediaQuery?: string
hydrateAfter?: number
hydrateWhen?: boolean
hydrateNever?: true
}
type LazyComponent<T> = DefineComponent<HydrationStrategies, {}, {}, {}, {}, {}, {}, { hydrated: () => void }> & T
export const AiLogsTile: typeof import("../components/AiLogsTile.vue")['default']
export const FinancialTile: typeof import("../components/FinancialTile.vue")['default']
export const SalespersonTile: typeof import("../components/SalespersonTile.vue")['default']
export const ServiceMapTile: typeof import("../components/ServiceMapTile.vue")['default']
export const SystemHealthTile: typeof import("../components/SystemHealthTile.vue")['default']
export const TileCard: typeof import("../components/TileCard.vue")['default']
export const TileWrapper: typeof import("../components/TileWrapper.vue")['default']
export const MapServiceMap: typeof import("../components/map/ServiceMap.vue")['default']
export const NuxtWelcome: typeof import("../node_modules/nuxt/dist/app/components/welcome.vue")['default']
export const NuxtLayout: typeof import("../node_modules/nuxt/dist/app/components/nuxt-layout")['default']
export const NuxtErrorBoundary: typeof import("../node_modules/nuxt/dist/app/components/nuxt-error-boundary.vue")['default']
export const ClientOnly: typeof import("../node_modules/nuxt/dist/app/components/client-only")['default']
export const DevOnly: typeof import("../node_modules/nuxt/dist/app/components/dev-only")['default']
export const ServerPlaceholder: typeof import("../node_modules/nuxt/dist/app/components/server-placeholder")['default']
export const NuxtLink: typeof import("../node_modules/nuxt/dist/app/components/nuxt-link")['default']
export const NuxtLoadingIndicator: typeof import("../node_modules/nuxt/dist/app/components/nuxt-loading-indicator")['default']
export const NuxtTime: typeof import("../node_modules/nuxt/dist/app/components/nuxt-time.vue")['default']
export const NuxtRouteAnnouncer: typeof import("../node_modules/nuxt/dist/app/components/nuxt-route-announcer")['default']
export const NuxtImg: typeof import("../node_modules/nuxt/dist/app/components/nuxt-stubs")['NuxtImg']
export const NuxtPicture: typeof import("../node_modules/nuxt/dist/app/components/nuxt-stubs")['NuxtPicture']
export const VAvatar: typeof import("vuetify/components")['VAvatar']
export const VBanner: typeof import("vuetify/components")['VBanner']
export const VBannerActions: typeof import("vuetify/components")['VBannerActions']
export const VBannerText: typeof import("vuetify/components")['VBannerText']
export const VApp: typeof import("vuetify/components")['VApp']
export const VAppBar: typeof import("vuetify/components")['VAppBar']
export const VAppBarNavIcon: typeof import("vuetify/components")['VAppBarNavIcon']
export const VAppBarTitle: typeof import("vuetify/components")['VAppBarTitle']
export const VCalendar: typeof import("vuetify/components")['VCalendar']
export const VAlert: typeof import("vuetify/components")['VAlert']
export const VAlertTitle: typeof import("vuetify/components")['VAlertTitle']
export const VBtnToggle: typeof import("vuetify/components")['VBtnToggle']
export const VBreadcrumbs: typeof import("vuetify/components")['VBreadcrumbs']
export const VBreadcrumbsItem: typeof import("vuetify/components")['VBreadcrumbsItem']
export const VBreadcrumbsDivider: typeof import("vuetify/components")['VBreadcrumbsDivider']
export const VBtnGroup: typeof import("vuetify/components")['VBtnGroup']
export const VBtn: typeof import("vuetify/components")['VBtn']
export const VBadge: typeof import("vuetify/components")['VBadge']
export const VBottomNavigation: typeof import("vuetify/components")['VBottomNavigation']
export const VCheckbox: typeof import("vuetify/components")['VCheckbox']
export const VCheckboxBtn: typeof import("vuetify/components")['VCheckboxBtn']
export const VCarousel: typeof import("vuetify/components")['VCarousel']
export const VCarouselItem: typeof import("vuetify/components")['VCarouselItem']
export const VChip: typeof import("vuetify/components")['VChip']
export const VCard: typeof import("vuetify/components")['VCard']
export const VCardActions: typeof import("vuetify/components")['VCardActions']
export const VCardItem: typeof import("vuetify/components")['VCardItem']
export const VCardSubtitle: typeof import("vuetify/components")['VCardSubtitle']
export const VCardText: typeof import("vuetify/components")['VCardText']
export const VCardTitle: typeof import("vuetify/components")['VCardTitle']
export const VBottomSheet: typeof import("vuetify/components")['VBottomSheet']
export const VChipGroup: typeof import("vuetify/components")['VChipGroup']
export const VColorPicker: typeof import("vuetify/components")['VColorPicker']
export const VCombobox: typeof import("vuetify/components")['VCombobox']
export const VCode: typeof import("vuetify/components")['VCode']
export const VCounter: typeof import("vuetify/components")['VCounter']
export const VDatePicker: typeof import("vuetify/components")['VDatePicker']
export const VDatePickerControls: typeof import("vuetify/components")['VDatePickerControls']
export const VDatePickerHeader: typeof import("vuetify/components")['VDatePickerHeader']
export const VDatePickerMonth: typeof import("vuetify/components")['VDatePickerMonth']
export const VDatePickerMonths: typeof import("vuetify/components")['VDatePickerMonths']
export const VDatePickerYears: typeof import("vuetify/components")['VDatePickerYears']
export const VDialog: typeof import("vuetify/components")['VDialog']
export const VDivider: typeof import("vuetify/components")['VDivider']
export const VFab: typeof import("vuetify/components")['VFab']
export const VField: typeof import("vuetify/components")['VField']
export const VFieldLabel: typeof import("vuetify/components")['VFieldLabel']
export const VEmptyState: typeof import("vuetify/components")['VEmptyState']
export const VExpansionPanels: typeof import("vuetify/components")['VExpansionPanels']
export const VExpansionPanel: typeof import("vuetify/components")['VExpansionPanel']
export const VExpansionPanelText: typeof import("vuetify/components")['VExpansionPanelText']
export const VExpansionPanelTitle: typeof import("vuetify/components")['VExpansionPanelTitle']
export const VDataTable: typeof import("vuetify/components")['VDataTable']
export const VDataTableHeaders: typeof import("vuetify/components")['VDataTableHeaders']
export const VDataTableFooter: typeof import("vuetify/components")['VDataTableFooter']
export const VDataTableRows: typeof import("vuetify/components")['VDataTableRows']
export const VDataTableRow: typeof import("vuetify/components")['VDataTableRow']
export const VDataTableVirtual: typeof import("vuetify/components")['VDataTableVirtual']
export const VDataTableServer: typeof import("vuetify/components")['VDataTableServer']
export const VHotkey: typeof import("vuetify/components")['VHotkey']
export const VFileInput: typeof import("vuetify/components")['VFileInput']
export const VFooter: typeof import("vuetify/components")['VFooter']
export const VImg: typeof import("vuetify/components")['VImg']
export const VItemGroup: typeof import("vuetify/components")['VItemGroup']
export const VItem: typeof import("vuetify/components")['VItem']
export const VIcon: typeof import("vuetify/components")['VIcon']
export const VComponentIcon: typeof import("vuetify/components")['VComponentIcon']
export const VSvgIcon: typeof import("vuetify/components")['VSvgIcon']
export const VLigatureIcon: typeof import("vuetify/components")['VLigatureIcon']
export const VClassIcon: typeof import("vuetify/components")['VClassIcon']
export const VInput: typeof import("vuetify/components")['VInput']
export const VInfiniteScroll: typeof import("vuetify/components")['VInfiniteScroll']
export const VKbd: typeof import("vuetify/components")['VKbd']
export const VMenu: typeof import("vuetify/components")['VMenu']
export const VNavigationDrawer: typeof import("vuetify/components")['VNavigationDrawer']
export const VLabel: typeof import("vuetify/components")['VLabel']
export const VMain: typeof import("vuetify/components")['VMain']
export const VMessages: typeof import("vuetify/components")['VMessages']
export const VOverlay: typeof import("vuetify/components")['VOverlay']
export const VList: typeof import("vuetify/components")['VList']
export const VListGroup: typeof import("vuetify/components")['VListGroup']
export const VListImg: typeof import("vuetify/components")['VListImg']
export const VListItem: typeof import("vuetify/components")['VListItem']
export const VListItemAction: typeof import("vuetify/components")['VListItemAction']
export const VListItemMedia: typeof import("vuetify/components")['VListItemMedia']
export const VListItemSubtitle: typeof import("vuetify/components")['VListItemSubtitle']
export const VListItemTitle: typeof import("vuetify/components")['VListItemTitle']
export const VListSubheader: typeof import("vuetify/components")['VListSubheader']
export const VPagination: typeof import("vuetify/components")['VPagination']
export const VNumberInput: typeof import("vuetify/components")['VNumberInput']
export const VProgressLinear: typeof import("vuetify/components")['VProgressLinear']
export const VOtpInput: typeof import("vuetify/components")['VOtpInput']
export const VRadioGroup: typeof import("vuetify/components")['VRadioGroup']
export const VSelectionControl: typeof import("vuetify/components")['VSelectionControl']
export const VProgressCircular: typeof import("vuetify/components")['VProgressCircular']
export const VSelect: typeof import("vuetify/components")['VSelect']
export const VSheet: typeof import("vuetify/components")['VSheet']
export const VSelectionControlGroup: typeof import("vuetify/components")['VSelectionControlGroup']
export const VSlideGroup: typeof import("vuetify/components")['VSlideGroup']
export const VSlideGroupItem: typeof import("vuetify/components")['VSlideGroupItem']
export const VSkeletonLoader: typeof import("vuetify/components")['VSkeletonLoader']
export const VRating: typeof import("vuetify/components")['VRating']
export const VSnackbar: typeof import("vuetify/components")['VSnackbar']
export const VTextarea: typeof import("vuetify/components")['VTextarea']
export const VSystemBar: typeof import("vuetify/components")['VSystemBar']
export const VSwitch: typeof import("vuetify/components")['VSwitch']
export const VStepper: typeof import("vuetify/components")['VStepper']
export const VStepperActions: typeof import("vuetify/components")['VStepperActions']
export const VStepperHeader: typeof import("vuetify/components")['VStepperHeader']
export const VStepperItem: typeof import("vuetify/components")['VStepperItem']
export const VStepperWindow: typeof import("vuetify/components")['VStepperWindow']
export const VStepperWindowItem: typeof import("vuetify/components")['VStepperWindowItem']
export const VSlider: typeof import("vuetify/components")['VSlider']
export const VTab: typeof import("vuetify/components")['VTab']
export const VTabs: typeof import("vuetify/components")['VTabs']
export const VTabsWindow: typeof import("vuetify/components")['VTabsWindow']
export const VTabsWindowItem: typeof import("vuetify/components")['VTabsWindowItem']
export const VTable: typeof import("vuetify/components")['VTable']
export const VTimeline: typeof import("vuetify/components")['VTimeline']
export const VTimelineItem: typeof import("vuetify/components")['VTimelineItem']
export const VTextField: typeof import("vuetify/components")['VTextField']
export const VTooltip: typeof import("vuetify/components")['VTooltip']
export const VToolbar: typeof import("vuetify/components")['VToolbar']
export const VToolbarTitle: typeof import("vuetify/components")['VToolbarTitle']
export const VToolbarItems: typeof import("vuetify/components")['VToolbarItems']
export const VWindow: typeof import("vuetify/components")['VWindow']
export const VWindowItem: typeof import("vuetify/components")['VWindowItem']
export const VTimePicker: typeof import("vuetify/components")['VTimePicker']
export const VTimePickerClock: typeof import("vuetify/components")['VTimePickerClock']
export const VTimePickerControls: typeof import("vuetify/components")['VTimePickerControls']
export const VTreeview: typeof import("vuetify/components")['VTreeview']
export const VTreeviewItem: typeof import("vuetify/components")['VTreeviewItem']
export const VTreeviewGroup: typeof import("vuetify/components")['VTreeviewGroup']
export const VConfirmEdit: typeof import("vuetify/components")['VConfirmEdit']
export const VDataIterator: typeof import("vuetify/components")['VDataIterator']
export const VDefaultsProvider: typeof import("vuetify/components")['VDefaultsProvider']
export const VContainer: typeof import("vuetify/components")['VContainer']
export const VCol: typeof import("vuetify/components")['VCol']
export const VRow: typeof import("vuetify/components")['VRow']
export const VSpacer: typeof import("vuetify/components")['VSpacer']
export const VForm: typeof import("vuetify/components")['VForm']
export const VAutocomplete: typeof import("vuetify/components")['VAutocomplete']
export const VHover: typeof import("vuetify/components")['VHover']
export const VLazy: typeof import("vuetify/components")['VLazy']
export const VLayout: typeof import("vuetify/components")['VLayout']
export const VLayoutItem: typeof import("vuetify/components")['VLayoutItem']
export const VLocaleProvider: typeof import("vuetify/components")['VLocaleProvider']
export const VRadio: typeof import("vuetify/components")['VRadio']
export const VParallax: typeof import("vuetify/components")['VParallax']
export const VNoSsr: typeof import("vuetify/components")['VNoSsr']
export const VRangeSlider: typeof import("vuetify/components")['VRangeSlider']
export const VResponsive: typeof import("vuetify/components")['VResponsive']
export const VSnackbarQueue: typeof import("vuetify/components")['VSnackbarQueue']
export const VSpeedDial: typeof import("vuetify/components")['VSpeedDial']
export const VSparkline: typeof import("vuetify/components")['VSparkline']
export const VVirtualScroll: typeof import("vuetify/components")['VVirtualScroll']
export const VThemeProvider: typeof import("vuetify/components")['VThemeProvider']
export const VFabTransition: typeof import("vuetify/components")['VFabTransition']
export const VDialogBottomTransition: typeof import("vuetify/components")['VDialogBottomTransition']
export const VDialogTopTransition: typeof import("vuetify/components")['VDialogTopTransition']
export const VFadeTransition: typeof import("vuetify/components")['VFadeTransition']
export const VScaleTransition: typeof import("vuetify/components")['VScaleTransition']
export const VScrollXTransition: typeof import("vuetify/components")['VScrollXTransition']
export const VScrollXReverseTransition: typeof import("vuetify/components")['VScrollXReverseTransition']
export const VScrollYTransition: typeof import("vuetify/components")['VScrollYTransition']
export const VScrollYReverseTransition: typeof import("vuetify/components")['VScrollYReverseTransition']
export const VSlideXTransition: typeof import("vuetify/components")['VSlideXTransition']
export const VSlideXReverseTransition: typeof import("vuetify/components")['VSlideXReverseTransition']
export const VSlideYTransition: typeof import("vuetify/components")['VSlideYTransition']
export const VSlideYReverseTransition: typeof import("vuetify/components")['VSlideYReverseTransition']
export const VExpandTransition: typeof import("vuetify/components")['VExpandTransition']
export const VExpandXTransition: typeof import("vuetify/components")['VExpandXTransition']
export const VExpandBothTransition: typeof import("vuetify/components")['VExpandBothTransition']
export const VDialogTransition: typeof import("vuetify/components")['VDialogTransition']
export const VValidation: typeof import("vuetify/components")['VValidation']
export const NuxtLinkLocale: typeof import("../node_modules/@nuxtjs/i18n/dist/runtime/components/NuxtLinkLocale")['default']
export const SwitchLocalePathLink: typeof import("../node_modules/@nuxtjs/i18n/dist/runtime/components/SwitchLocalePathLink")['default']
export const NuxtPage: typeof import("../node_modules/nuxt/dist/pages/runtime/page")['default']
export const NoScript: typeof import("../node_modules/nuxt/dist/head/runtime/components")['NoScript']
export const Link: typeof import("../node_modules/nuxt/dist/head/runtime/components")['Link']
export const Base: typeof import("../node_modules/nuxt/dist/head/runtime/components")['Base']
export const Title: typeof import("../node_modules/nuxt/dist/head/runtime/components")['Title']
export const Meta: typeof import("../node_modules/nuxt/dist/head/runtime/components")['Meta']
export const Style: typeof import("../node_modules/nuxt/dist/head/runtime/components")['Style']
export const Head: typeof import("../node_modules/nuxt/dist/head/runtime/components")['Head']
export const Html: typeof import("../node_modules/nuxt/dist/head/runtime/components")['Html']
export const Body: typeof import("../node_modules/nuxt/dist/head/runtime/components")['Body']
export const NuxtIsland: typeof import("../node_modules/nuxt/dist/app/components/nuxt-island")['default']
export const LazyAiLogsTile: LazyComponent<typeof import("../components/AiLogsTile.vue")['default']>
export const LazyFinancialTile: LazyComponent<typeof import("../components/FinancialTile.vue")['default']>
export const LazySalespersonTile: LazyComponent<typeof import("../components/SalespersonTile.vue")['default']>
export const LazyServiceMapTile: LazyComponent<typeof import("../components/ServiceMapTile.vue")['default']>
export const LazySystemHealthTile: LazyComponent<typeof import("../components/SystemHealthTile.vue")['default']>
export const LazyTileCard: LazyComponent<typeof import("../components/TileCard.vue")['default']>
export const LazyTileWrapper: LazyComponent<typeof import("../components/TileWrapper.vue")['default']>
export const LazyMapServiceMap: LazyComponent<typeof import("../components/map/ServiceMap.vue")['default']>
export const LazyNuxtWelcome: LazyComponent<typeof import("../node_modules/nuxt/dist/app/components/welcome.vue")['default']>
export const LazyNuxtLayout: LazyComponent<typeof import("../node_modules/nuxt/dist/app/components/nuxt-layout")['default']>
export const LazyNuxtErrorBoundary: LazyComponent<typeof import("../node_modules/nuxt/dist/app/components/nuxt-error-boundary.vue")['default']>
export const LazyClientOnly: LazyComponent<typeof import("../node_modules/nuxt/dist/app/components/client-only")['default']>
export const LazyDevOnly: LazyComponent<typeof import("../node_modules/nuxt/dist/app/components/dev-only")['default']>
export const LazyServerPlaceholder: LazyComponent<typeof import("../node_modules/nuxt/dist/app/components/server-placeholder")['default']>
export const LazyNuxtLink: LazyComponent<typeof import("../node_modules/nuxt/dist/app/components/nuxt-link")['default']>
export const LazyNuxtLoadingIndicator: LazyComponent<typeof import("../node_modules/nuxt/dist/app/components/nuxt-loading-indicator")['default']>
export const LazyNuxtTime: LazyComponent<typeof import("../node_modules/nuxt/dist/app/components/nuxt-time.vue")['default']>
export const LazyNuxtRouteAnnouncer: LazyComponent<typeof import("../node_modules/nuxt/dist/app/components/nuxt-route-announcer")['default']>
export const LazyNuxtImg: LazyComponent<typeof import("../node_modules/nuxt/dist/app/components/nuxt-stubs")['NuxtImg']>
export const LazyNuxtPicture: LazyComponent<typeof import("../node_modules/nuxt/dist/app/components/nuxt-stubs")['NuxtPicture']>
export const LazyVAvatar: LazyComponent<typeof import("vuetify/components")['VAvatar']>
export const LazyVBanner: LazyComponent<typeof import("vuetify/components")['VBanner']>
export const LazyVBannerActions: LazyComponent<typeof import("vuetify/components")['VBannerActions']>
export const LazyVBannerText: LazyComponent<typeof import("vuetify/components")['VBannerText']>
export const LazyVApp: LazyComponent<typeof import("vuetify/components")['VApp']>
export const LazyVAppBar: LazyComponent<typeof import("vuetify/components")['VAppBar']>
export const LazyVAppBarNavIcon: LazyComponent<typeof import("vuetify/components")['VAppBarNavIcon']>
export const LazyVAppBarTitle: LazyComponent<typeof import("vuetify/components")['VAppBarTitle']>
export const LazyVCalendar: LazyComponent<typeof import("vuetify/components")['VCalendar']>
export const LazyVAlert: LazyComponent<typeof import("vuetify/components")['VAlert']>
export const LazyVAlertTitle: LazyComponent<typeof import("vuetify/components")['VAlertTitle']>
export const LazyVBtnToggle: LazyComponent<typeof import("vuetify/components")['VBtnToggle']>
export const LazyVBreadcrumbs: LazyComponent<typeof import("vuetify/components")['VBreadcrumbs']>
export const LazyVBreadcrumbsItem: LazyComponent<typeof import("vuetify/components")['VBreadcrumbsItem']>
export const LazyVBreadcrumbsDivider: LazyComponent<typeof import("vuetify/components")['VBreadcrumbsDivider']>
export const LazyVBtnGroup: LazyComponent<typeof import("vuetify/components")['VBtnGroup']>
export const LazyVBtn: LazyComponent<typeof import("vuetify/components")['VBtn']>
export const LazyVBadge: LazyComponent<typeof import("vuetify/components")['VBadge']>
export const LazyVBottomNavigation: LazyComponent<typeof import("vuetify/components")['VBottomNavigation']>
export const LazyVCheckbox: LazyComponent<typeof import("vuetify/components")['VCheckbox']>
export const LazyVCheckboxBtn: LazyComponent<typeof import("vuetify/components")['VCheckboxBtn']>
export const LazyVCarousel: LazyComponent<typeof import("vuetify/components")['VCarousel']>
export const LazyVCarouselItem: LazyComponent<typeof import("vuetify/components")['VCarouselItem']>
export const LazyVChip: LazyComponent<typeof import("vuetify/components")['VChip']>
export const LazyVCard: LazyComponent<typeof import("vuetify/components")['VCard']>
export const LazyVCardActions: LazyComponent<typeof import("vuetify/components")['VCardActions']>
export const LazyVCardItem: LazyComponent<typeof import("vuetify/components")['VCardItem']>
export const LazyVCardSubtitle: LazyComponent<typeof import("vuetify/components")['VCardSubtitle']>
export const LazyVCardText: LazyComponent<typeof import("vuetify/components")['VCardText']>
export const LazyVCardTitle: LazyComponent<typeof import("vuetify/components")['VCardTitle']>
export const LazyVBottomSheet: LazyComponent<typeof import("vuetify/components")['VBottomSheet']>
export const LazyVChipGroup: LazyComponent<typeof import("vuetify/components")['VChipGroup']>
export const LazyVColorPicker: LazyComponent<typeof import("vuetify/components")['VColorPicker']>
export const LazyVCombobox: LazyComponent<typeof import("vuetify/components")['VCombobox']>
export const LazyVCode: LazyComponent<typeof import("vuetify/components")['VCode']>
export const LazyVCounter: LazyComponent<typeof import("vuetify/components")['VCounter']>
export const LazyVDatePicker: LazyComponent<typeof import("vuetify/components")['VDatePicker']>
export const LazyVDatePickerControls: LazyComponent<typeof import("vuetify/components")['VDatePickerControls']>
export const LazyVDatePickerHeader: LazyComponent<typeof import("vuetify/components")['VDatePickerHeader']>
export const LazyVDatePickerMonth: LazyComponent<typeof import("vuetify/components")['VDatePickerMonth']>
export const LazyVDatePickerMonths: LazyComponent<typeof import("vuetify/components")['VDatePickerMonths']>
export const LazyVDatePickerYears: LazyComponent<typeof import("vuetify/components")['VDatePickerYears']>
export const LazyVDialog: LazyComponent<typeof import("vuetify/components")['VDialog']>
export const LazyVDivider: LazyComponent<typeof import("vuetify/components")['VDivider']>
export const LazyVFab: LazyComponent<typeof import("vuetify/components")['VFab']>
export const LazyVField: LazyComponent<typeof import("vuetify/components")['VField']>
export const LazyVFieldLabel: LazyComponent<typeof import("vuetify/components")['VFieldLabel']>
export const LazyVEmptyState: LazyComponent<typeof import("vuetify/components")['VEmptyState']>
export const LazyVExpansionPanels: LazyComponent<typeof import("vuetify/components")['VExpansionPanels']>
export const LazyVExpansionPanel: LazyComponent<typeof import("vuetify/components")['VExpansionPanel']>
export const LazyVExpansionPanelText: LazyComponent<typeof import("vuetify/components")['VExpansionPanelText']>
export const LazyVExpansionPanelTitle: LazyComponent<typeof import("vuetify/components")['VExpansionPanelTitle']>
export const LazyVDataTable: LazyComponent<typeof import("vuetify/components")['VDataTable']>
export const LazyVDataTableHeaders: LazyComponent<typeof import("vuetify/components")['VDataTableHeaders']>
export const LazyVDataTableFooter: LazyComponent<typeof import("vuetify/components")['VDataTableFooter']>
export const LazyVDataTableRows: LazyComponent<typeof import("vuetify/components")['VDataTableRows']>
export const LazyVDataTableRow: LazyComponent<typeof import("vuetify/components")['VDataTableRow']>
export const LazyVDataTableVirtual: LazyComponent<typeof import("vuetify/components")['VDataTableVirtual']>
export const LazyVDataTableServer: LazyComponent<typeof import("vuetify/components")['VDataTableServer']>
export const LazyVHotkey: LazyComponent<typeof import("vuetify/components")['VHotkey']>
export const LazyVFileInput: LazyComponent<typeof import("vuetify/components")['VFileInput']>
export const LazyVFooter: LazyComponent<typeof import("vuetify/components")['VFooter']>
export const LazyVImg: LazyComponent<typeof import("vuetify/components")['VImg']>
export const LazyVItemGroup: LazyComponent<typeof import("vuetify/components")['VItemGroup']>
export const LazyVItem: LazyComponent<typeof import("vuetify/components")['VItem']>
export const LazyVIcon: LazyComponent<typeof import("vuetify/components")['VIcon']>
export const LazyVComponentIcon: LazyComponent<typeof import("vuetify/components")['VComponentIcon']>
export const LazyVSvgIcon: LazyComponent<typeof import("vuetify/components")['VSvgIcon']>
export const LazyVLigatureIcon: LazyComponent<typeof import("vuetify/components")['VLigatureIcon']>
export const LazyVClassIcon: LazyComponent<typeof import("vuetify/components")['VClassIcon']>
export const LazyVInput: LazyComponent<typeof import("vuetify/components")['VInput']>
export const LazyVInfiniteScroll: LazyComponent<typeof import("vuetify/components")['VInfiniteScroll']>
export const LazyVKbd: LazyComponent<typeof import("vuetify/components")['VKbd']>
export const LazyVMenu: LazyComponent<typeof import("vuetify/components")['VMenu']>
export const LazyVNavigationDrawer: LazyComponent<typeof import("vuetify/components")['VNavigationDrawer']>
export const LazyVLabel: LazyComponent<typeof import("vuetify/components")['VLabel']>
export const LazyVMain: LazyComponent<typeof import("vuetify/components")['VMain']>
export const LazyVMessages: LazyComponent<typeof import("vuetify/components")['VMessages']>
export const LazyVOverlay: LazyComponent<typeof import("vuetify/components")['VOverlay']>
export const LazyVList: LazyComponent<typeof import("vuetify/components")['VList']>
export const LazyVListGroup: LazyComponent<typeof import("vuetify/components")['VListGroup']>
export const LazyVListImg: LazyComponent<typeof import("vuetify/components")['VListImg']>
export const LazyVListItem: LazyComponent<typeof import("vuetify/components")['VListItem']>
export const LazyVListItemAction: LazyComponent<typeof import("vuetify/components")['VListItemAction']>
export const LazyVListItemMedia: LazyComponent<typeof import("vuetify/components")['VListItemMedia']>
export const LazyVListItemSubtitle: LazyComponent<typeof import("vuetify/components")['VListItemSubtitle']>
export const LazyVListItemTitle: LazyComponent<typeof import("vuetify/components")['VListItemTitle']>
export const LazyVListSubheader: LazyComponent<typeof import("vuetify/components")['VListSubheader']>
export const LazyVPagination: LazyComponent<typeof import("vuetify/components")['VPagination']>
export const LazyVNumberInput: LazyComponent<typeof import("vuetify/components")['VNumberInput']>
export const LazyVProgressLinear: LazyComponent<typeof import("vuetify/components")['VProgressLinear']>
export const LazyVOtpInput: LazyComponent<typeof import("vuetify/components")['VOtpInput']>
export const LazyVRadioGroup: LazyComponent<typeof import("vuetify/components")['VRadioGroup']>
export const LazyVSelectionControl: LazyComponent<typeof import("vuetify/components")['VSelectionControl']>
export const LazyVProgressCircular: LazyComponent<typeof import("vuetify/components")['VProgressCircular']>
export const LazyVSelect: LazyComponent<typeof import("vuetify/components")['VSelect']>
export const LazyVSheet: LazyComponent<typeof import("vuetify/components")['VSheet']>
export const LazyVSelectionControlGroup: LazyComponent<typeof import("vuetify/components")['VSelectionControlGroup']>
export const LazyVSlideGroup: LazyComponent<typeof import("vuetify/components")['VSlideGroup']>
export const LazyVSlideGroupItem: LazyComponent<typeof import("vuetify/components")['VSlideGroupItem']>
export const LazyVSkeletonLoader: LazyComponent<typeof import("vuetify/components")['VSkeletonLoader']>
export const LazyVRating: LazyComponent<typeof import("vuetify/components")['VRating']>
export const LazyVSnackbar: LazyComponent<typeof import("vuetify/components")['VSnackbar']>
export const LazyVTextarea: LazyComponent<typeof import("vuetify/components")['VTextarea']>
export const LazyVSystemBar: LazyComponent<typeof import("vuetify/components")['VSystemBar']>
export const LazyVSwitch: LazyComponent<typeof import("vuetify/components")['VSwitch']>
export const LazyVStepper: LazyComponent<typeof import("vuetify/components")['VStepper']>
export const LazyVStepperActions: LazyComponent<typeof import("vuetify/components")['VStepperActions']>
export const LazyVStepperHeader: LazyComponent<typeof import("vuetify/components")['VStepperHeader']>
export const LazyVStepperItem: LazyComponent<typeof import("vuetify/components")['VStepperItem']>
export const LazyVStepperWindow: LazyComponent<typeof import("vuetify/components")['VStepperWindow']>
export const LazyVStepperWindowItem: LazyComponent<typeof import("vuetify/components")['VStepperWindowItem']>
export const LazyVSlider: LazyComponent<typeof import("vuetify/components")['VSlider']>
export const LazyVTab: LazyComponent<typeof import("vuetify/components")['VTab']>
export const LazyVTabs: LazyComponent<typeof import("vuetify/components")['VTabs']>
export const LazyVTabsWindow: LazyComponent<typeof import("vuetify/components")['VTabsWindow']>
export const LazyVTabsWindowItem: LazyComponent<typeof import("vuetify/components")['VTabsWindowItem']>
export const LazyVTable: LazyComponent<typeof import("vuetify/components")['VTable']>
export const LazyVTimeline: LazyComponent<typeof import("vuetify/components")['VTimeline']>
export const LazyVTimelineItem: LazyComponent<typeof import("vuetify/components")['VTimelineItem']>
export const LazyVTextField: LazyComponent<typeof import("vuetify/components")['VTextField']>
export const LazyVTooltip: LazyComponent<typeof import("vuetify/components")['VTooltip']>
export const LazyVToolbar: LazyComponent<typeof import("vuetify/components")['VToolbar']>
export const LazyVToolbarTitle: LazyComponent<typeof import("vuetify/components")['VToolbarTitle']>
export const LazyVToolbarItems: LazyComponent<typeof import("vuetify/components")['VToolbarItems']>
export const LazyVWindow: LazyComponent<typeof import("vuetify/components")['VWindow']>
export const LazyVWindowItem: LazyComponent<typeof import("vuetify/components")['VWindowItem']>
export const LazyVTimePicker: LazyComponent<typeof import("vuetify/components")['VTimePicker']>
export const LazyVTimePickerClock: LazyComponent<typeof import("vuetify/components")['VTimePickerClock']>
export const LazyVTimePickerControls: LazyComponent<typeof import("vuetify/components")['VTimePickerControls']>
export const LazyVTreeview: LazyComponent<typeof import("vuetify/components")['VTreeview']>
export const LazyVTreeviewItem: LazyComponent<typeof import("vuetify/components")['VTreeviewItem']>
export const LazyVTreeviewGroup: LazyComponent<typeof import("vuetify/components")['VTreeviewGroup']>
export const LazyVConfirmEdit: LazyComponent<typeof import("vuetify/components")['VConfirmEdit']>
export const LazyVDataIterator: LazyComponent<typeof import("vuetify/components")['VDataIterator']>
export const LazyVDefaultsProvider: LazyComponent<typeof import("vuetify/components")['VDefaultsProvider']>
export const LazyVContainer: LazyComponent<typeof import("vuetify/components")['VContainer']>
export const LazyVCol: LazyComponent<typeof import("vuetify/components")['VCol']>
export const LazyVRow: LazyComponent<typeof import("vuetify/components")['VRow']>
export const LazyVSpacer: LazyComponent<typeof import("vuetify/components")['VSpacer']>
export const LazyVForm: LazyComponent<typeof import("vuetify/components")['VForm']>
export const LazyVAutocomplete: LazyComponent<typeof import("vuetify/components")['VAutocomplete']>
export const LazyVHover: LazyComponent<typeof import("vuetify/components")['VHover']>
export const LazyVLazy: LazyComponent<typeof import("vuetify/components")['VLazy']>
export const LazyVLayout: LazyComponent<typeof import("vuetify/components")['VLayout']>
export const LazyVLayoutItem: LazyComponent<typeof import("vuetify/components")['VLayoutItem']>
export const LazyVLocaleProvider: LazyComponent<typeof import("vuetify/components")['VLocaleProvider']>
export const LazyVRadio: LazyComponent<typeof import("vuetify/components")['VRadio']>
export const LazyVParallax: LazyComponent<typeof import("vuetify/components")['VParallax']>
export const LazyVNoSsr: LazyComponent<typeof import("vuetify/components")['VNoSsr']>
export const LazyVRangeSlider: LazyComponent<typeof import("vuetify/components")['VRangeSlider']>
export const LazyVResponsive: LazyComponent<typeof import("vuetify/components")['VResponsive']>
export const LazyVSnackbarQueue: LazyComponent<typeof import("vuetify/components")['VSnackbarQueue']>
export const LazyVSpeedDial: LazyComponent<typeof import("vuetify/components")['VSpeedDial']>
export const LazyVSparkline: LazyComponent<typeof import("vuetify/components")['VSparkline']>
export const LazyVVirtualScroll: LazyComponent<typeof import("vuetify/components")['VVirtualScroll']>
export const LazyVThemeProvider: LazyComponent<typeof import("vuetify/components")['VThemeProvider']>
export const LazyVFabTransition: LazyComponent<typeof import("vuetify/components")['VFabTransition']>
export const LazyVDialogBottomTransition: LazyComponent<typeof import("vuetify/components")['VDialogBottomTransition']>
export const LazyVDialogTopTransition: LazyComponent<typeof import("vuetify/components")['VDialogTopTransition']>
export const LazyVFadeTransition: LazyComponent<typeof import("vuetify/components")['VFadeTransition']>
export const LazyVScaleTransition: LazyComponent<typeof import("vuetify/components")['VScaleTransition']>
export const LazyVScrollXTransition: LazyComponent<typeof import("vuetify/components")['VScrollXTransition']>
export const LazyVScrollXReverseTransition: LazyComponent<typeof import("vuetify/components")['VScrollXReverseTransition']>
export const LazyVScrollYTransition: LazyComponent<typeof import("vuetify/components")['VScrollYTransition']>
export const LazyVScrollYReverseTransition: LazyComponent<typeof import("vuetify/components")['VScrollYReverseTransition']>
export const LazyVSlideXTransition: LazyComponent<typeof import("vuetify/components")['VSlideXTransition']>
export const LazyVSlideXReverseTransition: LazyComponent<typeof import("vuetify/components")['VSlideXReverseTransition']>
export const LazyVSlideYTransition: LazyComponent<typeof import("vuetify/components")['VSlideYTransition']>
export const LazyVSlideYReverseTransition: LazyComponent<typeof import("vuetify/components")['VSlideYReverseTransition']>
export const LazyVExpandTransition: LazyComponent<typeof import("vuetify/components")['VExpandTransition']>
export const LazyVExpandXTransition: LazyComponent<typeof import("vuetify/components")['VExpandXTransition']>
export const LazyVExpandBothTransition: LazyComponent<typeof import("vuetify/components")['VExpandBothTransition']>
export const LazyVDialogTransition: LazyComponent<typeof import("vuetify/components")['VDialogTransition']>
export const LazyVValidation: LazyComponent<typeof import("vuetify/components")['VValidation']>
export const LazyNuxtLinkLocale: LazyComponent<typeof import("../node_modules/@nuxtjs/i18n/dist/runtime/components/NuxtLinkLocale")['default']>
export const LazySwitchLocalePathLink: LazyComponent<typeof import("../node_modules/@nuxtjs/i18n/dist/runtime/components/SwitchLocalePathLink")['default']>
export const LazyNuxtPage: LazyComponent<typeof import("../node_modules/nuxt/dist/pages/runtime/page")['default']>
export const LazyNoScript: LazyComponent<typeof import("../node_modules/nuxt/dist/head/runtime/components")['NoScript']>
export const LazyLink: LazyComponent<typeof import("../node_modules/nuxt/dist/head/runtime/components")['Link']>
export const LazyBase: LazyComponent<typeof import("../node_modules/nuxt/dist/head/runtime/components")['Base']>
export const LazyTitle: LazyComponent<typeof import("../node_modules/nuxt/dist/head/runtime/components")['Title']>
export const LazyMeta: LazyComponent<typeof import("../node_modules/nuxt/dist/head/runtime/components")['Meta']>
export const LazyStyle: LazyComponent<typeof import("../node_modules/nuxt/dist/head/runtime/components")['Style']>
export const LazyHead: LazyComponent<typeof import("../node_modules/nuxt/dist/head/runtime/components")['Head']>
export const LazyHtml: LazyComponent<typeof import("../node_modules/nuxt/dist/head/runtime/components")['Html']>
export const LazyBody: LazyComponent<typeof import("../node_modules/nuxt/dist/head/runtime/components")['Body']>
export const LazyNuxtIsland: LazyComponent<typeof import("../node_modules/nuxt/dist/app/components/nuxt-island")['default']>
export const componentNames: string[]

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,119 +0,0 @@
// @ts-nocheck
export const localeCodes = [
"en",
"hu"
]
export const localeLoaders = {
"en": [{ key: "../locales/en.json", load: () => import("../locales/en.json" /* webpackChunkName: "locale__app_locales_en_json" */), cache: true }],
"hu": [{ key: "../locales/hu.json", load: () => import("../locales/hu.json" /* webpackChunkName: "locale__app_locales_hu_json" */), cache: true }]
}
export const vueI18nConfigs = [
]
export const nuxtI18nOptions = {
"experimental": {
"localeDetector": "",
"switchLocalePathLinkSSR": false,
"autoImportTranslationFunctions": false
},
"bundle": {
"compositionOnly": true,
"runtimeOnly": false,
"fullInstall": true,
"dropMessageCompiler": false
},
"compilation": {
"jit": true,
"strictMessage": true,
"escapeHtml": false
},
"customBlocks": {
"defaultSFCLang": "json",
"globalSFCScope": false
},
"vueI18n": "",
"locales": [
{
"code": "en",
"name": "English",
"language": "en-US",
"files": [
"/app/locales/en.json"
]
},
{
"code": "hu",
"name": "Magyar",
"language": "hu-HU",
"files": [
"/app/locales/hu.json"
]
}
],
"defaultLocale": "hu",
"defaultDirection": "ltr",
"routesNameSeparator": "___",
"trailingSlash": false,
"defaultLocaleRouteNameSuffix": "default",
"strategy": "no_prefix",
"lazy": true,
"langDir": "locales",
"detectBrowserLanguage": {
"alwaysRedirect": false,
"cookieCrossOrigin": false,
"cookieDomain": null,
"cookieKey": "i18n_redirected",
"cookieSecure": false,
"fallbackLocale": "",
"redirectOn": "root",
"useCookie": true
},
"differentDomains": false,
"baseUrl": "",
"dynamicRouteParams": false,
"customRoutes": "page",
"pages": {},
"skipSettingLocaleOnNavigate": false,
"types": "composition",
"debug": false,
"parallelPlugin": false,
"multiDomainLocales": false,
"i18nModules": []
}
export const normalizedLocales = [
{
"code": "en",
"name": "English",
"language": "en-US",
"files": [
{
"path": "/app/locales/en.json"
}
]
},
{
"code": "hu",
"name": "Magyar",
"language": "hu-HU",
"files": [
{
"path": "/app/locales/hu.json"
}
]
}
]
export const NUXT_I18N_MODULE_ID = "@nuxtjs/i18n"
export const parallelPlugin = false
export const isSSG = false
export const DEFAULT_DYNAMIC_PARAMS_KEY = "nuxtI18n"
export const DEFAULT_COOKIE_KEY = "i18n_redirected"
export const SWITCH_LOCALE_PATH_LINK_IDENTIFIER = "nuxt-i18n-slp"

View File

@@ -1,43 +0,0 @@
export { useScriptTriggerConsent, useScriptEventPage, useScriptTriggerElement, useScript, useScriptGoogleAnalytics, useScriptPlausibleAnalytics, useScriptCrisp, useScriptClarity, useScriptCloudflareWebAnalytics, useScriptFathomAnalytics, useScriptMatomoAnalytics, useScriptGoogleTagManager, useScriptGoogleAdsense, useScriptSegment, useScriptMetaPixel, useScriptXPixel, useScriptIntercom, useScriptHotjar, useScriptStripe, useScriptLemonSqueezy, useScriptVimeoPlayer, useScriptYouTubePlayer, useScriptGoogleMaps, useScriptNpm, useScriptUmamiAnalytics, useScriptSnapchatPixel, useScriptRybbitAnalytics, useScriptDatabuddyAnalytics, useScriptRedditPixel, useScriptPayPal } from '#app/composables/script-stubs';
export { isVue2, isVue3 } from 'vue-demi';
export { defineNuxtLink } from '#app/components/nuxt-link';
export { useNuxtApp, tryUseNuxtApp, defineNuxtPlugin, definePayloadPlugin, useRuntimeConfig, defineAppConfig } from '#app/nuxt';
export { useAppConfig, updateAppConfig } from '#app/config';
export { defineNuxtComponent } from '#app/composables/component';
export { useAsyncData, useLazyAsyncData, useNuxtData, refreshNuxtData, clearNuxtData } from '#app/composables/asyncData';
export { useHydration } from '#app/composables/hydrate';
export { callOnce } from '#app/composables/once';
export { useState, clearNuxtState } from '#app/composables/state';
export { clearError, createError, isNuxtError, showError, useError } from '#app/composables/error';
export { useFetch, useLazyFetch } from '#app/composables/fetch';
export { useCookie, refreshCookie } from '#app/composables/cookie';
export { onPrehydrate, prerenderRoutes, useRequestHeader, useRequestHeaders, useResponseHeader, useRequestEvent, useRequestFetch, setResponseStatus } from '#app/composables/ssr';
export { onNuxtReady } from '#app/composables/ready';
export { preloadComponents, prefetchComponents, preloadRouteComponents } from '#app/composables/preload';
export { abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, setPageLayout, navigateTo, useRoute, useRouter } from '#app/composables/router';
export { isPrerendered, loadPayload, preloadPayload, definePayloadReducer, definePayloadReviver } from '#app/composables/payload';
export { useLoadingIndicator } from '#app/composables/loading-indicator';
export { getAppManifest, getRouteRules } from '#app/composables/manifest';
export { reloadNuxtApp } from '#app/composables/chunk';
export { useRequestURL } from '#app/composables/url';
export { usePreviewMode } from '#app/composables/preview';
export { useRouteAnnouncer } from '#app/composables/route-announcer';
export { useRuntimeHook } from '#app/composables/runtime-hook';
export { useHead, useHeadSafe, useServerHeadSafe, useServerHead, useSeoMeta, useServerSeoMeta, injectHead } from '#app/composables/head';
export { onBeforeRouteLeave, onBeforeRouteUpdate, useLink } from 'vue-router';
export { withCtx, withDirectives, withKeys, withMemo, withModifiers, withScopeId, onActivated, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onDeactivated, onErrorCaptured, onMounted, onRenderTracked, onRenderTriggered, onServerPrefetch, onUnmounted, onUpdated, computed, customRef, isProxy, isReactive, isReadonly, isRef, markRaw, proxyRefs, reactive, readonly, ref, shallowReactive, shallowReadonly, shallowRef, toRaw, toRef, toRefs, triggerRef, unref, watch, watchEffect, watchPostEffect, watchSyncEffect, onWatcherCleanup, isShallow, effect, effectScope, getCurrentScope, onScopeDispose, defineComponent, defineAsyncComponent, resolveComponent, getCurrentInstance, h, inject, hasInjectionContext, nextTick, provide, toValue, useModel, useAttrs, useCssModule, useCssVars, useSlots, useTransitionState, useId, useTemplateRef, useShadowRoot, Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue';
export { requestIdleCallback, cancelIdleCallback } from '#app/compat/idle-callback';
export { setInterval } from '#app/compat/interval';
export { definePageMeta } from '../node_modules/nuxt/dist/pages/runtime/composables';
export { defineLazyHydrationComponent } from '#app/composables/lazy-hydration';
export { useHealthMonitor, HealthMetrics, SystemAlert, HealthMonitorState } from '../composables/useHealthMonitor';
export { default as usePolling, PollingOptions, PollingState } from '../composables/usePolling';
export { Role, Role, ScopeLevel, ScopeLevel, RoleRank, AdminTiles, useRBAC, TilePermission } from '../composables/useRBAC';
export { useServiceMap, Service, Scope } from '../composables/useServiceMap';
export { default as useUserManagement, UpdateUserRoleRequest, UserManagementState } from '../composables/useUserManagement';
export { useAuthStore, JwtPayload, User } from '../stores/auth';
export { useTileStore, UserTilePreference } from '../stores/tiles';
export { defineStore, acceptHMRUpdate, usePinia, storeToRefs } from '../node_modules/@pinia/nuxt/dist/runtime/composables';
export { useLocale, useDefaults, useDisplay, useLayout, useRtl, useTheme } from 'vuetify';
export { useI18n } from '../node_modules/vue-i18n/dist/vue-i18n';
export { useRouteBaseName, useLocalePath, useLocaleRoute, useSwitchLocalePath, useLocaleHead, useBrowserLocale, useCookieLocale, useSetI18nParams, defineI18nRoute, defineI18nLocale, defineI18nConfig } from '../node_modules/@nuxtjs/i18n/dist/runtime/composables/index';

View File

@@ -1 +0,0 @@
{"id":"dev","timestamp":1774810059036}

View File

@@ -1 +0,0 @@
{"id":"dev","timestamp":1774810059036,"prerendered":[]}

View File

@@ -1,17 +0,0 @@
{
"date": "2026-03-29T18:47:42.746Z",
"preset": "nitro-dev",
"framework": {
"name": "nuxt",
"version": "3.21.2"
},
"versions": {
"nitro": "2.13.2"
},
"dev": {
"pid": 19,
"workerAddress": {
"socketPath": "\u0000nitro-worker-19-4-4-8465.sock"
}
}
}

View File

@@ -1,30 +0,0 @@
/// <reference types="@pinia/nuxt" />
/// <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" />
/// <reference path="types/app-defaults.d.ts" />
/// <reference path="types/plugins.d.ts" />
/// <reference path="types/build.d.ts" />
/// <reference path="types/schema.d.ts" />
/// <reference path="types/app.config.d.ts" />
/// <reference path="../node_modules/@nuxt/vite-builder/dist/index.d.mts" />
/// <reference path="../node_modules/@nuxt/nitro-server/dist/index.d.mts" />
/// <reference types="@pinia/nuxt" />
/// <reference types="vuetify" />
/// <reference types="vuetify-nuxt-module/configuration" />
/// <reference path="types/i18n-plugin.d.ts" />
/// <reference types="vue-router" />
/// <reference path="types/middleware.d.ts" />
/// <reference path="types/nitro-middleware.d.ts" />
/// <reference path="types/layouts.d.ts" />
/// <reference path="types/components.d.ts" />
/// <reference path="imports.d.ts" />
/// <reference path="types/imports.d.ts" />
/// <reference path="schema/nuxt.schema.d.ts" />
/// <reference path="types/nitro.d.ts" />
export {}

View File

@@ -1,17 +0,0 @@
export interface NuxtCustomSchema {
}
export type CustomAppConfig = Exclude<NuxtCustomSchema['appConfig'], undefined>
type _CustomAppConfig = CustomAppConfig
declare module '@nuxt/schema' {
interface NuxtConfig extends Omit<NuxtCustomSchema, 'appConfig'> {}
interface NuxtOptions extends Omit<NuxtCustomSchema, 'appConfig'> {}
interface CustomAppConfig extends _CustomAppConfig {}
}
declare module 'nuxt/schema' {
interface NuxtConfig extends Omit<NuxtCustomSchema, 'appConfig'> {}
interface NuxtOptions extends Omit<NuxtCustomSchema, 'appConfig'> {}
interface CustomAppConfig extends _CustomAppConfig {}
}

View File

@@ -1,3 +0,0 @@
{
"id": "#"
}

View File

@@ -1,13 +0,0 @@
// 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";
;
const config = [
{"content":{"files":["/app/components/**/*.{vue,js,jsx,mjs,ts,tsx}","/app/components/global/**/*.{vue,js,jsx,mjs,ts,tsx}","/app/components/**/*.{vue,js,jsx,mjs,ts,tsx}","/app/layouts/**/*.{vue,js,jsx,mjs,ts,tsx}","/app/plugins/**/*.{js,ts,mjs}","/app/composables/**/*.{js,ts,mjs}","/app/utils/**/*.{js,ts,mjs}","/app/pages/**/*.{vue,js,jsx,mjs,ts,tsx}","/app/{A,a}pp.{vue,js,jsx,mjs,ts,tsx}","/app/{E,e}rror.{vue,js,jsx,mjs,ts,tsx}","/app/app.config.{js,ts,mjs}"]}},
{}
].reduce((acc, curr) => configMerger(acc, curr), {});
const resolvedConfig = config;
export default resolvedConfig;

View File

@@ -1,199 +0,0 @@
{
"compilerOptions": {
"paths": {
"@vue/runtime-core": [
"../node_modules/@vue/runtime-core"
],
"@vue/compiler-sfc": [
"../node_modules/@vue/compiler-sfc"
],
"unplugin-vue-router/client": [
"../node_modules/unplugin-vue-router/client"
],
"@nuxt/schema": [
"../node_modules/@nuxt/schema"
],
"nuxt": [
"../node_modules/nuxt"
],
"vite/client": [
"../node_modules/vite/client"
],
"nitropack/types": [
"../node_modules/nitropack/types"
],
"nitropack/runtime": [
"../node_modules/nitropack/runtime"
],
"nitropack": [
"../node_modules/nitropack"
],
"defu": [
"../node_modules/defu"
],
"h3": [
"../node_modules/h3"
],
"consola": [
"../node_modules/consola"
],
"ofetch": [
"../node_modules/ofetch"
],
"crossws": [
"../node_modules/crossws"
],
"~": [
".."
],
"~/*": [
"../*"
],
"@": [
".."
],
"@/*": [
"../*"
],
"~~": [
".."
],
"~~/*": [
"../*"
],
"@@": [
".."
],
"@@/*": [
"../*"
],
"#shared": [
"../shared"
],
"#shared/*": [
"../shared/*"
],
"assets": [
"../assets"
],
"assets/*": [
"../assets/*"
],
"public": [
"../public"
],
"public/*": [
"../public/*"
],
"#server": [
"../server"
],
"#server/*": [
"../server/*"
],
"#app": [
"../node_modules/nuxt/dist/app"
],
"#app/*": [
"../node_modules/nuxt/dist/app/*"
],
"vue-demi": [
"../node_modules/nuxt/dist/app/compat/vue-demi"
],
"pinia": [
"../node_modules/pinia/dist/pinia"
],
"vue-i18n": [
"../node_modules/vue-i18n/dist/vue-i18n"
],
"@intlify/shared": [
"../node_modules/@intlify/shared/dist/shared"
],
"@intlify/message-compiler": [
"../node_modules/@intlify/message-compiler/dist/message-compiler"
],
"@intlify/core-base": [
"../node_modules/@intlify/core-base/dist/core-base"
],
"@intlify/core": [
"../node_modules/@intlify/core/dist/core.node"
],
"@intlify/utils/h3": [
"../node_modules/@intlify/utils/dist/h3"
],
"ufo": [
"../node_modules/ufo/dist/index"
],
"is-https": [
"../node_modules/is-https/dist/index"
],
"#i18n": [
"../node_modules/@nuxtjs/i18n/dist/runtime/composables/index"
],
"#vue-router": [
"../node_modules/vue-router"
],
"#unhead/composables": [
"../node_modules/nuxt/dist/head/runtime/composables/v3"
],
"#imports": [
"./imports"
],
"#app-manifest": [
"./manifest/meta/dev.json"
],
"#components": [
"./components"
],
"#build": [
"."
],
"#build/*": [
"./*"
]
},
"esModuleInterop": true,
"skipLibCheck": true,
"target": "ESNext",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"allowArbitraryExtensions": true,
"strict": true,
"noUncheckedIndexedAccess": false,
"forceConsistentCasingInFileNames": true,
"noImplicitOverride": true,
"module": "preserve",
"noEmit": true,
"lib": [
"ESNext",
"dom",
"dom.iterable",
"webworker"
],
"jsx": "preserve",
"jsxImportSource": "vue",
"types": [],
"moduleResolution": "Bundler",
"useDefineForClassFields": true,
"noImplicitThis": true,
"allowSyntheticDefaultImports": true
},
"include": [
"../**/*",
"../.config/nuxt.*",
"./nuxt.d.ts",
"../node_modules/runtime",
"../node_modules/dist/runtime",
".."
],
"exclude": [
"../dist",
"../.data",
"../node_modules/runtime/server",
"../node_modules/dist/runtime/server",
"dev"
]
}

View File

@@ -1,168 +0,0 @@
{
"compilerOptions": {
"forceConsistentCasingInFileNames": true,
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"allowJs": true,
"resolveJsonModule": true,
"jsx": "preserve",
"allowSyntheticDefaultImports": true,
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
"paths": {
"#imports": [
"./types/nitro-imports"
],
"~/*": [
"../*"
],
"@/*": [
"../*"
],
"~~/*": [
"../*"
],
"@@/*": [
"../*"
],
"@vue/runtime-core": [
"../node_modules/@vue/runtime-core"
],
"@vue/compiler-sfc": [
"../node_modules/@vue/compiler-sfc"
],
"unplugin-vue-router/client": [
"../node_modules/unplugin-vue-router/client"
],
"@nuxt/schema": [
"../node_modules/@nuxt/schema"
],
"nuxt": [
"../node_modules/nuxt"
],
"vite/client": [
"../node_modules/vite/client"
],
"nitropack/types": [
"../node_modules/nitropack/types"
],
"nitropack/runtime": [
"../node_modules/nitropack/runtime"
],
"nitropack": [
"../node_modules/nitropack"
],
"defu": [
"../node_modules/defu"
],
"h3": [
"../node_modules/h3"
],
"consola": [
"../node_modules/consola"
],
"ofetch": [
"../node_modules/ofetch"
],
"crossws": [
"../node_modules/crossws"
],
"#shared": [
"../shared"
],
"#shared/*": [
"../shared/*"
],
"assets": [
"../assets"
],
"assets/*": [
"../assets/*"
],
"public": [
"../public"
],
"public/*": [
"../public/*"
],
"#server": [
"../server"
],
"#server/*": [
"../server/*"
],
"#build": [
"./"
],
"#build/*": [
"./*"
],
"#internal/nuxt/paths": [
"../node_modules/@nuxt/nitro-server/dist/runtime/utils/paths"
],
"pinia": [
"../node_modules/pinia/dist/pinia"
],
"vue-i18n": [
"../node_modules/vue-i18n/dist/vue-i18n"
],
"@intlify/shared": [
"../node_modules/@intlify/shared/dist/shared"
],
"@intlify/message-compiler": [
"../node_modules/@intlify/message-compiler/dist/message-compiler"
],
"@intlify/core-base": [
"../node_modules/@intlify/core-base/dist/core-base"
],
"@intlify/core": [
"../node_modules/@intlify/core/dist/core.node"
],
"@intlify/utils/h3": [
"../node_modules/@intlify/utils/dist/h3"
],
"ufo": [
"../node_modules/ufo/dist/index"
],
"is-https": [
"../node_modules/is-https/dist/index"
],
"#i18n": [
"../node_modules/@nuxtjs/i18n/dist/runtime/composables/index"
],
"#unhead/composables": [
"../node_modules/nuxt/dist/head/runtime/composables/v3"
]
},
"lib": [
"esnext",
"webworker",
"dom.iterable"
],
"noUncheckedIndexedAccess": true,
"allowArbitraryExtensions": true
},
"include": [
"./types/nitro-nuxt.d.ts",
"../node_modules/runtime/server",
"../node_modules/dist/runtime/server",
"../server/**/*",
"../shared/**/*.d.ts",
"./types/nitro.d.ts",
"../**/*"
],
"exclude": [
"../node_modules",
"../node_modules/nuxt/node_modules",
"../node_modules/@pinia/nuxt/node_modules",
"../node_modules/@nuxtjs/tailwindcss/node_modules",
"../node_modules/vuetify-nuxt-module/node_modules",
"../node_modules/@nuxtjs/i18n/node_modules",
"../node_modules/@nuxt/telemetry/node_modules",
"../dist"
]
}

View File

@@ -1,7 +0,0 @@
declare module 'nuxt/app/defaults' {
type DefaultAsyncDataErrorValue = null
type DefaultAsyncDataValue = null
type DefaultErrorValue = null
type DedupeOption = boolean | 'cancel' | 'defer'
}

View File

@@ -1,31 +0,0 @@
import type { CustomAppConfig } from 'nuxt/schema'
import type { Defu } from 'defu'
declare const inlineConfig = {
"nuxt": {}
}
type ResolvedAppConfig = Defu<typeof inlineConfig, []>
type IsAny<T> = 0 extends 1 & T ? true : false
type MergedAppConfig<Resolved extends Record<string, unknown>, Custom extends Record<string, unknown>> = {
[K in keyof (Resolved & Custom)]: K extends keyof Custom
? unknown extends Custom[K]
? Resolved[K]
: IsAny<Custom[K]> extends true
? Resolved[K]
: Custom[K] extends Record<string, any>
? Resolved[K] extends Record<string, any>
? MergedAppConfig<Resolved[K], Custom[K]>
: Exclude<Custom[K], undefined>
: Exclude<Custom[K], undefined>
: Resolved[K]
}
declare module 'nuxt/schema' {
interface AppConfig extends MergedAppConfig<ResolvedAppConfig, CustomAppConfig> { }
}
declare module '@nuxt/schema' {
interface AppConfig extends MergedAppConfig<ResolvedAppConfig, CustomAppConfig> { }
}

View File

@@ -1,24 +0,0 @@
declare module "#build/app-component.mjs";
declare module "#build/nitro.client.mjs";
declare module "#build/plugins.client.mjs";
declare module "#build/css.mjs";
declare module "#build/fetch.mjs";
declare module "#build/error-component.mjs";
declare module "#build/global-polyfills.mjs";
declare module "#build/layouts.mjs";
declare module "#build/middleware.mjs";
declare module "#build/nuxt.config.mjs";
declare module "#build/paths.mjs";
declare module "#build/root-component.mjs";
declare module "#build/plugins.server.mjs";
declare module "#build/test-component-wrapper.mjs";
declare module "#build/routes.mjs";
declare module "#build/pages.mjs";
declare module "#build/router.options.mjs";
declare module "#build/unhead-options.mjs";
declare module "#build/unhead.config.mjs";
declare module "#build/components.plugin.mjs";
declare module "#build/component-names.mjs";
declare module "#build/components.islands.mjs";
declare module "#build/component-chunk.mjs";
declare module "#build/route-rules.mjs";

View File

@@ -1 +0,0 @@
import "vite/client";

View File

@@ -1,439 +0,0 @@
import type { DefineComponent, SlotsType } from 'vue'
type IslandComponent<T> = DefineComponent<{}, {refresh: () => Promise<void>}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, SlotsType<{ fallback: { error: unknown } }>> & T
type HydrationStrategies = {
hydrateOnVisible?: IntersectionObserverInit | true
hydrateOnIdle?: number | true
hydrateOnInteraction?: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap> | true
hydrateOnMediaQuery?: string
hydrateAfter?: number
hydrateWhen?: boolean
hydrateNever?: true
}
type LazyComponent<T> = DefineComponent<HydrationStrategies, {}, {}, {}, {}, {}, {}, { hydrated: () => void }> & T
interface _GlobalComponents {
AiLogsTile: typeof import("../../components/AiLogsTile.vue")['default']
FinancialTile: typeof import("../../components/FinancialTile.vue")['default']
SalespersonTile: typeof import("../../components/SalespersonTile.vue")['default']
ServiceMapTile: typeof import("../../components/ServiceMapTile.vue")['default']
SystemHealthTile: typeof import("../../components/SystemHealthTile.vue")['default']
TileCard: typeof import("../../components/TileCard.vue")['default']
TileWrapper: typeof import("../../components/TileWrapper.vue")['default']
MapServiceMap: typeof import("../../components/map/ServiceMap.vue")['default']
NuxtWelcome: typeof import("../../node_modules/nuxt/dist/app/components/welcome.vue")['default']
NuxtLayout: typeof import("../../node_modules/nuxt/dist/app/components/nuxt-layout")['default']
NuxtErrorBoundary: typeof import("../../node_modules/nuxt/dist/app/components/nuxt-error-boundary.vue")['default']
ClientOnly: typeof import("../../node_modules/nuxt/dist/app/components/client-only")['default']
DevOnly: typeof import("../../node_modules/nuxt/dist/app/components/dev-only")['default']
ServerPlaceholder: typeof import("../../node_modules/nuxt/dist/app/components/server-placeholder")['default']
NuxtLink: typeof import("../../node_modules/nuxt/dist/app/components/nuxt-link")['default']
NuxtLoadingIndicator: typeof import("../../node_modules/nuxt/dist/app/components/nuxt-loading-indicator")['default']
NuxtTime: typeof import("../../node_modules/nuxt/dist/app/components/nuxt-time.vue")['default']
NuxtRouteAnnouncer: typeof import("../../node_modules/nuxt/dist/app/components/nuxt-route-announcer")['default']
NuxtImg: typeof import("../../node_modules/nuxt/dist/app/components/nuxt-stubs")['NuxtImg']
NuxtPicture: typeof import("../../node_modules/nuxt/dist/app/components/nuxt-stubs")['NuxtPicture']
VAvatar: typeof import("vuetify/components")['VAvatar']
VBanner: typeof import("vuetify/components")['VBanner']
VBannerActions: typeof import("vuetify/components")['VBannerActions']
VBannerText: typeof import("vuetify/components")['VBannerText']
VApp: typeof import("vuetify/components")['VApp']
VAppBar: typeof import("vuetify/components")['VAppBar']
VAppBarNavIcon: typeof import("vuetify/components")['VAppBarNavIcon']
VAppBarTitle: typeof import("vuetify/components")['VAppBarTitle']
VCalendar: typeof import("vuetify/components")['VCalendar']
VAlert: typeof import("vuetify/components")['VAlert']
VAlertTitle: typeof import("vuetify/components")['VAlertTitle']
VBtnToggle: typeof import("vuetify/components")['VBtnToggle']
VBreadcrumbs: typeof import("vuetify/components")['VBreadcrumbs']
VBreadcrumbsItem: typeof import("vuetify/components")['VBreadcrumbsItem']
VBreadcrumbsDivider: typeof import("vuetify/components")['VBreadcrumbsDivider']
VBtnGroup: typeof import("vuetify/components")['VBtnGroup']
VBtn: typeof import("vuetify/components")['VBtn']
VBadge: typeof import("vuetify/components")['VBadge']
VBottomNavigation: typeof import("vuetify/components")['VBottomNavigation']
VCheckbox: typeof import("vuetify/components")['VCheckbox']
VCheckboxBtn: typeof import("vuetify/components")['VCheckboxBtn']
VCarousel: typeof import("vuetify/components")['VCarousel']
VCarouselItem: typeof import("vuetify/components")['VCarouselItem']
VChip: typeof import("vuetify/components")['VChip']
VCard: typeof import("vuetify/components")['VCard']
VCardActions: typeof import("vuetify/components")['VCardActions']
VCardItem: typeof import("vuetify/components")['VCardItem']
VCardSubtitle: typeof import("vuetify/components")['VCardSubtitle']
VCardText: typeof import("vuetify/components")['VCardText']
VCardTitle: typeof import("vuetify/components")['VCardTitle']
VBottomSheet: typeof import("vuetify/components")['VBottomSheet']
VChipGroup: typeof import("vuetify/components")['VChipGroup']
VColorPicker: typeof import("vuetify/components")['VColorPicker']
VCombobox: typeof import("vuetify/components")['VCombobox']
VCode: typeof import("vuetify/components")['VCode']
VCounter: typeof import("vuetify/components")['VCounter']
VDatePicker: typeof import("vuetify/components")['VDatePicker']
VDatePickerControls: typeof import("vuetify/components")['VDatePickerControls']
VDatePickerHeader: typeof import("vuetify/components")['VDatePickerHeader']
VDatePickerMonth: typeof import("vuetify/components")['VDatePickerMonth']
VDatePickerMonths: typeof import("vuetify/components")['VDatePickerMonths']
VDatePickerYears: typeof import("vuetify/components")['VDatePickerYears']
VDialog: typeof import("vuetify/components")['VDialog']
VDivider: typeof import("vuetify/components")['VDivider']
VFab: typeof import("vuetify/components")['VFab']
VField: typeof import("vuetify/components")['VField']
VFieldLabel: typeof import("vuetify/components")['VFieldLabel']
VEmptyState: typeof import("vuetify/components")['VEmptyState']
VExpansionPanels: typeof import("vuetify/components")['VExpansionPanels']
VExpansionPanel: typeof import("vuetify/components")['VExpansionPanel']
VExpansionPanelText: typeof import("vuetify/components")['VExpansionPanelText']
VExpansionPanelTitle: typeof import("vuetify/components")['VExpansionPanelTitle']
VDataTable: typeof import("vuetify/components")['VDataTable']
VDataTableHeaders: typeof import("vuetify/components")['VDataTableHeaders']
VDataTableFooter: typeof import("vuetify/components")['VDataTableFooter']
VDataTableRows: typeof import("vuetify/components")['VDataTableRows']
VDataTableRow: typeof import("vuetify/components")['VDataTableRow']
VDataTableVirtual: typeof import("vuetify/components")['VDataTableVirtual']
VDataTableServer: typeof import("vuetify/components")['VDataTableServer']
VHotkey: typeof import("vuetify/components")['VHotkey']
VFileInput: typeof import("vuetify/components")['VFileInput']
VFooter: typeof import("vuetify/components")['VFooter']
VImg: typeof import("vuetify/components")['VImg']
VItemGroup: typeof import("vuetify/components")['VItemGroup']
VItem: typeof import("vuetify/components")['VItem']
VIcon: typeof import("vuetify/components")['VIcon']
VComponentIcon: typeof import("vuetify/components")['VComponentIcon']
VSvgIcon: typeof import("vuetify/components")['VSvgIcon']
VLigatureIcon: typeof import("vuetify/components")['VLigatureIcon']
VClassIcon: typeof import("vuetify/components")['VClassIcon']
VInput: typeof import("vuetify/components")['VInput']
VInfiniteScroll: typeof import("vuetify/components")['VInfiniteScroll']
VKbd: typeof import("vuetify/components")['VKbd']
VMenu: typeof import("vuetify/components")['VMenu']
VNavigationDrawer: typeof import("vuetify/components")['VNavigationDrawer']
VLabel: typeof import("vuetify/components")['VLabel']
VMain: typeof import("vuetify/components")['VMain']
VMessages: typeof import("vuetify/components")['VMessages']
VOverlay: typeof import("vuetify/components")['VOverlay']
VList: typeof import("vuetify/components")['VList']
VListGroup: typeof import("vuetify/components")['VListGroup']
VListImg: typeof import("vuetify/components")['VListImg']
VListItem: typeof import("vuetify/components")['VListItem']
VListItemAction: typeof import("vuetify/components")['VListItemAction']
VListItemMedia: typeof import("vuetify/components")['VListItemMedia']
VListItemSubtitle: typeof import("vuetify/components")['VListItemSubtitle']
VListItemTitle: typeof import("vuetify/components")['VListItemTitle']
VListSubheader: typeof import("vuetify/components")['VListSubheader']
VPagination: typeof import("vuetify/components")['VPagination']
VNumberInput: typeof import("vuetify/components")['VNumberInput']
VProgressLinear: typeof import("vuetify/components")['VProgressLinear']
VOtpInput: typeof import("vuetify/components")['VOtpInput']
VRadioGroup: typeof import("vuetify/components")['VRadioGroup']
VSelectionControl: typeof import("vuetify/components")['VSelectionControl']
VProgressCircular: typeof import("vuetify/components")['VProgressCircular']
VSelect: typeof import("vuetify/components")['VSelect']
VSheet: typeof import("vuetify/components")['VSheet']
VSelectionControlGroup: typeof import("vuetify/components")['VSelectionControlGroup']
VSlideGroup: typeof import("vuetify/components")['VSlideGroup']
VSlideGroupItem: typeof import("vuetify/components")['VSlideGroupItem']
VSkeletonLoader: typeof import("vuetify/components")['VSkeletonLoader']
VRating: typeof import("vuetify/components")['VRating']
VSnackbar: typeof import("vuetify/components")['VSnackbar']
VTextarea: typeof import("vuetify/components")['VTextarea']
VSystemBar: typeof import("vuetify/components")['VSystemBar']
VSwitch: typeof import("vuetify/components")['VSwitch']
VStepper: typeof import("vuetify/components")['VStepper']
VStepperActions: typeof import("vuetify/components")['VStepperActions']
VStepperHeader: typeof import("vuetify/components")['VStepperHeader']
VStepperItem: typeof import("vuetify/components")['VStepperItem']
VStepperWindow: typeof import("vuetify/components")['VStepperWindow']
VStepperWindowItem: typeof import("vuetify/components")['VStepperWindowItem']
VSlider: typeof import("vuetify/components")['VSlider']
VTab: typeof import("vuetify/components")['VTab']
VTabs: typeof import("vuetify/components")['VTabs']
VTabsWindow: typeof import("vuetify/components")['VTabsWindow']
VTabsWindowItem: typeof import("vuetify/components")['VTabsWindowItem']
VTable: typeof import("vuetify/components")['VTable']
VTimeline: typeof import("vuetify/components")['VTimeline']
VTimelineItem: typeof import("vuetify/components")['VTimelineItem']
VTextField: typeof import("vuetify/components")['VTextField']
VTooltip: typeof import("vuetify/components")['VTooltip']
VToolbar: typeof import("vuetify/components")['VToolbar']
VToolbarTitle: typeof import("vuetify/components")['VToolbarTitle']
VToolbarItems: typeof import("vuetify/components")['VToolbarItems']
VWindow: typeof import("vuetify/components")['VWindow']
VWindowItem: typeof import("vuetify/components")['VWindowItem']
VTimePicker: typeof import("vuetify/components")['VTimePicker']
VTimePickerClock: typeof import("vuetify/components")['VTimePickerClock']
VTimePickerControls: typeof import("vuetify/components")['VTimePickerControls']
VTreeview: typeof import("vuetify/components")['VTreeview']
VTreeviewItem: typeof import("vuetify/components")['VTreeviewItem']
VTreeviewGroup: typeof import("vuetify/components")['VTreeviewGroup']
VConfirmEdit: typeof import("vuetify/components")['VConfirmEdit']
VDataIterator: typeof import("vuetify/components")['VDataIterator']
VDefaultsProvider: typeof import("vuetify/components")['VDefaultsProvider']
VContainer: typeof import("vuetify/components")['VContainer']
VCol: typeof import("vuetify/components")['VCol']
VRow: typeof import("vuetify/components")['VRow']
VSpacer: typeof import("vuetify/components")['VSpacer']
VForm: typeof import("vuetify/components")['VForm']
VAutocomplete: typeof import("vuetify/components")['VAutocomplete']
VHover: typeof import("vuetify/components")['VHover']
VLazy: typeof import("vuetify/components")['VLazy']
VLayout: typeof import("vuetify/components")['VLayout']
VLayoutItem: typeof import("vuetify/components")['VLayoutItem']
VLocaleProvider: typeof import("vuetify/components")['VLocaleProvider']
VRadio: typeof import("vuetify/components")['VRadio']
VParallax: typeof import("vuetify/components")['VParallax']
VNoSsr: typeof import("vuetify/components")['VNoSsr']
VRangeSlider: typeof import("vuetify/components")['VRangeSlider']
VResponsive: typeof import("vuetify/components")['VResponsive']
VSnackbarQueue: typeof import("vuetify/components")['VSnackbarQueue']
VSpeedDial: typeof import("vuetify/components")['VSpeedDial']
VSparkline: typeof import("vuetify/components")['VSparkline']
VVirtualScroll: typeof import("vuetify/components")['VVirtualScroll']
VThemeProvider: typeof import("vuetify/components")['VThemeProvider']
VFabTransition: typeof import("vuetify/components")['VFabTransition']
VDialogBottomTransition: typeof import("vuetify/components")['VDialogBottomTransition']
VDialogTopTransition: typeof import("vuetify/components")['VDialogTopTransition']
VFadeTransition: typeof import("vuetify/components")['VFadeTransition']
VScaleTransition: typeof import("vuetify/components")['VScaleTransition']
VScrollXTransition: typeof import("vuetify/components")['VScrollXTransition']
VScrollXReverseTransition: typeof import("vuetify/components")['VScrollXReverseTransition']
VScrollYTransition: typeof import("vuetify/components")['VScrollYTransition']
VScrollYReverseTransition: typeof import("vuetify/components")['VScrollYReverseTransition']
VSlideXTransition: typeof import("vuetify/components")['VSlideXTransition']
VSlideXReverseTransition: typeof import("vuetify/components")['VSlideXReverseTransition']
VSlideYTransition: typeof import("vuetify/components")['VSlideYTransition']
VSlideYReverseTransition: typeof import("vuetify/components")['VSlideYReverseTransition']
VExpandTransition: typeof import("vuetify/components")['VExpandTransition']
VExpandXTransition: typeof import("vuetify/components")['VExpandXTransition']
VExpandBothTransition: typeof import("vuetify/components")['VExpandBothTransition']
VDialogTransition: typeof import("vuetify/components")['VDialogTransition']
VValidation: typeof import("vuetify/components")['VValidation']
NuxtLinkLocale: typeof import("../../node_modules/@nuxtjs/i18n/dist/runtime/components/NuxtLinkLocale")['default']
SwitchLocalePathLink: typeof import("../../node_modules/@nuxtjs/i18n/dist/runtime/components/SwitchLocalePathLink")['default']
NuxtPage: typeof import("../../node_modules/nuxt/dist/pages/runtime/page")['default']
NoScript: typeof import("../../node_modules/nuxt/dist/head/runtime/components")['NoScript']
Link: typeof import("../../node_modules/nuxt/dist/head/runtime/components")['Link']
Base: typeof import("../../node_modules/nuxt/dist/head/runtime/components")['Base']
Title: typeof import("../../node_modules/nuxt/dist/head/runtime/components")['Title']
Meta: typeof import("../../node_modules/nuxt/dist/head/runtime/components")['Meta']
Style: typeof import("../../node_modules/nuxt/dist/head/runtime/components")['Style']
Head: typeof import("../../node_modules/nuxt/dist/head/runtime/components")['Head']
Html: typeof import("../../node_modules/nuxt/dist/head/runtime/components")['Html']
Body: typeof import("../../node_modules/nuxt/dist/head/runtime/components")['Body']
NuxtIsland: typeof import("../../node_modules/nuxt/dist/app/components/nuxt-island")['default']
LazyAiLogsTile: LazyComponent<typeof import("../../components/AiLogsTile.vue")['default']>
LazyFinancialTile: LazyComponent<typeof import("../../components/FinancialTile.vue")['default']>
LazySalespersonTile: LazyComponent<typeof import("../../components/SalespersonTile.vue")['default']>
LazyServiceMapTile: LazyComponent<typeof import("../../components/ServiceMapTile.vue")['default']>
LazySystemHealthTile: LazyComponent<typeof import("../../components/SystemHealthTile.vue")['default']>
LazyTileCard: LazyComponent<typeof import("../../components/TileCard.vue")['default']>
LazyTileWrapper: LazyComponent<typeof import("../../components/TileWrapper.vue")['default']>
LazyMapServiceMap: LazyComponent<typeof import("../../components/map/ServiceMap.vue")['default']>
LazyNuxtWelcome: LazyComponent<typeof import("../../node_modules/nuxt/dist/app/components/welcome.vue")['default']>
LazyNuxtLayout: LazyComponent<typeof import("../../node_modules/nuxt/dist/app/components/nuxt-layout")['default']>
LazyNuxtErrorBoundary: LazyComponent<typeof import("../../node_modules/nuxt/dist/app/components/nuxt-error-boundary.vue")['default']>
LazyClientOnly: LazyComponent<typeof import("../../node_modules/nuxt/dist/app/components/client-only")['default']>
LazyDevOnly: LazyComponent<typeof import("../../node_modules/nuxt/dist/app/components/dev-only")['default']>
LazyServerPlaceholder: LazyComponent<typeof import("../../node_modules/nuxt/dist/app/components/server-placeholder")['default']>
LazyNuxtLink: LazyComponent<typeof import("../../node_modules/nuxt/dist/app/components/nuxt-link")['default']>
LazyNuxtLoadingIndicator: LazyComponent<typeof import("../../node_modules/nuxt/dist/app/components/nuxt-loading-indicator")['default']>
LazyNuxtTime: LazyComponent<typeof import("../../node_modules/nuxt/dist/app/components/nuxt-time.vue")['default']>
LazyNuxtRouteAnnouncer: LazyComponent<typeof import("../../node_modules/nuxt/dist/app/components/nuxt-route-announcer")['default']>
LazyNuxtImg: LazyComponent<typeof import("../../node_modules/nuxt/dist/app/components/nuxt-stubs")['NuxtImg']>
LazyNuxtPicture: LazyComponent<typeof import("../../node_modules/nuxt/dist/app/components/nuxt-stubs")['NuxtPicture']>
LazyVAvatar: LazyComponent<typeof import("vuetify/components")['VAvatar']>
LazyVBanner: LazyComponent<typeof import("vuetify/components")['VBanner']>
LazyVBannerActions: LazyComponent<typeof import("vuetify/components")['VBannerActions']>
LazyVBannerText: LazyComponent<typeof import("vuetify/components")['VBannerText']>
LazyVApp: LazyComponent<typeof import("vuetify/components")['VApp']>
LazyVAppBar: LazyComponent<typeof import("vuetify/components")['VAppBar']>
LazyVAppBarNavIcon: LazyComponent<typeof import("vuetify/components")['VAppBarNavIcon']>
LazyVAppBarTitle: LazyComponent<typeof import("vuetify/components")['VAppBarTitle']>
LazyVCalendar: LazyComponent<typeof import("vuetify/components")['VCalendar']>
LazyVAlert: LazyComponent<typeof import("vuetify/components")['VAlert']>
LazyVAlertTitle: LazyComponent<typeof import("vuetify/components")['VAlertTitle']>
LazyVBtnToggle: LazyComponent<typeof import("vuetify/components")['VBtnToggle']>
LazyVBreadcrumbs: LazyComponent<typeof import("vuetify/components")['VBreadcrumbs']>
LazyVBreadcrumbsItem: LazyComponent<typeof import("vuetify/components")['VBreadcrumbsItem']>
LazyVBreadcrumbsDivider: LazyComponent<typeof import("vuetify/components")['VBreadcrumbsDivider']>
LazyVBtnGroup: LazyComponent<typeof import("vuetify/components")['VBtnGroup']>
LazyVBtn: LazyComponent<typeof import("vuetify/components")['VBtn']>
LazyVBadge: LazyComponent<typeof import("vuetify/components")['VBadge']>
LazyVBottomNavigation: LazyComponent<typeof import("vuetify/components")['VBottomNavigation']>
LazyVCheckbox: LazyComponent<typeof import("vuetify/components")['VCheckbox']>
LazyVCheckboxBtn: LazyComponent<typeof import("vuetify/components")['VCheckboxBtn']>
LazyVCarousel: LazyComponent<typeof import("vuetify/components")['VCarousel']>
LazyVCarouselItem: LazyComponent<typeof import("vuetify/components")['VCarouselItem']>
LazyVChip: LazyComponent<typeof import("vuetify/components")['VChip']>
LazyVCard: LazyComponent<typeof import("vuetify/components")['VCard']>
LazyVCardActions: LazyComponent<typeof import("vuetify/components")['VCardActions']>
LazyVCardItem: LazyComponent<typeof import("vuetify/components")['VCardItem']>
LazyVCardSubtitle: LazyComponent<typeof import("vuetify/components")['VCardSubtitle']>
LazyVCardText: LazyComponent<typeof import("vuetify/components")['VCardText']>
LazyVCardTitle: LazyComponent<typeof import("vuetify/components")['VCardTitle']>
LazyVBottomSheet: LazyComponent<typeof import("vuetify/components")['VBottomSheet']>
LazyVChipGroup: LazyComponent<typeof import("vuetify/components")['VChipGroup']>
LazyVColorPicker: LazyComponent<typeof import("vuetify/components")['VColorPicker']>
LazyVCombobox: LazyComponent<typeof import("vuetify/components")['VCombobox']>
LazyVCode: LazyComponent<typeof import("vuetify/components")['VCode']>
LazyVCounter: LazyComponent<typeof import("vuetify/components")['VCounter']>
LazyVDatePicker: LazyComponent<typeof import("vuetify/components")['VDatePicker']>
LazyVDatePickerControls: LazyComponent<typeof import("vuetify/components")['VDatePickerControls']>
LazyVDatePickerHeader: LazyComponent<typeof import("vuetify/components")['VDatePickerHeader']>
LazyVDatePickerMonth: LazyComponent<typeof import("vuetify/components")['VDatePickerMonth']>
LazyVDatePickerMonths: LazyComponent<typeof import("vuetify/components")['VDatePickerMonths']>
LazyVDatePickerYears: LazyComponent<typeof import("vuetify/components")['VDatePickerYears']>
LazyVDialog: LazyComponent<typeof import("vuetify/components")['VDialog']>
LazyVDivider: LazyComponent<typeof import("vuetify/components")['VDivider']>
LazyVFab: LazyComponent<typeof import("vuetify/components")['VFab']>
LazyVField: LazyComponent<typeof import("vuetify/components")['VField']>
LazyVFieldLabel: LazyComponent<typeof import("vuetify/components")['VFieldLabel']>
LazyVEmptyState: LazyComponent<typeof import("vuetify/components")['VEmptyState']>
LazyVExpansionPanels: LazyComponent<typeof import("vuetify/components")['VExpansionPanels']>
LazyVExpansionPanel: LazyComponent<typeof import("vuetify/components")['VExpansionPanel']>
LazyVExpansionPanelText: LazyComponent<typeof import("vuetify/components")['VExpansionPanelText']>
LazyVExpansionPanelTitle: LazyComponent<typeof import("vuetify/components")['VExpansionPanelTitle']>
LazyVDataTable: LazyComponent<typeof import("vuetify/components")['VDataTable']>
LazyVDataTableHeaders: LazyComponent<typeof import("vuetify/components")['VDataTableHeaders']>
LazyVDataTableFooter: LazyComponent<typeof import("vuetify/components")['VDataTableFooter']>
LazyVDataTableRows: LazyComponent<typeof import("vuetify/components")['VDataTableRows']>
LazyVDataTableRow: LazyComponent<typeof import("vuetify/components")['VDataTableRow']>
LazyVDataTableVirtual: LazyComponent<typeof import("vuetify/components")['VDataTableVirtual']>
LazyVDataTableServer: LazyComponent<typeof import("vuetify/components")['VDataTableServer']>
LazyVHotkey: LazyComponent<typeof import("vuetify/components")['VHotkey']>
LazyVFileInput: LazyComponent<typeof import("vuetify/components")['VFileInput']>
LazyVFooter: LazyComponent<typeof import("vuetify/components")['VFooter']>
LazyVImg: LazyComponent<typeof import("vuetify/components")['VImg']>
LazyVItemGroup: LazyComponent<typeof import("vuetify/components")['VItemGroup']>
LazyVItem: LazyComponent<typeof import("vuetify/components")['VItem']>
LazyVIcon: LazyComponent<typeof import("vuetify/components")['VIcon']>
LazyVComponentIcon: LazyComponent<typeof import("vuetify/components")['VComponentIcon']>
LazyVSvgIcon: LazyComponent<typeof import("vuetify/components")['VSvgIcon']>
LazyVLigatureIcon: LazyComponent<typeof import("vuetify/components")['VLigatureIcon']>
LazyVClassIcon: LazyComponent<typeof import("vuetify/components")['VClassIcon']>
LazyVInput: LazyComponent<typeof import("vuetify/components")['VInput']>
LazyVInfiniteScroll: LazyComponent<typeof import("vuetify/components")['VInfiniteScroll']>
LazyVKbd: LazyComponent<typeof import("vuetify/components")['VKbd']>
LazyVMenu: LazyComponent<typeof import("vuetify/components")['VMenu']>
LazyVNavigationDrawer: LazyComponent<typeof import("vuetify/components")['VNavigationDrawer']>
LazyVLabel: LazyComponent<typeof import("vuetify/components")['VLabel']>
LazyVMain: LazyComponent<typeof import("vuetify/components")['VMain']>
LazyVMessages: LazyComponent<typeof import("vuetify/components")['VMessages']>
LazyVOverlay: LazyComponent<typeof import("vuetify/components")['VOverlay']>
LazyVList: LazyComponent<typeof import("vuetify/components")['VList']>
LazyVListGroup: LazyComponent<typeof import("vuetify/components")['VListGroup']>
LazyVListImg: LazyComponent<typeof import("vuetify/components")['VListImg']>
LazyVListItem: LazyComponent<typeof import("vuetify/components")['VListItem']>
LazyVListItemAction: LazyComponent<typeof import("vuetify/components")['VListItemAction']>
LazyVListItemMedia: LazyComponent<typeof import("vuetify/components")['VListItemMedia']>
LazyVListItemSubtitle: LazyComponent<typeof import("vuetify/components")['VListItemSubtitle']>
LazyVListItemTitle: LazyComponent<typeof import("vuetify/components")['VListItemTitle']>
LazyVListSubheader: LazyComponent<typeof import("vuetify/components")['VListSubheader']>
LazyVPagination: LazyComponent<typeof import("vuetify/components")['VPagination']>
LazyVNumberInput: LazyComponent<typeof import("vuetify/components")['VNumberInput']>
LazyVProgressLinear: LazyComponent<typeof import("vuetify/components")['VProgressLinear']>
LazyVOtpInput: LazyComponent<typeof import("vuetify/components")['VOtpInput']>
LazyVRadioGroup: LazyComponent<typeof import("vuetify/components")['VRadioGroup']>
LazyVSelectionControl: LazyComponent<typeof import("vuetify/components")['VSelectionControl']>
LazyVProgressCircular: LazyComponent<typeof import("vuetify/components")['VProgressCircular']>
LazyVSelect: LazyComponent<typeof import("vuetify/components")['VSelect']>
LazyVSheet: LazyComponent<typeof import("vuetify/components")['VSheet']>
LazyVSelectionControlGroup: LazyComponent<typeof import("vuetify/components")['VSelectionControlGroup']>
LazyVSlideGroup: LazyComponent<typeof import("vuetify/components")['VSlideGroup']>
LazyVSlideGroupItem: LazyComponent<typeof import("vuetify/components")['VSlideGroupItem']>
LazyVSkeletonLoader: LazyComponent<typeof import("vuetify/components")['VSkeletonLoader']>
LazyVRating: LazyComponent<typeof import("vuetify/components")['VRating']>
LazyVSnackbar: LazyComponent<typeof import("vuetify/components")['VSnackbar']>
LazyVTextarea: LazyComponent<typeof import("vuetify/components")['VTextarea']>
LazyVSystemBar: LazyComponent<typeof import("vuetify/components")['VSystemBar']>
LazyVSwitch: LazyComponent<typeof import("vuetify/components")['VSwitch']>
LazyVStepper: LazyComponent<typeof import("vuetify/components")['VStepper']>
LazyVStepperActions: LazyComponent<typeof import("vuetify/components")['VStepperActions']>
LazyVStepperHeader: LazyComponent<typeof import("vuetify/components")['VStepperHeader']>
LazyVStepperItem: LazyComponent<typeof import("vuetify/components")['VStepperItem']>
LazyVStepperWindow: LazyComponent<typeof import("vuetify/components")['VStepperWindow']>
LazyVStepperWindowItem: LazyComponent<typeof import("vuetify/components")['VStepperWindowItem']>
LazyVSlider: LazyComponent<typeof import("vuetify/components")['VSlider']>
LazyVTab: LazyComponent<typeof import("vuetify/components")['VTab']>
LazyVTabs: LazyComponent<typeof import("vuetify/components")['VTabs']>
LazyVTabsWindow: LazyComponent<typeof import("vuetify/components")['VTabsWindow']>
LazyVTabsWindowItem: LazyComponent<typeof import("vuetify/components")['VTabsWindowItem']>
LazyVTable: LazyComponent<typeof import("vuetify/components")['VTable']>
LazyVTimeline: LazyComponent<typeof import("vuetify/components")['VTimeline']>
LazyVTimelineItem: LazyComponent<typeof import("vuetify/components")['VTimelineItem']>
LazyVTextField: LazyComponent<typeof import("vuetify/components")['VTextField']>
LazyVTooltip: LazyComponent<typeof import("vuetify/components")['VTooltip']>
LazyVToolbar: LazyComponent<typeof import("vuetify/components")['VToolbar']>
LazyVToolbarTitle: LazyComponent<typeof import("vuetify/components")['VToolbarTitle']>
LazyVToolbarItems: LazyComponent<typeof import("vuetify/components")['VToolbarItems']>
LazyVWindow: LazyComponent<typeof import("vuetify/components")['VWindow']>
LazyVWindowItem: LazyComponent<typeof import("vuetify/components")['VWindowItem']>
LazyVTimePicker: LazyComponent<typeof import("vuetify/components")['VTimePicker']>
LazyVTimePickerClock: LazyComponent<typeof import("vuetify/components")['VTimePickerClock']>
LazyVTimePickerControls: LazyComponent<typeof import("vuetify/components")['VTimePickerControls']>
LazyVTreeview: LazyComponent<typeof import("vuetify/components")['VTreeview']>
LazyVTreeviewItem: LazyComponent<typeof import("vuetify/components")['VTreeviewItem']>
LazyVTreeviewGroup: LazyComponent<typeof import("vuetify/components")['VTreeviewGroup']>
LazyVConfirmEdit: LazyComponent<typeof import("vuetify/components")['VConfirmEdit']>
LazyVDataIterator: LazyComponent<typeof import("vuetify/components")['VDataIterator']>
LazyVDefaultsProvider: LazyComponent<typeof import("vuetify/components")['VDefaultsProvider']>
LazyVContainer: LazyComponent<typeof import("vuetify/components")['VContainer']>
LazyVCol: LazyComponent<typeof import("vuetify/components")['VCol']>
LazyVRow: LazyComponent<typeof import("vuetify/components")['VRow']>
LazyVSpacer: LazyComponent<typeof import("vuetify/components")['VSpacer']>
LazyVForm: LazyComponent<typeof import("vuetify/components")['VForm']>
LazyVAutocomplete: LazyComponent<typeof import("vuetify/components")['VAutocomplete']>
LazyVHover: LazyComponent<typeof import("vuetify/components")['VHover']>
LazyVLazy: LazyComponent<typeof import("vuetify/components")['VLazy']>
LazyVLayout: LazyComponent<typeof import("vuetify/components")['VLayout']>
LazyVLayoutItem: LazyComponent<typeof import("vuetify/components")['VLayoutItem']>
LazyVLocaleProvider: LazyComponent<typeof import("vuetify/components")['VLocaleProvider']>
LazyVRadio: LazyComponent<typeof import("vuetify/components")['VRadio']>
LazyVParallax: LazyComponent<typeof import("vuetify/components")['VParallax']>
LazyVNoSsr: LazyComponent<typeof import("vuetify/components")['VNoSsr']>
LazyVRangeSlider: LazyComponent<typeof import("vuetify/components")['VRangeSlider']>
LazyVResponsive: LazyComponent<typeof import("vuetify/components")['VResponsive']>
LazyVSnackbarQueue: LazyComponent<typeof import("vuetify/components")['VSnackbarQueue']>
LazyVSpeedDial: LazyComponent<typeof import("vuetify/components")['VSpeedDial']>
LazyVSparkline: LazyComponent<typeof import("vuetify/components")['VSparkline']>
LazyVVirtualScroll: LazyComponent<typeof import("vuetify/components")['VVirtualScroll']>
LazyVThemeProvider: LazyComponent<typeof import("vuetify/components")['VThemeProvider']>
LazyVFabTransition: LazyComponent<typeof import("vuetify/components")['VFabTransition']>
LazyVDialogBottomTransition: LazyComponent<typeof import("vuetify/components")['VDialogBottomTransition']>
LazyVDialogTopTransition: LazyComponent<typeof import("vuetify/components")['VDialogTopTransition']>
LazyVFadeTransition: LazyComponent<typeof import("vuetify/components")['VFadeTransition']>
LazyVScaleTransition: LazyComponent<typeof import("vuetify/components")['VScaleTransition']>
LazyVScrollXTransition: LazyComponent<typeof import("vuetify/components")['VScrollXTransition']>
LazyVScrollXReverseTransition: LazyComponent<typeof import("vuetify/components")['VScrollXReverseTransition']>
LazyVScrollYTransition: LazyComponent<typeof import("vuetify/components")['VScrollYTransition']>
LazyVScrollYReverseTransition: LazyComponent<typeof import("vuetify/components")['VScrollYReverseTransition']>
LazyVSlideXTransition: LazyComponent<typeof import("vuetify/components")['VSlideXTransition']>
LazyVSlideXReverseTransition: LazyComponent<typeof import("vuetify/components")['VSlideXReverseTransition']>
LazyVSlideYTransition: LazyComponent<typeof import("vuetify/components")['VSlideYTransition']>
LazyVSlideYReverseTransition: LazyComponent<typeof import("vuetify/components")['VSlideYReverseTransition']>
LazyVExpandTransition: LazyComponent<typeof import("vuetify/components")['VExpandTransition']>
LazyVExpandXTransition: LazyComponent<typeof import("vuetify/components")['VExpandXTransition']>
LazyVExpandBothTransition: LazyComponent<typeof import("vuetify/components")['VExpandBothTransition']>
LazyVDialogTransition: LazyComponent<typeof import("vuetify/components")['VDialogTransition']>
LazyVValidation: LazyComponent<typeof import("vuetify/components")['VValidation']>
LazyNuxtLinkLocale: LazyComponent<typeof import("../../node_modules/@nuxtjs/i18n/dist/runtime/components/NuxtLinkLocale")['default']>
LazySwitchLocalePathLink: LazyComponent<typeof import("../../node_modules/@nuxtjs/i18n/dist/runtime/components/SwitchLocalePathLink")['default']>
LazyNuxtPage: LazyComponent<typeof import("../../node_modules/nuxt/dist/pages/runtime/page")['default']>
LazyNoScript: LazyComponent<typeof import("../../node_modules/nuxt/dist/head/runtime/components")['NoScript']>
LazyLink: LazyComponent<typeof import("../../node_modules/nuxt/dist/head/runtime/components")['Link']>
LazyBase: LazyComponent<typeof import("../../node_modules/nuxt/dist/head/runtime/components")['Base']>
LazyTitle: LazyComponent<typeof import("../../node_modules/nuxt/dist/head/runtime/components")['Title']>
LazyMeta: LazyComponent<typeof import("../../node_modules/nuxt/dist/head/runtime/components")['Meta']>
LazyStyle: LazyComponent<typeof import("../../node_modules/nuxt/dist/head/runtime/components")['Style']>
LazyHead: LazyComponent<typeof import("../../node_modules/nuxt/dist/head/runtime/components")['Head']>
LazyHtml: LazyComponent<typeof import("../../node_modules/nuxt/dist/head/runtime/components")['Html']>
LazyBody: LazyComponent<typeof import("../../node_modules/nuxt/dist/head/runtime/components")['Body']>
LazyNuxtIsland: LazyComponent<typeof import("../../node_modules/nuxt/dist/app/components/nuxt-island")['default']>
}
declare module 'vue' {
export interface GlobalComponents extends _GlobalComponents { }
}
export {}

View File

@@ -1,20 +0,0 @@
// Generated by @nuxtjs/i18n
import type { ExportedGlobalComposer, Composer } from 'vue-i18n'
import type { NuxtI18nRoutingCustomProperties, ComposerCustomProperties } from '../../node_modules/@nuxtjs/i18n/dist/runtime/types.ts'
import type { Strategies, Directions, LocaleObject } from '../../node_modules/@nuxtjs/i18n/dist/types.d.ts'
declare module 'vue-i18n' {
interface ComposerCustom extends ComposerCustomProperties<LocaleObject[]> {}
interface ExportedGlobalComposer extends NuxtI18nRoutingCustomProperties<LocaleObject[]> {}
interface VueI18n extends NuxtI18nRoutingCustomProperties<LocaleObject[]> {}
}
declare module '#app' {
interface NuxtApp {
$i18n: ExportedGlobalComposer & Composer & NuxtI18nRoutingCustomProperties<LocaleObject[]>
}
}
export {}

View File

@@ -1,449 +0,0 @@
// Generated by auto imports
export {}
declare global {
const AdminTiles: typeof import('../../composables/useRBAC').AdminTiles
const Role: typeof import('../../composables/useRBAC').Role
const RoleRank: typeof import('../../composables/useRBAC').RoleRank
const ScopeLevel: typeof import('../../composables/useRBAC').ScopeLevel
const abortNavigation: typeof import('../../node_modules/nuxt/dist/app/composables/router').abortNavigation
const acceptHMRUpdate: typeof import('../../node_modules/@pinia/nuxt/dist/runtime/composables').acceptHMRUpdate
const addRouteMiddleware: typeof import('../../node_modules/nuxt/dist/app/composables/router').addRouteMiddleware
const callOnce: typeof import('../../node_modules/nuxt/dist/app/composables/once').callOnce
const cancelIdleCallback: typeof import('../../node_modules/nuxt/dist/app/compat/idle-callback').cancelIdleCallback
const clearError: typeof import('../../node_modules/nuxt/dist/app/composables/error').clearError
const clearNuxtData: typeof import('../../node_modules/nuxt/dist/app/composables/asyncData').clearNuxtData
const clearNuxtState: typeof import('../../node_modules/nuxt/dist/app/composables/state').clearNuxtState
const computed: typeof import('vue').computed
const createError: typeof import('../../node_modules/nuxt/dist/app/composables/error').createError
const customRef: typeof import('vue').customRef
const defineAppConfig: typeof import('../../node_modules/nuxt/dist/app/nuxt').defineAppConfig
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
const defineComponent: typeof import('vue').defineComponent
const defineI18nConfig: typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index').defineI18nConfig
const defineI18nLocale: typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index').defineI18nLocale
const defineI18nRoute: typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index').defineI18nRoute
const defineLazyHydrationComponent: typeof import('../../node_modules/nuxt/dist/app/composables/lazy-hydration').defineLazyHydrationComponent
const defineNuxtComponent: typeof import('../../node_modules/nuxt/dist/app/composables/component').defineNuxtComponent
const defineNuxtLink: typeof import('../../node_modules/nuxt/dist/app/components/nuxt-link').defineNuxtLink
const defineNuxtPlugin: typeof import('../../node_modules/nuxt/dist/app/nuxt').defineNuxtPlugin
const defineNuxtRouteMiddleware: typeof import('../../node_modules/nuxt/dist/app/composables/router').defineNuxtRouteMiddleware
const definePageMeta: typeof import('../../node_modules/nuxt/dist/pages/runtime/composables').definePageMeta
const definePayloadPlugin: typeof import('../../node_modules/nuxt/dist/app/nuxt').definePayloadPlugin
const definePayloadReducer: typeof import('../../node_modules/nuxt/dist/app/composables/payload').definePayloadReducer
const definePayloadReviver: typeof import('../../node_modules/nuxt/dist/app/composables/payload').definePayloadReviver
const defineStore: typeof import('../../node_modules/@pinia/nuxt/dist/runtime/composables').defineStore
const effect: typeof import('vue').effect
const effectScope: typeof import('vue').effectScope
const getAppManifest: typeof import('../../node_modules/nuxt/dist/app/composables/manifest').getAppManifest
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const getRouteRules: typeof import('../../node_modules/nuxt/dist/app/composables/manifest').getRouteRules
const h: typeof import('vue').h
const hasInjectionContext: typeof import('vue').hasInjectionContext
const inject: typeof import('vue').inject
const injectHead: typeof import('../../node_modules/nuxt/dist/app/composables/head').injectHead
const isNuxtError: typeof import('../../node_modules/nuxt/dist/app/composables/error').isNuxtError
const isPrerendered: typeof import('../../node_modules/nuxt/dist/app/composables/payload').isPrerendered
const isProxy: typeof import('vue').isProxy
const isReactive: typeof import('vue').isReactive
const isReadonly: typeof import('vue').isReadonly
const isRef: typeof import('vue').isRef
const isShallow: typeof import('vue').isShallow
const isVue2: typeof import('../../node_modules/nuxt/dist/app/compat/vue-demi').isVue2
const isVue3: typeof import('../../node_modules/nuxt/dist/app/compat/vue-demi').isVue3
const loadPayload: typeof import('../../node_modules/nuxt/dist/app/composables/payload').loadPayload
const markRaw: typeof import('vue').markRaw
const navigateTo: typeof import('../../node_modules/nuxt/dist/app/composables/router').navigateTo
const nextTick: typeof import('vue').nextTick
const onActivated: typeof import('vue').onActivated
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
const onDeactivated: typeof import('vue').onDeactivated
const onErrorCaptured: typeof import('vue').onErrorCaptured
const onMounted: typeof import('vue').onMounted
const onNuxtReady: typeof import('../../node_modules/nuxt/dist/app/composables/ready').onNuxtReady
const onPrehydrate: typeof import('../../node_modules/nuxt/dist/app/composables/ssr').onPrehydrate
const onRenderTracked: typeof import('vue').onRenderTracked
const onRenderTriggered: typeof import('vue').onRenderTriggered
const onScopeDispose: typeof import('vue').onScopeDispose
const onServerPrefetch: typeof import('vue').onServerPrefetch
const onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const prefetchComponents: typeof import('../../node_modules/nuxt/dist/app/composables/preload').prefetchComponents
const preloadComponents: typeof import('../../node_modules/nuxt/dist/app/composables/preload').preloadComponents
const preloadPayload: typeof import('../../node_modules/nuxt/dist/app/composables/payload').preloadPayload
const preloadRouteComponents: typeof import('../../node_modules/nuxt/dist/app/composables/preload').preloadRouteComponents
const prerenderRoutes: typeof import('../../node_modules/nuxt/dist/app/composables/ssr').prerenderRoutes
const provide: typeof import('vue').provide
const proxyRefs: typeof import('vue').proxyRefs
const reactive: typeof import('vue').reactive
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const refreshCookie: typeof import('../../node_modules/nuxt/dist/app/composables/cookie').refreshCookie
const refreshNuxtData: typeof import('../../node_modules/nuxt/dist/app/composables/asyncData').refreshNuxtData
const reloadNuxtApp: typeof import('../../node_modules/nuxt/dist/app/composables/chunk').reloadNuxtApp
const requestIdleCallback: typeof import('../../node_modules/nuxt/dist/app/compat/idle-callback').requestIdleCallback
const resolveComponent: typeof import('vue').resolveComponent
const setInterval: typeof import('../../node_modules/nuxt/dist/app/compat/interval').setInterval
const setPageLayout: typeof import('../../node_modules/nuxt/dist/app/composables/router').setPageLayout
const setResponseStatus: typeof import('../../node_modules/nuxt/dist/app/composables/ssr').setResponseStatus
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const showError: typeof import('../../node_modules/nuxt/dist/app/composables/error').showError
const storeToRefs: typeof import('../../node_modules/@pinia/nuxt/dist/runtime/composables').storeToRefs
const toRaw: typeof import('vue').toRaw
const toRef: typeof import('vue').toRef
const toRefs: typeof import('vue').toRefs
const toValue: typeof import('vue').toValue
const triggerRef: typeof import('vue').triggerRef
const tryUseNuxtApp: typeof import('../../node_modules/nuxt/dist/app/nuxt').tryUseNuxtApp
const unref: typeof import('vue').unref
const updateAppConfig: typeof import('../../node_modules/nuxt/dist/app/config').updateAppConfig
const useAppConfig: typeof import('../../node_modules/nuxt/dist/app/config').useAppConfig
const useAsyncData: typeof import('../../node_modules/nuxt/dist/app/composables/asyncData').useAsyncData
const useAttrs: typeof import('vue').useAttrs
const useAuthStore: typeof import('../../stores/auth').useAuthStore
const useBrowserLocale: typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index').useBrowserLocale
const useCookie: typeof import('../../node_modules/nuxt/dist/app/composables/cookie').useCookie
const useCookieLocale: typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index').useCookieLocale
const useCssModule: typeof import('vue').useCssModule
const useCssVars: typeof import('vue').useCssVars
const useDefaults: typeof import('vuetify').useDefaults
const useDisplay: typeof import('vuetify').useDisplay
const useError: typeof import('../../node_modules/nuxt/dist/app/composables/error').useError
const useFetch: typeof import('../../node_modules/nuxt/dist/app/composables/fetch').useFetch
const useHead: typeof import('../../node_modules/nuxt/dist/app/composables/head').useHead
const useHeadSafe: typeof import('../../node_modules/nuxt/dist/app/composables/head').useHeadSafe
const useHealthMonitor: typeof import('../../composables/useHealthMonitor').useHealthMonitor
const useHydration: typeof import('../../node_modules/nuxt/dist/app/composables/hydrate').useHydration
const useI18n: typeof import('../../node_modules/vue-i18n/dist/vue-i18n').useI18n
const useId: typeof import('vue').useId
const useLayout: typeof import('vuetify').useLayout
const useLazyAsyncData: typeof import('../../node_modules/nuxt/dist/app/composables/asyncData').useLazyAsyncData
const useLazyFetch: typeof import('../../node_modules/nuxt/dist/app/composables/fetch').useLazyFetch
const useLink: typeof import('vue-router').useLink
const useLoadingIndicator: typeof import('../../node_modules/nuxt/dist/app/composables/loading-indicator').useLoadingIndicator
const useLocale: typeof import('vuetify').useLocale
const useLocaleHead: typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index').useLocaleHead
const useLocalePath: typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index').useLocalePath
const useLocaleRoute: typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index').useLocaleRoute
const useModel: typeof import('vue').useModel
const useNuxtApp: typeof import('../../node_modules/nuxt/dist/app/nuxt').useNuxtApp
const useNuxtData: typeof import('../../node_modules/nuxt/dist/app/composables/asyncData').useNuxtData
const usePinia: typeof import('../../node_modules/@pinia/nuxt/dist/runtime/composables').usePinia
const usePolling: typeof import('../../composables/usePolling').default
const usePreviewMode: typeof import('../../node_modules/nuxt/dist/app/composables/preview').usePreviewMode
const useRBAC: typeof import('../../composables/useRBAC').useRBAC
const useRequestEvent: typeof import('../../node_modules/nuxt/dist/app/composables/ssr').useRequestEvent
const useRequestFetch: typeof import('../../node_modules/nuxt/dist/app/composables/ssr').useRequestFetch
const useRequestHeader: typeof import('../../node_modules/nuxt/dist/app/composables/ssr').useRequestHeader
const useRequestHeaders: typeof import('../../node_modules/nuxt/dist/app/composables/ssr').useRequestHeaders
const useRequestURL: typeof import('../../node_modules/nuxt/dist/app/composables/url').useRequestURL
const useResponseHeader: typeof import('../../node_modules/nuxt/dist/app/composables/ssr').useResponseHeader
const useRoute: typeof import('../../node_modules/nuxt/dist/app/composables/router').useRoute
const useRouteAnnouncer: typeof import('../../node_modules/nuxt/dist/app/composables/route-announcer').useRouteAnnouncer
const useRouteBaseName: typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index').useRouteBaseName
const useRouter: typeof import('../../node_modules/nuxt/dist/app/composables/router').useRouter
const useRtl: typeof import('vuetify').useRtl
const useRuntimeConfig: typeof import('../../node_modules/nuxt/dist/app/nuxt').useRuntimeConfig
const useRuntimeHook: typeof import('../../node_modules/nuxt/dist/app/composables/runtime-hook').useRuntimeHook
const useScript: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScript
const useScriptClarity: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptClarity
const useScriptCloudflareWebAnalytics: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptCloudflareWebAnalytics
const useScriptCrisp: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptCrisp
const useScriptDatabuddyAnalytics: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptDatabuddyAnalytics
const useScriptEventPage: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptEventPage
const useScriptFathomAnalytics: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptFathomAnalytics
const useScriptGoogleAdsense: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptGoogleAdsense
const useScriptGoogleAnalytics: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptGoogleAnalytics
const useScriptGoogleMaps: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptGoogleMaps
const useScriptGoogleTagManager: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptGoogleTagManager
const useScriptHotjar: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptHotjar
const useScriptIntercom: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptIntercom
const useScriptLemonSqueezy: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptLemonSqueezy
const useScriptMatomoAnalytics: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptMatomoAnalytics
const useScriptMetaPixel: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptMetaPixel
const useScriptNpm: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptNpm
const useScriptPayPal: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptPayPal
const useScriptPlausibleAnalytics: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptPlausibleAnalytics
const useScriptRedditPixel: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptRedditPixel
const useScriptRybbitAnalytics: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptRybbitAnalytics
const useScriptSegment: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptSegment
const useScriptSnapchatPixel: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptSnapchatPixel
const useScriptStripe: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptStripe
const useScriptTriggerConsent: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptTriggerConsent
const useScriptTriggerElement: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptTriggerElement
const useScriptUmamiAnalytics: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptUmamiAnalytics
const useScriptVimeoPlayer: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptVimeoPlayer
const useScriptXPixel: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptXPixel
const useScriptYouTubePlayer: typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs').useScriptYouTubePlayer
const useSeoMeta: typeof import('../../node_modules/nuxt/dist/app/composables/head').useSeoMeta
const useServerHead: typeof import('../../node_modules/nuxt/dist/app/composables/head').useServerHead
const useServerHeadSafe: typeof import('../../node_modules/nuxt/dist/app/composables/head').useServerHeadSafe
const useServerSeoMeta: typeof import('../../node_modules/nuxt/dist/app/composables/head').useServerSeoMeta
const useServiceMap: typeof import('../../composables/useServiceMap').useServiceMap
const useSetI18nParams: typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index').useSetI18nParams
const useShadowRoot: typeof import('vue').useShadowRoot
const useSlots: typeof import('vue').useSlots
const useState: typeof import('../../node_modules/nuxt/dist/app/composables/state').useState
const useSwitchLocalePath: typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index').useSwitchLocalePath
const useTemplateRef: typeof import('vue').useTemplateRef
const useTheme: typeof import('vuetify').useTheme
const useTileStore: typeof import('../../stores/tiles').useTileStore
const useTransitionState: typeof import('vue').useTransitionState
const useUserManagement: typeof import('../../composables/useUserManagement').default
const watch: typeof import('vue').watch
const watchEffect: typeof import('vue').watchEffect
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
const withCtx: typeof import('vue').withCtx
const withDirectives: typeof import('vue').withDirectives
const withKeys: typeof import('vue').withKeys
const withMemo: typeof import('vue').withMemo
const withModifiers: typeof import('vue').withModifiers
const withScopeId: typeof import('vue').withScopeId
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
// @ts-ignore
export type { HealthMetrics, SystemAlert, HealthMonitorState } from '../../composables/useHealthMonitor'
import('../../composables/useHealthMonitor')
// @ts-ignore
export type { PollingOptions, PollingState } from '../../composables/usePolling'
import('../../composables/usePolling')
// @ts-ignore
export type { Role, ScopeLevel, TilePermission } from '../../composables/useRBAC'
import('../../composables/useRBAC')
// @ts-ignore
export type { Service, Scope } from '../../composables/useServiceMap'
import('../../composables/useServiceMap')
// @ts-ignore
export type { UpdateUserRoleRequest, UserManagementState } from '../../composables/useUserManagement'
import('../../composables/useUserManagement')
// @ts-ignore
export type { JwtPayload, User } from '../../stores/auth'
import('../../stores/auth')
// @ts-ignore
export type { UserTilePreference } from '../../stores/tiles'
import('../../stores/tiles')
}
// for vue template auto import
import { UnwrapRef } from 'vue'
declare module 'vue' {
interface ComponentCustomProperties {
readonly AdminTiles: UnwrapRef<typeof import('../../composables/useRBAC')['AdminTiles']>
readonly Role: UnwrapRef<typeof import('../../composables/useRBAC')['Role']>
readonly RoleRank: UnwrapRef<typeof import('../../composables/useRBAC')['RoleRank']>
readonly ScopeLevel: UnwrapRef<typeof import('../../composables/useRBAC')['ScopeLevel']>
readonly abortNavigation: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/router')['abortNavigation']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('../../node_modules/@pinia/nuxt/dist/runtime/composables')['acceptHMRUpdate']>
readonly addRouteMiddleware: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/router')['addRouteMiddleware']>
readonly callOnce: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/once')['callOnce']>
readonly cancelIdleCallback: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/compat/idle-callback')['cancelIdleCallback']>
readonly clearError: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/error')['clearError']>
readonly clearNuxtData: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/asyncData')['clearNuxtData']>
readonly clearNuxtState: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/state')['clearNuxtState']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly createError: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/error')['createError']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly defineAppConfig: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/nuxt')['defineAppConfig']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly defineI18nConfig: UnwrapRef<typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['defineI18nConfig']>
readonly defineI18nLocale: UnwrapRef<typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['defineI18nLocale']>
readonly defineI18nRoute: UnwrapRef<typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['defineI18nRoute']>
readonly defineLazyHydrationComponent: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/lazy-hydration')['defineLazyHydrationComponent']>
readonly defineNuxtComponent: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/component')['defineNuxtComponent']>
readonly defineNuxtLink: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/components/nuxt-link')['defineNuxtLink']>
readonly defineNuxtPlugin: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/nuxt')['defineNuxtPlugin']>
readonly defineNuxtRouteMiddleware: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/router')['defineNuxtRouteMiddleware']>
readonly definePageMeta: UnwrapRef<typeof import('../../node_modules/nuxt/dist/pages/runtime/composables')['definePageMeta']>
readonly definePayloadPlugin: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/nuxt')['definePayloadPlugin']>
readonly definePayloadReducer: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/payload')['definePayloadReducer']>
readonly definePayloadReviver: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/payload')['definePayloadReviver']>
readonly defineStore: UnwrapRef<typeof import('../../node_modules/@pinia/nuxt/dist/runtime/composables')['defineStore']>
readonly effect: UnwrapRef<typeof import('vue')['effect']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly getAppManifest: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/manifest')['getAppManifest']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getRouteRules: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/manifest')['getRouteRules']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly hasInjectionContext: UnwrapRef<typeof import('vue')['hasInjectionContext']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectHead: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/head')['injectHead']>
readonly isNuxtError: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/error')['isNuxtError']>
readonly isPrerendered: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/payload')['isPrerendered']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly isShallow: UnwrapRef<typeof import('vue')['isShallow']>
readonly isVue2: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/compat/vue-demi')['isVue2']>
readonly isVue3: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/compat/vue-demi')['isVue3']>
readonly loadPayload: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/payload')['loadPayload']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly navigateTo: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/router')['navigateTo']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onNuxtReady: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/ready')['onNuxtReady']>
readonly onPrehydrate: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/ssr')['onPrehydrate']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
readonly prefetchComponents: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/preload')['prefetchComponents']>
readonly preloadComponents: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/preload')['preloadComponents']>
readonly preloadPayload: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/payload')['preloadPayload']>
readonly preloadRouteComponents: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/preload')['preloadRouteComponents']>
readonly prerenderRoutes: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/ssr')['prerenderRoutes']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly proxyRefs: UnwrapRef<typeof import('vue')['proxyRefs']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly refreshCookie: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/cookie')['refreshCookie']>
readonly refreshNuxtData: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/asyncData')['refreshNuxtData']>
readonly reloadNuxtApp: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/chunk')['reloadNuxtApp']>
readonly requestIdleCallback: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/compat/idle-callback')['requestIdleCallback']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly setInterval: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/compat/interval')['setInterval']>
readonly setPageLayout: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/router')['setPageLayout']>
readonly setResponseStatus: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/ssr')['setResponseStatus']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showError: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/error')['showError']>
readonly storeToRefs: UnwrapRef<typeof import('../../node_modules/@pinia/nuxt/dist/runtime/composables')['storeToRefs']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryUseNuxtApp: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/nuxt')['tryUseNuxtApp']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly updateAppConfig: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/config')['updateAppConfig']>
readonly useAppConfig: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/config')['useAppConfig']>
readonly useAsyncData: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/asyncData')['useAsyncData']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useAuthStore: UnwrapRef<typeof import('../../stores/auth')['useAuthStore']>
readonly useBrowserLocale: UnwrapRef<typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['useBrowserLocale']>
readonly useCookie: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/cookie')['useCookie']>
readonly useCookieLocale: UnwrapRef<typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['useCookieLocale']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useDefaults: UnwrapRef<typeof import('vuetify')['useDefaults']>
readonly useDisplay: UnwrapRef<typeof import('vuetify')['useDisplay']>
readonly useError: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/error')['useError']>
readonly useFetch: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/fetch')['useFetch']>
readonly useHead: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/head')['useHead']>
readonly useHeadSafe: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/head')['useHeadSafe']>
readonly useHealthMonitor: UnwrapRef<typeof import('../../composables/useHealthMonitor')['useHealthMonitor']>
readonly useHydration: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/hydrate')['useHydration']>
readonly useI18n: UnwrapRef<typeof import('../../node_modules/vue-i18n/dist/vue-i18n')['useI18n']>
readonly useId: UnwrapRef<typeof import('vue')['useId']>
readonly useLayout: UnwrapRef<typeof import('vuetify')['useLayout']>
readonly useLazyAsyncData: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/asyncData')['useLazyAsyncData']>
readonly useLazyFetch: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/fetch')['useLazyFetch']>
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
readonly useLoadingIndicator: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/loading-indicator')['useLoadingIndicator']>
readonly useLocale: UnwrapRef<typeof import('vuetify')['useLocale']>
readonly useLocaleHead: UnwrapRef<typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['useLocaleHead']>
readonly useLocalePath: UnwrapRef<typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['useLocalePath']>
readonly useLocaleRoute: UnwrapRef<typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['useLocaleRoute']>
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
readonly useNuxtApp: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/nuxt')['useNuxtApp']>
readonly useNuxtData: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/asyncData')['useNuxtData']>
readonly usePinia: UnwrapRef<typeof import('../../node_modules/@pinia/nuxt/dist/runtime/composables')['usePinia']>
readonly usePolling: UnwrapRef<typeof import('../../composables/usePolling')['default']>
readonly usePreviewMode: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/preview')['usePreviewMode']>
readonly useRBAC: UnwrapRef<typeof import('../../composables/useRBAC')['useRBAC']>
readonly useRequestEvent: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/ssr')['useRequestEvent']>
readonly useRequestFetch: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/ssr')['useRequestFetch']>
readonly useRequestHeader: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/ssr')['useRequestHeader']>
readonly useRequestHeaders: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/ssr')['useRequestHeaders']>
readonly useRequestURL: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/url')['useRequestURL']>
readonly useResponseHeader: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/ssr')['useResponseHeader']>
readonly useRoute: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/router')['useRoute']>
readonly useRouteAnnouncer: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/route-announcer')['useRouteAnnouncer']>
readonly useRouteBaseName: UnwrapRef<typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['useRouteBaseName']>
readonly useRouter: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/router')['useRouter']>
readonly useRtl: UnwrapRef<typeof import('vuetify')['useRtl']>
readonly useRuntimeConfig: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/nuxt')['useRuntimeConfig']>
readonly useRuntimeHook: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/runtime-hook')['useRuntimeHook']>
readonly useScript: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScript']>
readonly useScriptClarity: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptClarity']>
readonly useScriptCloudflareWebAnalytics: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptCloudflareWebAnalytics']>
readonly useScriptCrisp: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptCrisp']>
readonly useScriptDatabuddyAnalytics: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptDatabuddyAnalytics']>
readonly useScriptEventPage: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptEventPage']>
readonly useScriptFathomAnalytics: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptFathomAnalytics']>
readonly useScriptGoogleAdsense: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptGoogleAdsense']>
readonly useScriptGoogleAnalytics: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptGoogleAnalytics']>
readonly useScriptGoogleMaps: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptGoogleMaps']>
readonly useScriptGoogleTagManager: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptGoogleTagManager']>
readonly useScriptHotjar: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptHotjar']>
readonly useScriptIntercom: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptIntercom']>
readonly useScriptLemonSqueezy: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptLemonSqueezy']>
readonly useScriptMatomoAnalytics: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptMatomoAnalytics']>
readonly useScriptMetaPixel: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptMetaPixel']>
readonly useScriptNpm: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptNpm']>
readonly useScriptPayPal: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptPayPal']>
readonly useScriptPlausibleAnalytics: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptPlausibleAnalytics']>
readonly useScriptRedditPixel: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptRedditPixel']>
readonly useScriptRybbitAnalytics: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptRybbitAnalytics']>
readonly useScriptSegment: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptSegment']>
readonly useScriptSnapchatPixel: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptSnapchatPixel']>
readonly useScriptStripe: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptStripe']>
readonly useScriptTriggerConsent: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptTriggerConsent']>
readonly useScriptTriggerElement: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptTriggerElement']>
readonly useScriptUmamiAnalytics: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptUmamiAnalytics']>
readonly useScriptVimeoPlayer: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptVimeoPlayer']>
readonly useScriptXPixel: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptXPixel']>
readonly useScriptYouTubePlayer: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/script-stubs')['useScriptYouTubePlayer']>
readonly useSeoMeta: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/head')['useSeoMeta']>
readonly useServerHead: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/head')['useServerHead']>
readonly useServerHeadSafe: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/head')['useServerHeadSafe']>
readonly useServerSeoMeta: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/head')['useServerSeoMeta']>
readonly useServiceMap: UnwrapRef<typeof import('../../composables/useServiceMap')['useServiceMap']>
readonly useSetI18nParams: UnwrapRef<typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['useSetI18nParams']>
readonly useShadowRoot: UnwrapRef<typeof import('vue')['useShadowRoot']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useState: UnwrapRef<typeof import('../../node_modules/nuxt/dist/app/composables/state')['useState']>
readonly useSwitchLocalePath: UnwrapRef<typeof import('../../node_modules/@nuxtjs/i18n/dist/runtime/composables/index')['useSwitchLocalePath']>
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
readonly useTheme: UnwrapRef<typeof import('vuetify')['useTheme']>
readonly useTileStore: UnwrapRef<typeof import('../../stores/tiles')['useTileStore']>
readonly useTransitionState: UnwrapRef<typeof import('vue')['useTransitionState']>
readonly useUserManagement: UnwrapRef<typeof import('../../composables/useUserManagement')['default']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
readonly withCtx: UnwrapRef<typeof import('vue')['withCtx']>
readonly withDirectives: UnwrapRef<typeof import('vue')['withDirectives']>
readonly withKeys: UnwrapRef<typeof import('vue')['withKeys']>
readonly withMemo: UnwrapRef<typeof import('vue')['withMemo']>
readonly withModifiers: UnwrapRef<typeof import('vue')['withModifiers']>
readonly withScopeId: UnwrapRef<typeof import('vue')['withScopeId']>
}
}

View File

@@ -1,14 +0,0 @@
import type { ComputedRef, MaybeRef } from 'vue'
type ComponentProps<T> = T extends new(...args: any) => { $props: infer P } ? NonNullable<P>
: T extends (props: infer P, ...args: any) => any ? P
: {}
declare module 'nuxt/app' {
interface NuxtLayouts {
}
export type LayoutKey = keyof NuxtLayouts extends never ? string : keyof NuxtLayouts
interface PageMeta {
layout?: MaybeRef<LayoutKey | false> | ComputedRef<LayoutKey | false>
}
}

View File

@@ -1,7 +0,0 @@
import type { NavigationGuard } from 'vue-router'
export type MiddlewareKey = never
declare module 'nuxt/app' {
interface PageMeta {
middleware?: MiddlewareKey | NavigationGuard | Array<MiddlewareKey | NavigationGuard>
}
}

View File

@@ -1,14 +0,0 @@
// Generated by nitro
// App Config
import type { Defu } from 'defu'
type UserAppConfig = Defu<{}, []>
declare module "nitropack/types" {
interface AppConfig extends UserAppConfig {}
}
export {}

View File

@@ -1,149 +0,0 @@
declare global {
const H3Error: typeof import('../../node_modules/h3').H3Error
const H3Event: typeof import('../../node_modules/h3').H3Event
const __buildAssetsURL: typeof import('../../node_modules/@nuxt/nitro-server/dist/runtime/utils/paths').buildAssetsURL
const __publicAssetsURL: typeof import('../../node_modules/@nuxt/nitro-server/dist/runtime/utils/paths').publicAssetsURL
const appendCorsHeaders: typeof import('../../node_modules/h3').appendCorsHeaders
const appendCorsPreflightHeaders: typeof import('../../node_modules/h3').appendCorsPreflightHeaders
const appendHeader: typeof import('../../node_modules/h3').appendHeader
const appendHeaders: typeof import('../../node_modules/h3').appendHeaders
const appendResponseHeader: typeof import('../../node_modules/h3').appendResponseHeader
const appendResponseHeaders: typeof import('../../node_modules/h3').appendResponseHeaders
const assertMethod: typeof import('../../node_modules/h3').assertMethod
const cachedEventHandler: typeof import('../../node_modules/nitropack/dist/runtime/internal/cache').cachedEventHandler
const cachedFunction: typeof import('../../node_modules/nitropack/dist/runtime/internal/cache').cachedFunction
const callNodeListener: typeof import('../../node_modules/h3').callNodeListener
const clearResponseHeaders: typeof import('../../node_modules/h3').clearResponseHeaders
const clearSession: typeof import('../../node_modules/h3').clearSession
const createApp: typeof import('../../node_modules/h3').createApp
const createAppEventHandler: typeof import('../../node_modules/h3').createAppEventHandler
const createError: typeof import('../../node_modules/h3').createError
const createEvent: typeof import('../../node_modules/h3').createEvent
const createEventStream: typeof import('../../node_modules/h3').createEventStream
const createRouter: typeof import('../../node_modules/h3').createRouter
const defaultContentType: typeof import('../../node_modules/h3').defaultContentType
const defineAppConfig: typeof import('../../node_modules/@nuxt/nitro-server/dist/runtime/utils/config').defineAppConfig
const defineCachedEventHandler: typeof import('../../node_modules/nitropack/dist/runtime/internal/cache').defineCachedEventHandler
const defineCachedFunction: typeof import('../../node_modules/nitropack/dist/runtime/internal/cache').defineCachedFunction
const defineEventHandler: typeof import('../../node_modules/h3').defineEventHandler
const defineLazyEventHandler: typeof import('../../node_modules/h3').defineLazyEventHandler
const defineNitroErrorHandler: typeof import('../../node_modules/nitropack/dist/runtime/internal/error/utils').defineNitroErrorHandler
const defineNitroPlugin: typeof import('../../node_modules/nitropack/dist/runtime/internal/plugin').defineNitroPlugin
const defineNodeListener: typeof import('../../node_modules/h3').defineNodeListener
const defineNodeMiddleware: typeof import('../../node_modules/h3').defineNodeMiddleware
const defineRenderHandler: typeof import('../../node_modules/nitropack/dist/runtime/internal/renderer').defineRenderHandler
const defineRequestMiddleware: typeof import('../../node_modules/h3').defineRequestMiddleware
const defineResponseMiddleware: typeof import('../../node_modules/h3').defineResponseMiddleware
const defineRouteMeta: typeof import('../../node_modules/nitropack/dist/runtime/internal/meta').defineRouteMeta
const defineTask: typeof import('../../node_modules/nitropack/dist/runtime/internal/task').defineTask
const defineWebSocket: typeof import('../../node_modules/h3').defineWebSocket
const defineWebSocketHandler: typeof import('../../node_modules/h3').defineWebSocketHandler
const deleteCookie: typeof import('../../node_modules/h3').deleteCookie
const dynamicEventHandler: typeof import('../../node_modules/h3').dynamicEventHandler
const eventHandler: typeof import('../../node_modules/h3').eventHandler
const fetchWithEvent: typeof import('../../node_modules/h3').fetchWithEvent
const fromNodeMiddleware: typeof import('../../node_modules/h3').fromNodeMiddleware
const fromPlainHandler: typeof import('../../node_modules/h3').fromPlainHandler
const fromWebHandler: typeof import('../../node_modules/h3').fromWebHandler
const getCookie: typeof import('../../node_modules/h3').getCookie
const getHeader: typeof import('../../node_modules/h3').getHeader
const getHeaders: typeof import('../../node_modules/h3').getHeaders
const getMethod: typeof import('../../node_modules/h3').getMethod
const getProxyRequestHeaders: typeof import('../../node_modules/h3').getProxyRequestHeaders
const getQuery: typeof import('../../node_modules/h3').getQuery
const getRequestFingerprint: typeof import('../../node_modules/h3').getRequestFingerprint
const getRequestHeader: typeof import('../../node_modules/h3').getRequestHeader
const getRequestHeaders: typeof import('../../node_modules/h3').getRequestHeaders
const getRequestHost: typeof import('../../node_modules/h3').getRequestHost
const getRequestIP: typeof import('../../node_modules/h3').getRequestIP
const getRequestPath: typeof import('../../node_modules/h3').getRequestPath
const getRequestProtocol: typeof import('../../node_modules/h3').getRequestProtocol
const getRequestURL: typeof import('../../node_modules/h3').getRequestURL
const getRequestWebStream: typeof import('../../node_modules/h3').getRequestWebStream
const getResponseHeader: typeof import('../../node_modules/h3').getResponseHeader
const getResponseHeaders: typeof import('../../node_modules/h3').getResponseHeaders
const getResponseStatus: typeof import('../../node_modules/h3').getResponseStatus
const getResponseStatusText: typeof import('../../node_modules/h3').getResponseStatusText
const getRouteRules: typeof import('../../node_modules/nitropack/dist/runtime/internal/route-rules').getRouteRules
const getRouterParam: typeof import('../../node_modules/h3').getRouterParam
const getRouterParams: typeof import('../../node_modules/h3').getRouterParams
const getSession: typeof import('../../node_modules/h3').getSession
const getValidatedQuery: typeof import('../../node_modules/h3').getValidatedQuery
const getValidatedRouterParams: typeof import('../../node_modules/h3').getValidatedRouterParams
const handleCacheHeaders: typeof import('../../node_modules/h3').handleCacheHeaders
const handleCors: typeof import('../../node_modules/h3').handleCors
const isCorsOriginAllowed: typeof import('../../node_modules/h3').isCorsOriginAllowed
const isError: typeof import('../../node_modules/h3').isError
const isEvent: typeof import('../../node_modules/h3').isEvent
const isEventHandler: typeof import('../../node_modules/h3').isEventHandler
const isMethod: typeof import('../../node_modules/h3').isMethod
const isPreflightRequest: typeof import('../../node_modules/h3').isPreflightRequest
const isStream: typeof import('../../node_modules/h3').isStream
const isWebResponse: typeof import('../../node_modules/h3').isWebResponse
const lazyEventHandler: typeof import('../../node_modules/h3').lazyEventHandler
const nitroPlugin: typeof import('../../node_modules/nitropack/dist/runtime/internal/plugin').nitroPlugin
const parseCookies: typeof import('../../node_modules/h3').parseCookies
const promisifyNodeListener: typeof import('../../node_modules/h3').promisifyNodeListener
const proxyRequest: typeof import('../../node_modules/h3').proxyRequest
const readBody: typeof import('../../node_modules/h3').readBody
const readFormData: typeof import('../../node_modules/h3').readFormData
const readMultipartFormData: typeof import('../../node_modules/h3').readMultipartFormData
const readRawBody: typeof import('../../node_modules/h3').readRawBody
const readValidatedBody: typeof import('../../node_modules/h3').readValidatedBody
const removeResponseHeader: typeof import('../../node_modules/h3').removeResponseHeader
const runTask: typeof import('../../node_modules/nitropack/dist/runtime/internal/task').runTask
const sanitizeStatusCode: typeof import('../../node_modules/h3').sanitizeStatusCode
const sanitizeStatusMessage: typeof import('../../node_modules/h3').sanitizeStatusMessage
const sealSession: typeof import('../../node_modules/h3').sealSession
const send: typeof import('../../node_modules/h3').send
const sendError: typeof import('../../node_modules/h3').sendError
const sendIterable: typeof import('../../node_modules/h3').sendIterable
const sendNoContent: typeof import('../../node_modules/h3').sendNoContent
const sendProxy: typeof import('../../node_modules/h3').sendProxy
const sendRedirect: typeof import('../../node_modules/h3').sendRedirect
const sendStream: typeof import('../../node_modules/h3').sendStream
const sendWebResponse: typeof import('../../node_modules/h3').sendWebResponse
const serveStatic: typeof import('../../node_modules/h3').serveStatic
const setCookie: typeof import('../../node_modules/h3').setCookie
const setHeader: typeof import('../../node_modules/h3').setHeader
const setHeaders: typeof import('../../node_modules/h3').setHeaders
const setResponseHeader: typeof import('../../node_modules/h3').setResponseHeader
const setResponseHeaders: typeof import('../../node_modules/h3').setResponseHeaders
const setResponseStatus: typeof import('../../node_modules/h3').setResponseStatus
const splitCookiesString: typeof import('../../node_modules/h3').splitCookiesString
const toEventHandler: typeof import('../../node_modules/h3').toEventHandler
const toNodeListener: typeof import('../../node_modules/h3').toNodeListener
const toPlainHandler: typeof import('../../node_modules/h3').toPlainHandler
const toWebHandler: typeof import('../../node_modules/h3').toWebHandler
const toWebRequest: typeof import('../../node_modules/h3').toWebRequest
const unsealSession: typeof import('../../node_modules/h3').unsealSession
const updateSession: typeof import('../../node_modules/h3').updateSession
const useAppConfig: typeof import('../../node_modules/nitropack/dist/runtime/internal/config').useAppConfig
const useBase: typeof import('../../node_modules/h3').useBase
const useEvent: typeof import('../../node_modules/nitropack/dist/runtime/internal/context').useEvent
const useNitroApp: typeof import('../../node_modules/nitropack/dist/runtime/internal/app').useNitroApp
const useRuntimeConfig: typeof import('../../node_modules/nitropack/dist/runtime/internal/config').useRuntimeConfig
const useSession: typeof import('../../node_modules/h3').useSession
const useStorage: typeof import('../../node_modules/nitropack/dist/runtime/internal/storage').useStorage
const writeEarlyHints: typeof import('../../node_modules/h3').writeEarlyHints
}
// for type re-export
declare global {
// @ts-ignore
export type { EventHandler, EventHandlerRequest, EventHandlerResponse, EventHandlerObject, H3EventContext } from '../../node_modules/h3'
import('../../node_modules/h3')
}
export { H3Event, H3Error, appendCorsHeaders, appendCorsPreflightHeaders, appendHeader, appendHeaders, appendResponseHeader, appendResponseHeaders, assertMethod, callNodeListener, clearResponseHeaders, clearSession, createApp, createAppEventHandler, createError, createEvent, createEventStream, createRouter, defaultContentType, defineEventHandler, defineLazyEventHandler, defineNodeListener, defineNodeMiddleware, defineRequestMiddleware, defineResponseMiddleware, defineWebSocket, defineWebSocketHandler, deleteCookie, dynamicEventHandler, eventHandler, fetchWithEvent, fromNodeMiddleware, fromPlainHandler, fromWebHandler, getCookie, getHeader, getHeaders, getMethod, getProxyRequestHeaders, getQuery, getRequestFingerprint, getRequestHeader, getRequestHeaders, getRequestHost, getRequestIP, getRequestPath, getRequestProtocol, getRequestURL, getRequestWebStream, getResponseHeader, getResponseHeaders, getResponseStatus, getResponseStatusText, getRouterParam, getRouterParams, getSession, getValidatedQuery, getValidatedRouterParams, handleCacheHeaders, handleCors, isCorsOriginAllowed, isError, isEvent, isEventHandler, isMethod, isPreflightRequest, isStream, isWebResponse, lazyEventHandler, parseCookies, promisifyNodeListener, proxyRequest, readBody, readFormData, readMultipartFormData, readRawBody, readValidatedBody, removeResponseHeader, sanitizeStatusCode, sanitizeStatusMessage, sealSession, send, sendError, sendIterable, sendNoContent, sendProxy, sendRedirect, sendStream, sendWebResponse, serveStatic, setCookie, setHeader, setHeaders, setResponseHeader, setResponseHeaders, setResponseStatus, splitCookiesString, toEventHandler, toNodeListener, toPlainHandler, toWebHandler, toWebRequest, unsealSession, updateSession, useBase, useSession, writeEarlyHints } from 'h3';
export { useNitroApp } from 'nitropack/runtime/internal/app';
export { useRuntimeConfig, useAppConfig } from 'nitropack/runtime/internal/config';
export { defineNitroPlugin, nitroPlugin } from 'nitropack/runtime/internal/plugin';
export { defineCachedFunction, defineCachedEventHandler, cachedFunction, cachedEventHandler } from 'nitropack/runtime/internal/cache';
export { useStorage } from 'nitropack/runtime/internal/storage';
export { defineRenderHandler } from 'nitropack/runtime/internal/renderer';
export { defineRouteMeta } from 'nitropack/runtime/internal/meta';
export { getRouteRules } from 'nitropack/runtime/internal/route-rules';
export { useEvent } from 'nitropack/runtime/internal/context';
export { defineTask, runTask } from 'nitropack/runtime/internal/task';
export { defineNitroErrorHandler } from 'nitropack/runtime/internal/error/utils';
export { buildAssetsURL as __buildAssetsURL, publicAssetsURL as __publicAssetsURL } from '/app/node_modules/@nuxt/nitro-server/dist/runtime/utils/paths';
export { defineAppConfig } from '/app/node_modules/@nuxt/nitro-server/dist/runtime/utils/config';

View File

@@ -1,17 +0,0 @@
export type LayoutKey = string
declare module 'nitropack' {
interface NitroRouteConfig {
appLayout?: LayoutKey | false
}
interface NitroRouteRules {
appLayout?: LayoutKey | false
}
}
declare module 'nitropack/types' {
interface NitroRouteConfig {
appLayout?: LayoutKey | false
}
interface NitroRouteRules {
appLayout?: LayoutKey | false
}
}

View File

@@ -1,17 +0,0 @@
export type MiddlewareKey = never
declare module 'nitropack' {
interface NitroRouteConfig {
appMiddleware?: MiddlewareKey | MiddlewareKey[] | Record<MiddlewareKey, boolean>
}
interface NitroRouteRules {
appMiddleware?: MiddlewareKey | MiddlewareKey[] | Record<MiddlewareKey, boolean>
}
}
declare module 'nitropack/types' {
interface NitroRouteConfig {
appMiddleware?: MiddlewareKey | MiddlewareKey[] | Record<MiddlewareKey, boolean>
}
interface NitroRouteRules {
appMiddleware?: MiddlewareKey | MiddlewareKey[] | Record<MiddlewareKey, boolean>
}
}

View File

@@ -1,39 +0,0 @@
/// <reference path="nitro-layouts.d.ts" />
/// <reference path="app.config.d.ts" />
/// <reference path="runtime-config.d.ts" />
/// <reference path="../../node_modules/@nuxt/nitro-server/dist/index.d.mts" />
/// <reference path="nitro-middleware.d.ts" />
/// <reference path="./schema.d.ts" />
import type { RuntimeConfig } from 'nuxt/schema'
import type { H3Event } from 'h3'
import type { LogObject } from 'consola'
import type { NuxtIslandContext, NuxtIslandResponse, NuxtRenderHTMLContext } from 'nuxt/app'
declare module 'nitropack' {
interface NitroRuntimeConfigApp {
buildAssetsDir: string
cdnURL: string
}
interface NitroRuntimeConfig extends RuntimeConfig {}
interface NitroRouteConfig {
ssr?: boolean
noScripts?: boolean
/** @deprecated Use `noScripts` instead */
experimentalNoScripts?: boolean
}
interface NitroRouteRules {
ssr?: boolean
noScripts?: boolean
/** @deprecated Use `noScripts` instead */
experimentalNoScripts?: boolean
appMiddleware?: Record<string, boolean>
appLayout?: string | false
}
interface NitroRuntimeHooks {
'dev:ssr-logs': (ctx: { logs: LogObject[], path: string }) => void | Promise<void>
'render:html': (htmlContext: NuxtRenderHTMLContext, context: { event: H3Event }) => void | Promise<void>
'render:island': (islandResponse: NuxtIslandResponse, context: { event: H3Event, islandContext: NuxtIslandContext }) => void | Promise<void>
}
}

View File

@@ -1,14 +0,0 @@
// Generated by nitro
import type { Serialize, Simplify } from "nitropack/types";
declare module "nitropack/types" {
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T
interface InternalApi {
'/__nuxt_error': {
'default': Simplify<Serialize<Awaited<ReturnType<typeof import('../../node_modules/@nuxt/nitro-server/dist/runtime/handlers/renderer').default>>>>
}
'/__nuxt_island/**': {
'default': Simplify<Serialize<Awaited<ReturnType<typeof import('../../server/#internal/nuxt/island-renderer').default>>>>
}
}
}
export {}

View File

@@ -1,3 +0,0 @@
/// <reference path="./nitro-routes.d.ts" />
/// <reference path="./nitro-config.d.ts" />
/// <reference path="./nitro-imports.d.ts" />

View File

@@ -1,42 +0,0 @@
// Generated by Nuxt'
import type { Plugin } from '#app'
type Decorate<T extends Record<string, any>> = { [K in keyof T as K extends string ? `$${K}` : never]: T[K] }
type InjectionType<A extends Plugin> = A extends {default: Plugin<infer T>} ? Decorate<T> : unknown
type NuxtAppInjections =
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/revive-payload.client.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/head/runtime/plugins/unhead.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/pages/runtime/plugins/router.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/browser-devtools-timing.client.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/payload.client.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/dev-server-logs.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/navigation-repaint.client.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/check-outdated-build.client.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/revive-payload.server.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/chunk-reload.client.js")> &
InjectionType<typeof import("../../node_modules/@pinia/nuxt/dist/runtime/plugin.vue3.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/pages/runtime/plugins/prefetch.client.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/pages/runtime/plugins/check-if-page-unused.js")> &
InjectionType<typeof import("../../node_modules/@nuxtjs/i18n/dist/runtime/plugins/switch-locale-path-ssr.js")> &
InjectionType<typeof import("../../node_modules/@nuxtjs/i18n/dist/runtime/plugins/i18n.js")> &
InjectionType<typeof import("../../node_modules/vuetify-nuxt-module/dist/runtime/plugins/vuetify-i18n.js")> &
InjectionType<typeof import("../../node_modules/vuetify-nuxt-module/dist/runtime/plugins/vuetify-icons.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/warn.dev.server.js")> &
InjectionType<typeof import("../../node_modules/nuxt/dist/app/plugins/check-if-layout-used.js")> &
InjectionType<typeof import("../../node_modules/vuetify-nuxt-module/dist/runtime/plugins/vuetify-sync.js")>
declare module '#app' {
interface NuxtApp extends NuxtAppInjections { }
interface NuxtAppLiterals {
pluginName: 'nuxt:revive-payload:client' | 'nuxt:head' | 'nuxt:router' | 'nuxt:browser-devtools-timing' | 'nuxt:payload' | 'nuxt:revive-payload:server' | 'nuxt:chunk-reload' | 'pinia' | 'nuxt:global-components' | 'nuxt:prefetch' | 'nuxt:checkIfPageUnused' | 'i18n:plugin:switch-locale-path-ssr' | 'i18n:plugin' | 'nuxt:checkIfLayoutUsed' | 'vuetify:configuration:plugin'
}
}
declare module 'vue' {
interface ComponentCustomProperties extends NuxtAppInjections { }
}
export { }

View File

@@ -1,217 +0,0 @@
import { RuntimeConfig as UserRuntimeConfig, PublicRuntimeConfig as UserPublicRuntimeConfig } from 'nuxt/schema'
import { NuxtModule, ModuleDependencyMeta } from '@nuxt/schema'
interface SharedRuntimeConfig {
app: {
buildId: string,
baseURL: string,
buildAssetsDir: string,
cdnURL: string,
},
nitro: {
envPrefix: string,
},
}
interface SharedPublicRuntimeConfig {
apiBaseUrl: string,
appName: string,
appVersion: string,
i18n: {
baseUrl: string,
defaultLocale: string,
defaultDirection: string,
strategy: string,
lazy: boolean,
rootRedirect: any,
routesNameSeparator: string,
defaultLocaleRouteNameSuffix: string,
skipSettingLocaleOnNavigate: boolean,
differentDomains: boolean,
trailingSlash: boolean,
configLocales: Array<{
}>,
locales: {
en: {
domain: any,
},
hu: {
domain: any,
},
},
detectBrowserLanguage: {
alwaysRedirect: boolean,
cookieCrossOrigin: boolean,
cookieDomain: any,
cookieKey: string,
cookieSecure: boolean,
fallbackLocale: string,
redirectOn: string,
useCookie: boolean,
},
experimental: {
localeDetector: string,
switchLocalePathLinkSSR: boolean,
autoImportTranslationFunctions: boolean,
},
multiDomainLocales: boolean,
},
}
declare module '@nuxt/schema' {
interface ModuleDependencies {
["pinia"]?: ModuleDependencyMeta<typeof import("@pinia/nuxt").default extends NuxtModule<infer O> ? O | false : Record<string, unknown>> | false
["@nuxtjs/tailwindcss"]?: ModuleDependencyMeta<typeof import("@nuxtjs/tailwindcss").default extends NuxtModule<infer O> ? O | false : Record<string, unknown>> | false
["vuetify-nuxt-module"]?: ModuleDependencyMeta<typeof import("vuetify-nuxt-module").default extends NuxtModule<infer O> ? O | false : Record<string, unknown>> | false
["@nuxtjs/i18n"]?: ModuleDependencyMeta<typeof import("@nuxtjs/i18n").default extends NuxtModule<infer O> ? O | false : Record<string, unknown>> | false
["@nuxt/telemetry"]?: ModuleDependencyMeta<typeof import("@nuxt/telemetry").default extends NuxtModule<infer O> ? O | false : Record<string, unknown>> | false
}
interface NuxtOptions {
/**
* Configuration for `@pinia/nuxt`
*/
["pinia"]: typeof import("@pinia/nuxt").default extends NuxtModule<infer O, unknown, boolean> ? O | false : Record<string, any> | false
/**
* Configuration for `@nuxtjs/tailwindcss`
*/
["tailwindcss"]: typeof import("@nuxtjs/tailwindcss").default extends NuxtModule<infer O, unknown, boolean> ? O | false : Record<string, any> | false
/**
* Configuration for `vuetify-nuxt-module`
*/
["vuetify"]: typeof import("vuetify-nuxt-module").default extends NuxtModule<infer O, unknown, boolean> ? O | false : Record<string, any> | false
/**
* Configuration for `@nuxtjs/i18n`
*/
["i18n"]: typeof import("@nuxtjs/i18n").default extends NuxtModule<infer O, unknown, boolean> ? O | false : Record<string, any> | false
/**
* Configuration for `@nuxt/telemetry`
*/
["telemetry"]: typeof import("@nuxt/telemetry").default extends NuxtModule<infer O, unknown, boolean> ? O | false : Record<string, any> | false
}
interface NuxtConfig {
/**
* Configuration for `@pinia/nuxt`
*/
["pinia"]?: typeof import("@pinia/nuxt").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> | false : Record<string, any> | false
/**
* Configuration for `@nuxtjs/tailwindcss`
*/
["tailwindcss"]?: typeof import("@nuxtjs/tailwindcss").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> | false : Record<string, any> | false
/**
* Configuration for `vuetify-nuxt-module`
*/
["vuetify"]?: typeof import("vuetify-nuxt-module").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> | false : Record<string, any> | false
/**
* Configuration for `@nuxtjs/i18n`
*/
["i18n"]?: typeof import("@nuxtjs/i18n").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> | false : Record<string, any> | false
/**
* Configuration for `@nuxt/telemetry`
*/
["telemetry"]?: typeof import("@nuxt/telemetry").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> | false : Record<string, any> | false
modules?: (undefined | null | false | NuxtModule<any> | string | [NuxtModule | string, Record<string, any>] | ["@pinia/nuxt", Exclude<NuxtConfig["pinia"], boolean>] | ["@nuxtjs/tailwindcss", Exclude<NuxtConfig["tailwindcss"], boolean>] | ["vuetify-nuxt-module", Exclude<NuxtConfig["vuetify"], boolean>] | ["@nuxtjs/i18n", Exclude<NuxtConfig["i18n"], boolean>] | ["@nuxt/telemetry", Exclude<NuxtConfig["telemetry"], boolean>])[],
}
interface RuntimeConfig extends UserRuntimeConfig {}
interface PublicRuntimeConfig extends UserPublicRuntimeConfig {}
}
declare module 'nuxt/schema' {
interface ModuleDependencies {
["pinia"]?: ModuleDependencyMeta<typeof import("@pinia/nuxt").default extends NuxtModule<infer O> ? O | false : Record<string, unknown>> | false
["@nuxtjs/tailwindcss"]?: ModuleDependencyMeta<typeof import("@nuxtjs/tailwindcss").default extends NuxtModule<infer O> ? O | false : Record<string, unknown>> | false
["vuetify-nuxt-module"]?: ModuleDependencyMeta<typeof import("vuetify-nuxt-module").default extends NuxtModule<infer O> ? O | false : Record<string, unknown>> | false
["@nuxtjs/i18n"]?: ModuleDependencyMeta<typeof import("@nuxtjs/i18n").default extends NuxtModule<infer O> ? O | false : Record<string, unknown>> | false
["@nuxt/telemetry"]?: ModuleDependencyMeta<typeof import("@nuxt/telemetry").default extends NuxtModule<infer O> ? O | false : Record<string, unknown>> | false
}
interface NuxtOptions {
/**
* Configuration for `@pinia/nuxt`
* @see https://www.npmjs.com/package/@pinia/nuxt
*/
["pinia"]: typeof import("@pinia/nuxt").default extends NuxtModule<infer O, unknown, boolean> ? O | false : Record<string, any> | false
/**
* Configuration for `@nuxtjs/tailwindcss`
* @see https://www.npmjs.com/package/@nuxtjs/tailwindcss
*/
["tailwindcss"]: typeof import("@nuxtjs/tailwindcss").default extends NuxtModule<infer O, unknown, boolean> ? O | false : Record<string, any> | false
/**
* Configuration for `vuetify-nuxt-module`
* @see https://www.npmjs.com/package/vuetify-nuxt-module
*/
["vuetify"]: typeof import("vuetify-nuxt-module").default extends NuxtModule<infer O, unknown, boolean> ? O | false : Record<string, any> | false
/**
* Configuration for `@nuxtjs/i18n`
* @see https://www.npmjs.com/package/@nuxtjs/i18n
*/
["i18n"]: typeof import("@nuxtjs/i18n").default extends NuxtModule<infer O, unknown, boolean> ? O | false : Record<string, any> | false
/**
* Configuration for `@nuxt/telemetry`
* @see https://www.npmjs.com/package/@nuxt/telemetry
*/
["telemetry"]: typeof import("@nuxt/telemetry").default extends NuxtModule<infer O, unknown, boolean> ? O | false : Record<string, any> | false
}
interface NuxtConfig {
/**
* Configuration for `@pinia/nuxt`
* @see https://www.npmjs.com/package/@pinia/nuxt
*/
["pinia"]?: typeof import("@pinia/nuxt").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> | false : Record<string, any> | false
/**
* Configuration for `@nuxtjs/tailwindcss`
* @see https://www.npmjs.com/package/@nuxtjs/tailwindcss
*/
["tailwindcss"]?: typeof import("@nuxtjs/tailwindcss").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> | false : Record<string, any> | false
/**
* Configuration for `vuetify-nuxt-module`
* @see https://www.npmjs.com/package/vuetify-nuxt-module
*/
["vuetify"]?: typeof import("vuetify-nuxt-module").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> | false : Record<string, any> | false
/**
* Configuration for `@nuxtjs/i18n`
* @see https://www.npmjs.com/package/@nuxtjs/i18n
*/
["i18n"]?: typeof import("@nuxtjs/i18n").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> | false : Record<string, any> | false
/**
* Configuration for `@nuxt/telemetry`
* @see https://www.npmjs.com/package/@nuxt/telemetry
*/
["telemetry"]?: typeof import("@nuxt/telemetry").default extends NuxtModule<infer O, unknown, boolean> ? Partial<O> | false : Record<string, any> | false
modules?: (undefined | null | false | NuxtModule<any> | string | [NuxtModule | string, Record<string, any>] | ["@pinia/nuxt", Exclude<NuxtConfig["pinia"], boolean>] | ["@nuxtjs/tailwindcss", Exclude<NuxtConfig["tailwindcss"], boolean>] | ["vuetify-nuxt-module", Exclude<NuxtConfig["vuetify"], boolean>] | ["@nuxtjs/i18n", Exclude<NuxtConfig["i18n"], boolean>] | ["@nuxt/telemetry", Exclude<NuxtConfig["telemetry"], boolean>])[],
}
interface RuntimeConfig extends SharedRuntimeConfig {}
interface PublicRuntimeConfig extends SharedPublicRuntimeConfig {}
}
declare module 'vue' {
interface ComponentCustomProperties {
$config: UserRuntimeConfig
}
}

View File

@@ -1,44 +0,0 @@
# Multi-stage build for Nuxt 3 admin frontend
FROM node:20-slim as builder
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY nuxt.config.ts ./
COPY tsconfig.json ./
# Install dependencies
RUN npm install --no-audit --progress=false
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Production stage
FROM node:20-slim as runner
WORKDIR /app
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nuxtuser
# Copy built application and dependencies
COPY --from=builder --chown=nuxtuser:nodejs /app/.output ./
COPY --from=builder --chown=nuxtuser:nodejs /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
# Switch to non-root user
USER nuxtuser
# Expose port 3000 (Nuxt default)
EXPOSE 3000
# Start the application
ENV NUXT_HOST=0.0.0.0
ENV NUXT_PORT=3000
CMD ["node", "./server/index.mjs"]

View File

@@ -1,24 +0,0 @@
# Development Dockerfile for Nuxt 3 admin frontend
FROM node:20-slim
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY nuxt.config.ts ./
COPY tsconfig.json ./
# Install dependencies
RUN npm install --no-audit --progress=false
# Copy source code
COPY . .
# Expose Nuxt development port
EXPOSE 8502
# Start development server
ENV NUXT_HOST=0.0.0.0
ENV NUXT_PORT=8502
CMD ["npm", "run", "dev", "--", "-o"]

View File

@@ -1,18 +0,0 @@
<template>
<div>
<NuxtPage />
</div>
</template>
<script setup lang="ts">
// Root app component
</script>
<style>
/* Global styles */
html, body {
margin: 0;
padding: 0;
font-family: 'Roboto', sans-serif;
}
</style>

View File

@@ -1,635 +0,0 @@
<template>
<v-card
color="indigo-darken-1"
variant="tonal"
class="h-100 d-flex flex-column"
>
<v-card-title class="d-flex align-center justify-space-between">
<div class="d-flex align-center">
<v-icon icon="mdi-robot" class="mr-2"></v-icon>
<span class="text-subtitle-1 font-weight-bold">AI Pipeline Monitor</span>
</div>
<div class="d-flex align-center">
<v-chip size="small" color="green" class="mr-2">
<v-icon icon="mdi-pulse" size="small" class="mr-1"></v-icon>
Live
</v-chip>
<v-chip size="small" :color="connectionStatusColor">
<v-icon :icon="connectionStatusIcon" size="small" class="mr-1"></v-icon>
{{ connectionStatusText }}
</v-chip>
</div>
</v-card-title>
<v-card-text class="flex-grow-1 pa-0">
<!-- Connection Status Bar -->
<div class="px-4 pt-2 pb-1" :class="connectionStatusBarClass">
<div class="d-flex align-center justify-space-between">
<div class="text-caption">
<v-icon :icon="connectionStatusIcon" size="small" class="mr-1"></v-icon>
{{ connectionStatusMessage }}
</div>
<div class="text-caption">
Polling: {{ pollingInterval / 1000 }}s
<v-btn
icon="mdi-refresh"
size="x-small"
variant="text"
class="ml-1"
@click="forceRefresh"
:loading="isRefreshing"
></v-btn>
</div>
</div>
</div>
<!-- Robot Status Dashboard -->
<div class="pa-4">
<div class="text-subtitle-2 font-weight-medium mb-2">Robot Status Dashboard</div>
<!-- Geographical Filter -->
<div class="mb-3">
<v-chip-group v-model="selectedRegion" column>
<v-chip size="small" value="all">All Regions</v-chip>
<v-chip size="small" value="GB">UK (GB)</v-chip>
<v-chip size="small" value="EU">Europe</v-chip>
<v-chip size="small" value="US">North America</v-chip>
<v-chip size="small" value="OC">Oceania</v-chip>
</v-chip-group>
</div>
<!-- Robot Status Cards -->
<v-row dense class="mb-4">
<v-col v-for="robot in filteredRobots" :key="robot.id" cols="12" sm="6">
<v-card variant="outlined" class="pa-2">
<div class="d-flex align-center justify-space-between">
<div>
<div class="d-flex align-center">
<v-icon :icon="robot.icon" size="small" :color="robot.statusColor" class="mr-2"></v-icon>
<span class="text-caption font-weight-medium">{{ robot.name }}</span>
</div>
<div class="text-caption text-grey mt-1">{{ robot.description }}</div>
</div>
<div class="text-right">
<div class="text-caption" :class="`text-${robot.statusColor}`">{{ robot.status }}</div>
<div class="text-caption text-grey">{{ robot.region }}</div>
</div>
</div>
<!-- Progress Bar -->
<v-progress-linear
v-if="robot.progress !== undefined"
:model-value="robot.progress"
height="6"
:color="robot.progressColor"
class="mt-2"
></v-progress-linear>
<!-- Stats -->
<div class="d-flex justify-space-between mt-2">
<div class="text-caption">
<v-icon icon="mdi-check-circle" size="x-small" color="success" class="mr-1"></v-icon>
{{ robot.successRate }}%
</div>
<div class="text-caption">
<v-icon icon="mdi-alert-circle" size="x-small" color="error" class="mr-1"></v-icon>
{{ robot.failureRate }}%
</div>
<div class="text-caption">
<v-icon icon="mdi-clock-outline" size="x-small" color="warning" class="mr-1"></v-icon>
{{ robot.avgTime }}s
</div>
</div>
</v-card>
</v-col>
</v-row>
<!-- Overall Pipeline Stats -->
<v-card variant="outlined" class="pa-3 mb-4">
<div class="text-subtitle-2 font-weight-medium mb-2">Pipeline Overview</div>
<v-row dense>
<v-col cols="6" sm="3">
<div class="text-center">
<div class="text-h5 font-weight-bold text-primary">{{ pipelineStats.totalProcessed }}</div>
<div class="text-caption text-grey">Total Processed</div>
</div>
</v-col>
<v-col cols="6" sm="3">
<div class="text-center">
<div class="text-h5 font-weight-bold text-success">{{ pipelineStats.successRate }}%</div>
<div class="text-caption text-grey">Success Rate</div>
</div>
</v-col>
<v-col cols="6" sm="3">
<div class="text-center">
<div class="text-h5 font-weight-bold text-warning">{{ pipelineStats.avgProcessingTime }}s</div>
<div class="text-caption text-grey">Avg Time</div>
</div>
</v-col>
<v-col cols="6" sm="3">
<div class="text-center">
<div class="text-h5 font-weight-bold" :class="pipelineStats.queueSize > 100 ? 'text-error' : 'text-info'">{{ pipelineStats.queueSize }}</div>
<div class="text-caption text-grey">Queue Size</div>
</div>
</v-col>
</v-row>
</v-card>
<!-- Recent Activity -->
<div class="text-subtitle-2 font-weight-medium mb-2">Recent Activity</div>
<div class="log-entries-container pa-2" ref="logContainer" style="height: 150px;">
<!-- Loading State -->
<div v-if="isLoading && logs.length === 0" class="text-center py-4">
<v-progress-circular indeterminate color="primary" size="20"></v-progress-circular>
<div class="text-caption mt-1">Loading AI logs...</div>
</div>
<!-- Empty State -->
<div v-else-if="logs.length === 0" class="text-center py-4">
<v-icon icon="mdi-robot-off" size="32" color="grey-lighten-1"></v-icon>
<div class="text-body-2 mt-1">No AI activity</div>
</div>
<!-- Log Entries -->
<div v-else class="log-entries">
<div
v-for="(log, index) in visibleLogs"
:key="log.id"
class="log-entry mb-2 pa-2"
:class="{ 'new-entry': log.isNew }"
>
<div class="d-flex align-center">
<v-icon
:color="getLogColor(log.type)"
:icon="getLogIcon(log.type)"
size="small"
class="mr-2"
></v-icon>
<div class="flex-grow-1">
<div class="text-caption">{{ log.message }}</div>
<div class="d-flex align-center mt-1">
<v-chip size="x-small" :color="getRobotColor(log.robot)" class="mr-1">
{{ log.robot }}
</v-chip>
<span class="text-caption text-grey">{{ formatTime(log.timestamp) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</v-card-text>
<v-card-actions class="pa-3">
<div class="d-flex justify-space-between align-center w-100">
<div class="text-caption">
<v-icon icon="mdi-robot" size="small" class="mr-1"></v-icon>
{{ activeRobots }} active {{ filteredRobots.length }} filtered
</div>
<div class="d-flex">
<v-btn
size="x-small"
variant="text"
@click="toggleAutoScroll"
:color="autoScroll ? 'primary' : 'grey'"
>
<v-icon :icon="autoScroll ? 'mdi-pin' : 'mdi-pin-off'" size="small" class="mr-1"></v-icon>
{{ autoScroll ? 'Auto-scroll' : 'Manual' }}
</v-btn>
</div>
</div>
</v-card-actions>
</v-card>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
// Types
interface AiLogEntry {
id: string
timestamp: Date
message: string
type: 'info' | 'success' | 'warning' | 'error' | 'gold'
robot: string
vehicleId?: string
status?: string
details?: string
isNew?: boolean
}
interface RobotStatus {
id: string
name: string
description: string
region: string
status: 'running' | 'idle' | 'error' | 'paused'
statusColor: string
icon: string
progress?: number
progressColor: string
successRate: number
failureRate: number
avgTime: number
lastActivity: Date
}
// State
const logs = ref<AiLogEntry[]>([])
const isLoading = ref(true)
const isRefreshing = ref(false)
const autoScroll = ref(true)
const pollingInterval = ref(5000) // 5 seconds
const connectionStatus = ref<'connected' | 'disconnected' | 'error'>('connected')
const logContainer = ref<HTMLElement | null>(null)
const selectedRegion = ref('all')
// Robot status data
const robots = ref<RobotStatus[]>([
{
id: 'gb-discovery',
name: 'GB Discovery',
description: 'UK catalog discovery from MOT CSV',
region: 'GB',
status: 'running',
statusColor: 'success',
icon: 'mdi-magnify',
progress: 75,
progressColor: 'primary',
successRate: 92,
failureRate: 3,
avgTime: 45,
lastActivity: new Date()
},
{
id: 'gb-hunter',
name: 'GB Hunter',
description: 'DVLA API vehicle data fetcher',
region: 'GB',
status: 'running',
statusColor: 'success',
icon: 'mdi-target',
progress: 60,
progressColor: 'indigo',
successRate: 88,
failureRate: 5,
avgTime: 12,
lastActivity: new Date()
},
{
id: 'nhtsa-fetcher',
name: 'NHTSA Fetcher',
description: 'US vehicle specifications',
region: 'US',
status: 'idle',
statusColor: 'warning',
icon: 'mdi-database-import',
progress: 0,
progressColor: 'orange',
successRate: 95,
failureRate: 2,
avgTime: 8,
lastActivity: new Date(Date.now() - 3600000) // 1 hour ago
},
{
id: 'system-ocr',
name: 'System OCR',
description: 'Document processing AI',
region: 'all',
status: 'running',
statusColor: 'success',
icon: 'mdi-text-recognition',
progress: 90,
progressColor: 'green',
successRate: 85,
failureRate: 8,
avgTime: 25,
lastActivity: new Date()
},
{
id: 'rdw-enricher',
name: 'RDW Enricher',
description: 'Dutch vehicle data',
region: 'EU',
status: 'error',
statusColor: 'error',
icon: 'mdi-alert-circle',
progress: 30,
progressColor: 'red',
successRate: 78,
failureRate: 15,
avgTime: 18,
lastActivity: new Date(Date.now() - 1800000) // 30 minutes ago
},
{
id: 'alchemist-pro',
name: 'Alchemist Pro',
description: 'Gold status optimizer',
region: 'all',
status: 'running',
statusColor: 'success',
icon: 'mdi-star',
progress: 85,
progressColor: 'amber',
successRate: 96,
failureRate: 1,
avgTime: 32,
lastActivity: new Date()
}
])
// Mock data generator
const generateMockLog = (): AiLogEntry => {
const robotList = robots.value.map(r => r.name)
const types: AiLogEntry['type'][] = ['info', 'success', 'warning', 'error', 'gold']
const messages = [
'Vehicle #4521 changed to Gold Status',
'New vehicle discovered in UK catalog',
'DVLA API quota limit reached',
'OCR processing completed for invoice #789',
'Service validation failed - missing coordinates',
'Price comparison completed for 15 services',
'Vehicle technical data enriched successfully',
'Database synchronization in progress',
'AI model training completed',
'Real-time monitoring activated'
]
const robot = robotList[Math.floor(Math.random() * robotList.length)]
const type = types[Math.floor(Math.random() * types.length)]
const message = messages[Math.floor(Math.random() * messages.length)]
return {
id: `log_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date(),
message,
type,
robot,
vehicleId: type === 'gold' ? `#${Math.floor(Math.random() * 10000)}` : undefined,
status: type === 'gold' ? 'GOLD' : type === 'success' ? 'SUCCESS' : type === 'error' ? 'FAILED' : 'PROCESSING',
details: type === 'error' ? 'API timeout after 30 seconds' : undefined,
isNew: true
}
}
// Computed properties
const filteredRobots = computed(() => {
if (selectedRegion.value === 'all') return robots.value
return robots.value.filter(robot => robot.region === selectedRegion.value || robot.region === 'all')
})
const visibleLogs = computed(() => {
// Show latest 5 logs
return [...logs.value].slice(-5).reverse()
})
const activeRobots = computed(() => {
return robots.value.filter(r => r.status === 'running').length
})
const pipelineStats = computed(() => {
const totalRobots = robots.value.length
const runningRobots = robots.value.filter(r => r.status === 'running').length
const totalSuccessRate = robots.value.reduce((sum, r) => sum + r.successRate, 0) / totalRobots
const totalAvgTime = robots.value.reduce((sum, r) => sum + r.avgTime, 0) / totalRobots
return {
totalProcessed: Math.floor(Math.random() * 10000) + 5000,
successRate: Math.round(totalSuccessRate),
avgProcessingTime: Math.round(totalAvgTime),
queueSize: Math.floor(Math.random() * 200),
runningRobots,
totalRobots
}
})
const connectionStatusColor = computed(() => {
switch (connectionStatus.value) {
case 'connected': return 'green'
case 'disconnected': return 'orange'
case 'error': return 'red'
default: return 'grey'
}
})
const connectionStatusIcon = computed(() => {
switch (connectionStatus.value) {
case 'connected': return 'mdi-check-circle'
case 'disconnected': return 'mdi-alert-circle'
case 'error': return 'mdi-close-circle'
default: return 'mdi-help-circle'
}
})
const connectionStatusText = computed(() => {
switch (connectionStatus.value) {
case 'connected': return 'Connected'
case 'disconnected': return 'Disconnected'
case 'error': return 'Error'
default: return 'Unknown'
}
})
const connectionStatusMessage = computed(() => {
switch (connectionStatus.value) {
case 'connected': return 'Connected to AI logs stream'
case 'disconnected': return 'Disconnected - using mock data'
case 'error': return 'Connection error - check API endpoint'
default: return 'Status unknown'
}
})
const connectionStatusBarClass = computed(() => {
switch (connectionStatus.value) {
case 'connected': return 'bg-green-lighten-5'
case 'disconnected': return 'bg-orange-lighten-5'
case 'error': return 'bg-red-lighten-5'
default: return 'bg-grey-lighten-5'
}
})
// Helper functions
// Helper functions
const getLogColor = (type: AiLogEntry['type']) => {
switch (type) {
case 'info': return 'blue'
case 'success': return 'green'
case 'warning': return 'orange'
case 'error': return 'red'
case 'gold': return 'amber'
default: return 'grey'
}
}
const getLogIcon = (type: AiLogEntry['type']) => {
switch (type) {
case 'info': return 'mdi-information'
case 'success': return 'mdi-check-circle'
case 'warning': return 'mdi-alert'
case 'error': return 'mdi-alert-circle'
case 'gold': return 'mdi-star'
default: return 'mdi-help-circle'
}
}
const getRobotColor = (robotName: string) => {
const robot = robots.value.find(r => r.name === robotName)
return robot?.statusColor || 'grey'
}
const getStatusColor = (status: string) => {
switch (status.toLowerCase()) {
case 'running': return 'success'
case 'idle': return 'warning'
case 'error': return 'error'
case 'paused': return 'grey'
default: return 'grey'
}
}
const formatTime = (timestamp: Date) => {
const now = new Date()
const diff = now.getTime() - timestamp.getTime()
if (diff < 60000) return 'Just now'
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`
return timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
}
// Data fetching and polling
const fetchLogs = async () => {
if (isRefreshing.value) return
isRefreshing.value = true
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 500))
// Add new mock log
const newLog = generateMockLog()
logs.value.push(newLog)
// Keep only last 50 logs
if (logs.value.length > 50) {
logs.value = logs.value.slice(-50)
}
// Mark old logs as not new
setTimeout(() => {
logs.value.forEach(log => {
if (log.isNew && Date.now() - log.timestamp.getTime() > 5000) {
log.isNew = false
}
})
}, 5000)
// Update connection status randomly
if (Math.random() > 0.95) {
connectionStatus.value = 'disconnected'
} else if (Math.random() > 0.98) {
connectionStatus.value = 'error'
} else {
connectionStatus.value = 'connected'
}
} catch (error) {
console.error('Failed to fetch AI logs:', error)
connectionStatus.value = 'error'
} finally {
isRefreshing.value = false
isLoading.value = false
}
}
const forceRefresh = () => {
fetchLogs()
}
const toggleAutoScroll = () => {
autoScroll.value = !autoScroll.value
}
const clearLogs = () => {
logs.value = []
}
const scrollToBottom = () => {
if (logContainer.value && autoScroll.value) {
nextTick(() => {
logContainer.value!.scrollTop = logContainer.value!.scrollHeight
})
}
}
// Polling management
let pollInterval: number | null = null
const startPolling = () => {
if (pollInterval) clearInterval(pollInterval)
pollInterval = setInterval(() => {
fetchLogs()
scrollToBottom()
}, pollingInterval.value) as unknown as number
}
const stopPolling = () => {
if (pollInterval) {
clearInterval(pollInterval)
pollInterval = null
}
}
// Lifecycle hooks
onMounted(() => {
// Initial load
fetchLogs()
// Start polling
startPolling()
// Generate initial logs
for (let i = 0; i < 10; i++) {
const log = generateMockLog()
log.timestamp = new Date(Date.now() - (10 - i) * 60000) // Staggered times
log.isNew = false
logs.value.push(log)
}
isLoading.value = false
})
onUnmounted(() => {
stopPolling()
})
</script>
<style scoped>
.log-entries-container {
overflow-y: auto;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 4px;
background-color: rgba(255, 255, 255, 0.05);
}
.log-entry {
border-left: 3px solid;
background-color: rgba(255, 255, 255, 0.02);
transition: background-color 0.2s;
}
.log-entry:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.log-entry.new-entry {
background-color: rgba(33, 150, 243, 0.1);
border-left-color: #2196f3;
}
.h-100 {
height: 100%;
}
</style>

View File

@@ -1,474 +0,0 @@
<template>
<v-card
color="teal-darken-1"
variant="tonal"
class="h-100 d-flex flex-column"
>
<v-card-title class="d-flex align-center justify-space-between">
<div class="d-flex align-center">
<v-icon icon="mdi-chart-line" class="mr-2"></v-icon>
<span class="text-subtitle-1 font-weight-bold">Financial Overview</span>
</div>
<div class="d-flex align-center">
<v-chip size="small" color="green" class="mr-2">
<v-icon icon="mdi-cash" size="small" class="mr-1"></v-icon>
Live
</v-chip>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn
size="x-small"
variant="text"
v-bind="props"
class="text-caption"
>
{{ selectedPeriod }}
<v-icon icon="mdi-chevron-down" size="small"></v-icon>
</v-btn>
</template>
<v-list density="compact">
<v-list-item
v-for="period in periodOptions"
:key="period.value"
@click="selectedPeriod = period.value"
>
<v-list-item-title>{{ period.label }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</div>
</v-card-title>
<v-card-text class="flex-grow-1 pa-0">
<div class="pa-4">
<!-- Key Financial Metrics -->
<v-row dense class="mb-4">
<v-col cols="6" sm="3">
<v-card variant="outlined" class="pa-2 text-center">
<div class="text-h6 font-weight-bold text-primary">{{ formatCurrency(revenue) }}</div>
<div class="text-caption text-grey">Revenue</div>
<div class="text-caption" :class="revenueGrowth >= 0 ? 'text-success' : 'text-error'">
<v-icon :icon="revenueGrowth >= 0 ? 'mdi-arrow-up' : 'mdi-arrow-down'" size="x-small" class="mr-1"></v-icon>
{{ Math.abs(revenueGrowth) }}%
</div>
</v-card>
</v-col>
<v-col cols="6" sm="3">
<v-card variant="outlined" class="pa-2 text-center">
<div class="text-h6 font-weight-bold text-error">{{ formatCurrency(expenses) }}</div>
<div class="text-caption text-grey">Expenses</div>
<div class="text-caption" :class="expenseGrowth <= 0 ? 'text-success' : 'text-error'">
<v-icon :icon="expenseGrowth <= 0 ? 'mdi-arrow-down' : 'mdi-arrow-up'" size="x-small" class="mr-1"></v-icon>
{{ Math.abs(expenseGrowth) }}%
</div>
</v-card>
</v-col>
<v-col cols="6" sm="3">
<v-card variant="outlined" class="pa-2 text-center">
<div class="text-h6 font-weight-bold text-success">{{ formatCurrency(profit) }}</div>
<div class="text-caption text-grey">Profit</div>
<div class="text-caption" :class="profitMargin >= 20 ? 'text-success' : profitMargin >= 10 ? 'text-warning' : 'text-error'">
{{ profitMargin }}% margin
</div>
</v-card>
</v-col>
<v-col cols="6" sm="3">
<v-card variant="outlined" class="pa-2 text-center">
<div class="text-h6 font-weight-bold text-indigo">{{ formatCurrency(cashFlow) }}</div>
<div class="text-caption text-grey">Cash Flow</div>
<div class="text-caption" :class="cashFlow >= 0 ? 'text-success' : 'text-error'">
{{ cashFlow >= 0 ? 'Positive' : 'Negative' }}
</div>
</v-card>
</v-col>
</v-row>
<!-- Revenue vs Expenses Chart -->
<div class="mb-4">
<div class="text-subtitle-2 font-weight-medium mb-2">Revenue vs Expenses</div>
<div class="chart-container" style="height: 200px;">
<canvas ref="revenueExpenseChart"></canvas>
</div>
</div>
<!-- Expense Breakdown -->
<div class="mb-4">
<div class="text-subtitle-2 font-weight-medium mb-2">Expense Breakdown</div>
<v-row dense>
<v-col v-for="category in expenseCategories" :key="category.name" cols="6" sm="3">
<v-card variant="outlined" class="pa-2">
<div class="d-flex align-center justify-space-between">
<div>
<div class="text-caption font-weight-medium">{{ category.name }}</div>
<div class="text-caption text-grey">{{ formatCurrency(category.amount) }}</div>
</div>
<div class="text-right">
<div class="text-caption">{{ category.percentage }}%</div>
<v-progress-linear
:model-value="category.percentage"
height="4"
:color="category.color"
class="mt-1"
></v-progress-linear>
</div>
</div>
</v-card>
</v-col>
</v-row>
</div>
<!-- Regional Performance -->
<div>
<div class="text-subtitle-2 font-weight-medium mb-2">Regional Performance</div>
<v-table density="compact" class="elevation-1">
<thead>
<tr>
<th class="text-left">Region</th>
<th class="text-right">Revenue</th>
<th class="text-right">Growth</th>
<th class="text-right">Margin</th>
</tr>
</thead>
<tbody>
<tr v-for="region in regionalPerformance" :key="region.name">
<td class="text-left">
<v-chip size="x-small" :color="getRegionColor(region.name)" class="mr-1">
{{ region.name }}
</v-chip>
</td>
<td class="text-right">{{ formatCurrency(region.revenue) }}</td>
<td class="text-right" :class="region.growth >= 0 ? 'text-success' : 'text-error'">
{{ region.growth >= 0 ? '+' : '' }}{{ region.growth }}%
</td>
<td class="text-right" :class="region.margin >= 20 ? 'text-success' : region.margin >= 10 ? 'text-warning' : 'text-error'">
{{ region.margin }}%
</td>
</tr>
</tbody>
</v-table>
</div>
</div>
</v-card-text>
<v-card-actions class="pa-3">
<div class="d-flex justify-space-between align-center w-100">
<div class="text-caption">
<v-icon icon="mdi-calendar" size="small" class="mr-1"></v-icon>
Last updated: {{ lastUpdated }}
</div>
<div class="d-flex">
<v-btn
size="x-small"
variant="text"
@click="refreshData"
:loading="isRefreshing"
>
<v-icon icon="mdi-refresh" size="small" class="mr-1"></v-icon>
Refresh
</v-btn>
<v-btn
size="x-small"
variant="text"
@click="exportData"
class="ml-2"
>
<v-icon icon="mdi-download" size="small" class="mr-1"></v-icon>
Export
</v-btn>
</div>
</div>
</v-card-actions>
</v-card>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
import { Chart, registerables } from 'chart.js'
// Register Chart.js components
Chart.register(...registerables)
// Types
interface ExpenseCategory {
name: string
amount: number
percentage: number
color: string
}
interface RegionalPerformance {
name: string
revenue: number
growth: number
margin: number
}
// State
const selectedPeriod = ref('month')
const isRefreshing = ref(false)
const revenueExpenseChart = ref<HTMLCanvasElement | null>(null)
let chartInstance: Chart | null = null
// Period options
const periodOptions = [
{ label: 'Last 7 Days', value: 'week' },
{ label: 'Last Month', value: 'month' },
{ label: 'Last Quarter', value: 'quarter' },
{ label: 'Last Year', value: 'year' }
]
// Mock financial data
const revenue = ref(1254300)
const expenses = ref(892500)
const revenueGrowth = ref(12.5)
const expenseGrowth = ref(8.2)
const cashFlow = ref(361800)
// Computed properties
const profit = computed(() => revenue.value - expenses.value)
const profitMargin = computed(() => {
if (revenue.value === 0) return 0
return Math.round((profit.value / revenue.value) * 100)
})
const expenseCategories = ref<ExpenseCategory[]>([
{ name: 'Personnel', amount: 425000, percentage: 48, color: 'indigo' },
{ name: 'Operations', amount: 215000, percentage: 24, color: 'blue' },
{ name: 'Marketing', amount: 125000, percentage: 14, color: 'green' },
{ name: 'Technology', amount: 85000, percentage: 10, color: 'orange' },
{ name: 'Other', amount: 42500, percentage: 5, color: 'grey' }
])
const regionalPerformance = ref<RegionalPerformance[]>([
{ name: 'GB', revenue: 450000, growth: 15.2, margin: 22 },
{ name: 'EU', revenue: 385000, growth: 8.7, margin: 18 },
{ name: 'US', revenue: 275000, growth: 21.5, margin: 25 },
{ name: 'OC', revenue: 144300, growth: 5.3, margin: 12 }
])
const lastUpdated = computed(() => {
const now = new Date()
return now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
})
// Helper functions
const formatCurrency = (amount: number) => {
if (amount >= 1000000) {
return `${(amount / 1000000).toFixed(1)}M`
} else if (amount >= 1000) {
return `${(amount / 1000).toFixed(0)}K`
}
return `${amount.toFixed(0)}`
}
const getRegionColor = (region: string) => {
switch (region) {
case 'GB': return 'blue'
case 'EU': return 'green'
case 'US': return 'red'
case 'OC': return 'orange'
default: return 'grey'
}
}
// Chart functions
const initChart = () => {
if (!revenueExpenseChart.value) return
// Destroy existing chart
if (chartInstance) {
chartInstance.destroy()
}
const ctx = revenueExpenseChart.value.getContext('2d')
if (!ctx) return
// Generate mock data based on selected period
const labels = generateChartLabels()
const revenueData = generateRevenueData()
const expenseData = generateExpenseData()
chartInstance = new Chart(ctx, {
type: 'line',
data: {
labels,
datasets: [
{
label: 'Revenue',
data: revenueData,
borderColor: '#4CAF50',
backgroundColor: 'rgba(76, 175, 80, 0.1)',
tension: 0.4,
fill: true
},
{
label: 'Expenses',
data: expenseData,
borderColor: '#F44336',
backgroundColor: 'rgba(244, 67, 54, 0.1)',
tension: 0.4,
fill: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
labels: {
usePointStyle: true,
padding: 10
}
},
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: (context) => {
return `${context.dataset.label}: ${formatCurrency(context.raw as number)}`
}
}
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
beginAtZero: true,
ticks: {
callback: (value) => formatCurrency(value as number)
}
}
}
}
})
}
const generateChartLabels = () => {
switch (selectedPeriod.value) {
case 'week':
return ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
case 'month':
return ['Week 1', 'Week 2', 'Week 3', 'Week 4']
case 'quarter':
return ['Jan-Mar', 'Apr-Jun', 'Jul-Sep', 'Oct-Dec']
case 'year':
return ['Q1', 'Q2', 'Q3', 'Q4']
default:
return ['Week 1', 'Week 2', 'Week 3', 'Week 4']
}
}
const generateRevenueData = () => {
const base = 100000
const variance = 0.3
const count = generateChartLabels().length
return Array.from({ length: count }, (_, i) => {
const growth = 1 + (i * 0.1)
const random = 1 + (Math.random() * variance * 2 - variance)
return Math.round(base * growth * random)
})
}
const generateExpenseData = () => {
const base = 70000
const variance = 0.2
const count = generateChartLabels().length
return Array.from({ length: count }, (_, i) => {
const growth = 1 + (i * 0.05)
const random = 1 + (Math.random() * variance * 2 - variance)
return Math.round(base * growth * random)
})
}
// Actions
const refreshData = () => {
isRefreshing.value = true
// Simulate API call
setTimeout(() => {
// Update with new random data
revenue.value = Math.round(1254300 * (1 + Math.random() * 0.1 - 0.05))
expenses.value = Math.round(892500 * (1 + Math.random() * 0.1 - 0.05))
revenueGrowth.value = parseFloat((Math.random() * 20 - 5).toFixed(1))
expenseGrowth.value = parseFloat((Math.random() * 15 - 5).toFixed(1))
cashFlow.value = revenue.value - expenses.value
// Update chart
initChart()
isRefreshing.value = false
}, 1000)
}
const exportData = () => {
// Simulate export
const data = {
revenue: revenue.value,
expenses: expenses.value,
profit: profit.value,
profitMargin: profitMargin.value,
period: selectedPeriod.value,
timestamp: new Date().toISOString()
}
const dataStr = JSON.stringify(data, null, 2)
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr)
const exportFileDefaultName = `financial_report_${new Date().toISOString().split('T')[0]}.json`
const linkElement = document.createElement('a')
linkElement.setAttribute('href', dataUri)
linkElement.setAttribute('download', exportFileDefaultName)
linkElement.click()
}
// Lifecycle hooks
onMounted(() => {
nextTick(() => {
initChart()
})
})
onUnmounted(() => {
if (chartInstance) {
chartInstance.destroy()
}
})
// Watch for period changes
watch(selectedPeriod, () => {
initChart()
})
</script>
<style scoped>
.chart-container {
position: relative;
width: 100%;
}
.h-100 {
height: 100%;
}
.v-table {
background: transparent;
}
.v-table :deep(thead) th {
background-color: rgba(0, 0, 0, 0.02);
font-weight: 600;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
</style>

View File

@@ -1,497 +0,0 @@
<template>
<v-card
color="orange-darken-1"
variant="tonal"
class="h-100 d-flex flex-column"
>
<v-card-title class="d-flex align-center justify-space-between">
<div class="d-flex align-center">
<v-icon icon="mdi-account-group" class="mr-2"></v-icon>
<span class="text-subtitle-1 font-weight-bold">Sales Pipeline</span>
</div>
<div class="d-flex align-center">
<v-chip size="small" color="green" class="mr-2">
<v-icon icon="mdi-trending-up" size="small" class="mr-1"></v-icon>
Active
</v-chip>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn
size="x-small"
variant="text"
v-bind="props"
class="text-caption"
>
{{ selectedTeam }}
<v-icon icon="mdi-chevron-down" size="small"></v-icon>
</v-btn>
</template>
<v-list density="compact">
<v-list-item
v-for="team in teamOptions"
:key="team.value"
@click="selectedTeam = team.value"
>
<v-list-item-title>{{ team.label }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</div>
</v-card-title>
<v-card-text class="flex-grow-1 pa-0">
<div class="pa-4">
<!-- Pipeline Stages -->
<div class="mb-4">
<div class="text-subtitle-2 font-weight-medium mb-2">Pipeline Stages</div>
<v-row dense>
<v-col v-for="stage in pipelineStages" :key="stage.name" cols="6" sm="3">
<v-card variant="outlined" class="pa-2">
<div class="d-flex align-center justify-space-between">
<div>
<div class="text-caption font-weight-medium">{{ stage.name }}</div>
<div class="text-caption text-grey">{{ stage.count }} leads</div>
</div>
<div class="text-right">
<div class="text-caption" :class="`text-${stage.color}`">{{ stage.conversion }}%</div>
<v-progress-linear
:model-value="stage.conversion"
height="4"
:color="stage.color"
class="mt-1"
></v-progress-linear>
</div>
</div>
<div class="text-caption text-grey mt-1">
Avg: {{ stage.avgDays }} days
</div>
</v-card>
</v-col>
</v-row>
</div>
<!-- Conversion Funnel Chart -->
<div class="mb-4">
<div class="text-subtitle-2 font-weight-medium mb-2">Conversion Funnel</div>
<div class="chart-container" style="height: 180px;">
<canvas ref="funnelChart"></canvas>
</div>
</div>
<!-- Top Performers -->
<div class="mb-4">
<div class="text-subtitle-2 font-weight-medium mb-2">Top Performers</div>
<v-table density="compact" class="elevation-1">
<thead>
<tr>
<th class="text-left">Salesperson</th>
<th class="text-right">Leads</th>
<th class="text-right">Converted</th>
<th class="text-right">Rate</th>
<th class="text-right">Revenue</th>
</tr>
</thead>
<tbody>
<tr v-for="person in topPerformers" :key="person.name">
<td class="text-left">
<div class="d-flex align-center">
<v-avatar size="24" class="mr-2">
<v-img :src="person.avatar" :alt="person.name"></v-img>
</v-avatar>
<span class="text-caption">{{ person.name }}</span>
</div>
</td>
<td class="text-right">{{ person.leads }}</td>
<td class="text-right">{{ person.converted }}</td>
<td class="text-right" :class="person.conversionRate >= 30 ? 'text-success' : person.conversionRate >= 20 ? 'text-warning' : 'text-error'">
{{ person.conversionRate }}%
</td>
<td class="text-right font-weight-medium">{{ formatCurrency(person.revenue) }}</td>
</tr>
</tbody>
</v-table>
</div>
<!-- Recent Activities -->
<div>
<div class="text-subtitle-2 font-weight-medium mb-2">Recent Activities</div>
<div class="activity-list">
<div v-for="activity in recentActivities" :key="activity.id" class="activity-item mb-2 pa-2">
<div class="d-flex align-center">
<v-avatar size="28" class="mr-2">
<v-img :src="activity.avatar" :alt="activity.salesperson"></v-img>
</v-avatar>
<div class="flex-grow-1">
<div class="text-caption">
<span class="font-weight-medium">{{ activity.salesperson }}</span>
{{ activity.action }}
<span class="font-weight-medium">{{ activity.client }}</span>
</div>
<div class="d-flex align-center mt-1">
<v-chip size="x-small" :color="getStageColor(activity.stage)" class="mr-1">
{{ activity.stage }}
</v-chip>
<span class="text-caption text-grey">{{ formatTime(activity.timestamp) }}</span>
</div>
</div>
<v-icon :icon="getActivityIcon(activity.type)" size="small" :color="getActivityColor(activity.type)"></v-icon>
</div>
</div>
</div>
</div>
</div>
</v-card-text>
<v-card-actions class="pa-3">
<div class="d-flex justify-space-between align-center w-100">
<div class="text-caption">
<v-icon icon="mdi-chart-timeline" size="small" class="mr-1"></v-icon>
Total Pipeline: {{ formatCurrency(totalPipelineValue) }}
</div>
<div class="d-flex">
<v-btn
size="x-small"
variant="text"
@click="refreshPipeline"
:loading="isRefreshing"
>
<v-icon icon="mdi-refresh" size="small" class="mr-1"></v-icon>
Refresh
</v-btn>
<v-btn
size="x-small"
variant="text"
@click="addNewLead"
class="ml-2"
>
<v-icon icon="mdi-plus" size="small" class="mr-1"></v-icon>
New Lead
</v-btn>
</div>
</div>
</v-card-actions>
</v-card>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { Chart, registerables } from 'chart.js'
// Register Chart.js components
Chart.register(...registerables)
// Types
interface PipelineStage {
name: string
count: number
conversion: number
avgDays: number
color: string
}
interface SalesPerson {
name: string
avatar: string
leads: number
converted: number
conversionRate: number
revenue: number
}
interface Activity {
id: string
salesperson: string
avatar: string
action: string
client: string
stage: string
type: 'call' | 'meeting' | 'email' | 'proposal' | 'closed'
timestamp: Date
}
// State
const selectedTeam = ref('all')
const isRefreshing = ref(false)
const funnelChart = ref<HTMLCanvasElement | null>(null)
let chartInstance: Chart | null = null
// Team options
const teamOptions = [
{ label: 'All Teams', value: 'all' },
{ label: 'Enterprise', value: 'enterprise' },
{ label: 'SMB', value: 'smb' },
{ label: 'Government', value: 'government' }
]
// Pipeline data
const pipelineStages = ref<PipelineStage[]>([
{ name: 'Prospecting', count: 142, conversion: 65, avgDays: 3, color: 'blue' },
{ name: 'Qualification', count: 92, conversion: 45, avgDays: 7, color: 'indigo' },
{ name: 'Proposal', count: 41, conversion: 30, avgDays: 14, color: 'orange' },
{ name: 'Negotiation', count: 28, conversion: 20, avgDays: 21, color: 'red' },
{ name: 'Closed Won', count: 12, conversion: 15, avgDays: 30, color: 'green' }
])
const topPerformers = ref<SalesPerson[]>([
{ name: 'Alex Johnson', avatar: 'https://i.pravatar.cc/150?img=1', leads: 45, converted: 18, conversionRate: 40, revenue: 125000 },
{ name: 'Maria Garcia', avatar: 'https://i.pravatar.cc/150?img=2', leads: 38, converted: 15, conversionRate: 39, revenue: 112000 },
{ name: 'David Chen', avatar: 'https://i.pravatar.cc/150?img=3', leads: 42, converted: 16, conversionRate: 38, revenue: 108000 },
{ name: 'Sarah Williams', avatar: 'https://i.pravatar.cc/150?img=4', leads: 35, converted: 13, conversionRate: 37, revenue: 98000 }
])
const recentActivities = ref<Activity[]>([
{ id: '1', salesperson: 'Alex Johnson', avatar: 'https://i.pravatar.cc/150?img=1', action: 'sent proposal to', client: 'TechCorp Inc.', stage: 'Proposal', type: 'proposal', timestamp: new Date(Date.now() - 3600000) },
{ id: '2', salesperson: 'Maria Garcia', avatar: 'https://i.pravatar.cc/150?img=2', action: 'closed deal with', client: 'Global Motors', stage: 'Closed Won', type: 'closed', timestamp: new Date(Date.now() - 7200000) },
{ id: '3', salesperson: 'David Chen', avatar: 'https://i.pravatar.cc/150?img=3', action: 'scheduled meeting with', client: 'HealthPlus', stage: 'Qualification', type: 'meeting', timestamp: new Date(Date.now() - 10800000) },
{ id: '4', salesperson: 'Sarah Williams', avatar: 'https://i.pravatar.cc/150?img=4', action: 'called', client: 'EduTech Solutions', stage: 'Prospecting', type: 'call', timestamp: new Date(Date.now() - 14400000) }
])
// Computed properties
const totalPipelineValue = computed(() => {
return pipelineStages.value.reduce((total, stage) => {
// Estimate value based on stage
const stageValue = stage.count * 5000 // Average deal size
return total + stageValue
}, 0)
})
// Helper functions
const formatCurrency = (amount: number) => {
if (amount >= 1000000) {
return `${(amount / 1000000).toFixed(1)}M`
} else if (amount >= 1000) {
return `${(amount / 1000).toFixed(0)}K`
}
return `${amount.toFixed(0)}`
}
const formatTime = (timestamp: Date) => {
const now = new Date()
const diff = now.getTime() - timestamp.getTime()
if (diff < 60000) return 'Just now'
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`
return timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
}
const getStageColor = (stage: string) => {
switch (stage.toLowerCase()) {
case 'prospecting': return 'blue'
case 'qualification': return 'indigo'
case 'proposal': return 'orange'
case 'negotiation': return 'red'
case 'closed won': return 'green'
default: return 'grey'
}
}
const getActivityIcon = (type: Activity['type']) => {
switch (type) {
case 'call': return 'mdi-phone'
case 'meeting': return 'mdi-calendar'
case 'email': return 'mdi-email'
case 'proposal': return 'mdi-file-document'
case 'closed': return 'mdi-check-circle'
default: return 'mdi-help-circle'
}
}
const getActivityColor = (type: Activity['type']) => {
switch (type) {
case 'call': return 'blue'
case 'meeting': return 'indigo'
case 'email': return 'green'
case 'proposal': return 'orange'
case 'closed': return 'success'
default: return 'grey'
}
}
// Chart functions
const initChart = () => {
if (!funnelChart.value) return
// Destroy existing chart
if (chartInstance) {
chartInstance.destroy()
}
const ctx = funnelChart.value.getContext('2d')
if (!ctx) return
// Prepare funnel data
const labels = pipelineStages.value.map(stage => stage.name)
const data = pipelineStages.value.map(stage => stage.count)
const backgroundColors = pipelineStages.value.map(stage => {
switch (stage.color) {
case 'blue': return '#2196F3'
case 'indigo': return '#3F51B5'
case 'orange': return '#FF9800'
case 'red': return '#F44336'
case 'green': return '#4CAF50'
default: return '#9E9E9E'
}
})
chartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels,
datasets: [{
label: 'Leads',
data,
backgroundColor: backgroundColors,
borderColor: backgroundColors.map(color => color.replace('0.8', '1')),
borderWidth: 1,
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
indexAxis: 'y', // Horizontal bar chart for funnel
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: (context) => {
const stage = pipelineStages.value[context.dataIndex]
return `${context.dataset.label}: ${context.raw} (${stage.conversion}% conversion)`
}
}
}
},
scales: {
x: {
beginAtZero: true,
grid: {
display: false
},
title: {
display: true,
text: 'Number of Leads'
}
},
y: {
grid: {
display: false
}
}
}
}
})
}
// Actions
const refreshPipeline = () => {
isRefreshing.value = true
// Simulate API call
setTimeout(() => {
// Update with new random data
pipelineStages.value.forEach(stage => {
stage.count = Math.round(stage.count * (1 + Math.random() * 0.2 - 0.1))
stage.conversion = Math.round(stage.conversion * (1 + Math.random() * 0.1 - 0.05))
})
// Update top performers
topPerformers.value.forEach(person => {
person.leads = Math.round(person.leads * (1 + Math.random() * 0.1 - 0.05))
person.converted = Math.round(person.converted * (1 + Math.random() * 0.1 - 0.05))
person.conversionRate = Math.round((person.converted / person.leads) * 100)
person.revenue = Math.round(person.revenue * (1 + Math.random() * 0.15 - 0.05))
})
// Add new activity
const activities = ['called', 'emailed', 'met with', 'sent proposal to', 'closed deal with']
const clients = ['TechCorp', 'Global Motors', 'HealthPlus', 'EduTech', 'FinancePro', 'AutoGroup']
const salespeople = topPerformers.value
const newActivity: Activity = {
id: `act_${Date.now()}`,
salesperson: salespeople[Math.floor(Math.random() * salespeople.length)].name,
avatar: `https://i.pravatar.cc/150?img=${Math.floor(Math.random() * 10) + 1}`,
action: activities[Math.floor(Math.random() * activities.length)],
client: clients[Math.floor(Math.random() * clients.length)],
stage: pipelineStages.value[Math.floor(Math.random() * pipelineStages.value.length)].name,
type: ['call', 'meeting', 'email', 'proposal', 'closed'][Math.floor(Math.random() * 5)] as Activity['type'],
timestamp: new Date()
}
recentActivities.value.unshift(newActivity)
// Keep only last 5 activities
if (recentActivities.value.length > 5) {
recentActivities.value = recentActivities.value.slice(0, 5)
}
// Update chart
initChart()
isRefreshing.value = false
}, 1000)
}
const addNewLead = () => {
// Simulate adding new lead
pipelineStages.value[0].count += 1
// Show notification
console.log('New lead added to pipeline')
}
// Lifecycle hooks
onMounted(() => {
nextTick(() => {
initChart()
})
})
onUnmounted(() => {
if (chartInstance) {
chartInstance.destroy()
}
})
</script>
<style scoped>
.chart-container {
position: relative;
width: 100%;
}
.h-100 {
height: 100%;
}
.activity-list {
max-height: 150px;
overflow-y: auto;
}
.activity-item {
border-left: 3px solid;
background-color: rgba(255, 255, 255, 0.02);
transition: background-color 0.2s;
border-left-color: #FF9800; /* Orange accent */
}
.activity-item:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.v-table {
background: transparent;
}
.v-table :deep(thead) th {
background-color: rgba(0, 0, 0, 0.02);
font-weight: 600;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
</style>

View File

@@ -1,202 +0,0 @@
<template>
<TileWrapper
title="Geographical Map"
subtitle="Service moderation map"
icon="map"
:loading="loading"
>
<div class="service-map-tile">
<div class="mini-map">
<div class="map-placeholder">
<div class="map-grid">
<div
v-for="point in mapPoints"
:key="point.id"
class="map-point"
:class="point.status"
:style="{
left: `${point.x}%`,
top: `${point.y}%`
}"
:title="point.name"
></div>
</div>
</div>
</div>
<div class="tile-stats">
<div class="stat">
<span class="stat-label">Pending in Scope</span>
<span class="stat-value">{{ pendingCount }}</span>
</div>
<div class="stat">
<span class="stat-label">Scope</span>
<span class="stat-value scope">{{ scopeLabel }}</span>
</div>
</div>
<div class="tile-actions">
<button @click="navigateToMap" class="btn-primary">
Open Full Map
</button>
<button @click="refresh" class="btn-secondary">
Refresh
</button>
</div>
</div>
</TileWrapper>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import TileWrapper from '~/components/TileWrapper.vue'
import { useServiceMap } from '~/composables/useServiceMap'
const router = useRouter()
const { pendingServices, scopeLabel } = useServiceMap()
const loading = ref(false)
const pendingCount = computed(() => pendingServices.value.length)
// Generate random points for the mini map visualization
const mapPoints = computed(() => {
return pendingServices.value.slice(0, 8).map((service, index) => ({
id: service.id,
name: service.name,
status: service.status,
x: 10 + (index % 4) * 25 + Math.random() * 10,
y: 10 + Math.floor(index / 4) * 30 + Math.random() * 10
}))
})
const navigateToMap = () => {
router.push('/moderation-map')
}
const refresh = () => {
loading.value = true
// Simulate API call
setTimeout(() => {
loading.value = false
}, 1000)
}
</script>
<style scoped>
.service-map-tile {
display: flex;
flex-direction: column;
height: 100%;
}
.mini-map {
flex: 1;
margin-bottom: 15px;
}
.map-placeholder {
width: 100%;
height: 150px;
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
border-radius: 8px;
position: relative;
overflow: hidden;
border: 1px solid #90caf9;
}
.map-grid {
position: relative;
width: 100%;
height: 100%;
}
.map-point {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
transform: translate(-50%, -50%);
border: 2px solid white;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.map-point.pending {
background-color: #3b82f6;
}
.map-point.approved {
background-color: #28a745;
}
.tile-stats {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
}
.stat {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.stat-label {
font-size: 0.85rem;
color: #666;
margin-bottom: 4px;
}
.stat-value {
font-size: 1.5rem;
font-weight: bold;
color: #333;
}
.stat-value.scope {
font-size: 1rem;
color: #4a90e2;
background: #e3f2fd;
padding: 4px 8px;
border-radius: 12px;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tile-actions {
display: flex;
gap: 10px;
}
.btn-primary {
flex: 2;
background-color: #4a90e2;
color: white;
border: none;
padding: 10px;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
transition: background-color 0.2s;
}
.btn-primary:hover {
background-color: #3a7bc8;
}
.btn-secondary {
flex: 1;
background-color: #f8f9fa;
color: #495057;
border: 1px solid #dee2e6;
padding: 10px;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-secondary:hover {
background-color: #e9ecef;
}
</style>

View File

@@ -1,590 +0,0 @@
<template>
<v-card
color="blue-grey-darken-1"
variant="tonal"
class="h-100 d-flex flex-column"
>
<v-card-title class="d-flex align-center justify-space-between">
<div class="d-flex align-center">
<v-icon icon="mdi-server" class="mr-2"></v-icon>
<span class="text-subtitle-1 font-weight-bold">System Health</span>
</div>
<div class="d-flex align-center">
<v-chip size="small" :color="overallStatusColor" class="mr-2">
<v-icon :icon="overallStatusIcon" size="small" class="mr-1"></v-icon>
{{ overallStatusText }}
</v-chip>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn
size="x-small"
variant="text"
v-bind="props"
class="text-caption"
>
{{ selectedEnvironment }}
<v-icon icon="mdi-chevron-down" size="small"></v-icon>
</v-btn>
</template>
<v-list density="compact">
<v-list-item
v-for="env in environmentOptions"
:key="env.value"
@click="selectedEnvironment = env.value"
>
<v-list-item-title>{{ env.label }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</div>
</v-card-title>
<v-card-text class="flex-grow-1 pa-0">
<div class="pa-4">
<!-- System Status Overview -->
<div class="mb-4">
<div class="text-subtitle-2 font-weight-medium mb-2">System Status</div>
<v-row dense>
<v-col v-for="component in systemComponents" :key="component.name" cols="6" sm="3">
<v-card variant="outlined" class="pa-2">
<div class="d-flex align-center justify-space-between">
<div>
<div class="d-flex align-center">
<v-icon :icon="component.icon" size="small" :color="component.statusColor" class="mr-2"></v-icon>
<span class="text-caption font-weight-medium">{{ component.name }}</span>
</div>
<div class="text-caption text-grey mt-1">{{ component.description }}</div>
</div>
<div class="text-right">
<div class="text-caption" :class="`text-${component.statusColor}`">{{ component.status }}</div>
<div class="text-caption text-grey">{{ component.uptime }}%</div>
</div>
</div>
<!-- Response Time Indicator -->
<div v-if="component.responseTime" class="mt-2">
<div class="d-flex justify-space-between">
<span class="text-caption text-grey">Response</span>
<span class="text-caption" :class="getResponseTimeColor(component.responseTime)">{{ component.responseTime }}ms</span>
</div>
<v-progress-linear
:model-value="Math.min(component.responseTime / 10, 100)"
height="4"
:color="getResponseTimeColor(component.responseTime)"
class="mt-1"
></v-progress-linear>
</div>
</v-card>
</v-col>
</v-row>
</div>
<!-- API Response Times Chart -->
<div class="mb-4">
<div class="text-subtitle-2 font-weight-medium mb-2">API Response Times (Last 24h)</div>
<div class="chart-container" style="height: 150px;">
<canvas ref="responseTimeChart"></canvas>
</div>
</div>
<!-- Database Metrics -->
<div class="mb-4">
<div class="text-subtitle-2 font-weight-medium mb-2">Database Metrics</div>
<v-row dense>
<v-col cols="6" sm="3">
<v-card variant="outlined" class="pa-2 text-center">
<div class="text-h6 font-weight-bold" :class="databaseMetrics.connections > 80 ? 'text-error' : 'text-success'">{{ databaseMetrics.connections }}</div>
<div class="text-caption text-grey">Connections</div>
<div class="text-caption">{{ databaseMetrics.activeConnections }} active</div>
</v-card>
</v-col>
<v-col cols="6" sm="3">
<v-card variant="outlined" class="pa-2 text-center">
<div class="text-h6 font-weight-bold" :class="databaseMetrics.queryTime > 500 ? 'text-error' : databaseMetrics.queryTime > 200 ? 'text-warning' : 'text-success'">{{ databaseMetrics.queryTime }}ms</div>
<div class="text-caption text-grey">Avg Query Time</div>
<div class="text-caption">{{ databaseMetrics.queriesPerSecond }} qps</div>
</v-card>
</v-col>
<v-col cols="6" sm="3">
<v-card variant="outlined" class="pa-2 text-center">
<div class="text-h6 font-weight-bold" :class="databaseMetrics.cacheHitRate < 80 ? 'text-error' : databaseMetrics.cacheHitRate < 90 ? 'text-warning' : 'text-success'">{{ databaseMetrics.cacheHitRate }}%</div>
<div class="text-caption text-grey">Cache Hit Rate</div>
<div class="text-caption">{{ formatBytes(databaseMetrics.cacheSize) }} cache</div>
</v-card>
</v-col>
<v-col cols="6" sm="3">
<v-card variant="outlined" class="pa-2 text-center">
<div class="text-h6 font-weight-bold" :class="databaseMetrics.replicationLag > 1000 ? 'text-error' : databaseMetrics.replicationLag > 500 ? 'text-warning' : 'text-success'">{{ databaseMetrics.replicationLag }}ms</div>
<div class="text-caption text-grey">Replication Lag</div>
<div class="text-caption">{{ databaseMetrics.replicationStatus }}</div>
</v-card>
</v-col>
</v-row>
</div>
<!-- Server Resources -->
<div>
<div class="text-subtitle-2 font-weight-medium mb-2">Server Resources</div>
<v-row dense>
<v-col cols="12" sm="6">
<v-card variant="outlined" class="pa-3">
<div class="d-flex justify-space-between align-center mb-2">
<span class="text-caption font-weight-medium">CPU Usage</span>
<span class="text-caption" :class="serverResources.cpu > 80 ? 'text-error' : serverResources.cpu > 60 ? 'text-warning' : 'text-success'">{{ serverResources.cpu }}%</span>
</div>
<v-progress-linear
:model-value="serverResources.cpu"
height="8"
:color="serverResources.cpu > 80 ? 'error' : serverResources.cpu > 60 ? 'warning' : 'success'"
rounded
></v-progress-linear>
<div class="text-caption text-grey mt-1">{{ serverResources.cpuCores }} cores @ {{ serverResources.cpuFrequency }}GHz</div>
</v-card>
</v-col>
<v-col cols="12" sm="6">
<v-card variant="outlined" class="pa-3">
<div class="d-flex justify-space-between align-center mb-2">
<span class="text-caption font-weight-medium">Memory Usage</span>
<span class="text-caption" :class="serverResources.memory > 80 ? 'text-error' : serverResources.memory > 60 ? 'text-warning' : 'text-success'">{{ serverResources.memory }}%</span>
</div>
<v-progress-linear
:model-value="serverResources.memory"
height="8"
:color="serverResources.memory > 80 ? 'error' : serverResources.memory > 60 ? 'warning' : 'success'"
rounded
></v-progress-linear>
<div class="text-caption text-grey mt-1">{{ formatBytes(serverResources.memoryUsed) }} / {{ formatBytes(serverResources.memoryTotal) }}</div>
</v-card>
</v-col>
<v-col cols="12" sm="6">
<v-card variant="outlined" class="pa-3">
<div class="d-flex justify-space-between align-center mb-2">
<span class="text-caption font-weight-medium">Disk I/O</span>
<span class="text-caption" :class="serverResources.diskIO > 80 ? 'text-error' : serverResources.diskIO > 60 ? 'text-warning' : 'text-success'">{{ serverResources.diskIO }}%</span>
</div>
<v-progress-linear
:model-value="serverResources.diskIO"
height="8"
:color="serverResources.diskIO > 80 ? 'error' : serverResources.diskIO > 60 ? 'warning' : 'success'"
rounded
></v-progress-linear>
<div class="text-caption text-grey mt-1">{{ formatBytes(serverResources.diskRead) }}/s read, {{ formatBytes(serverResources.diskWrite) }}/s write</div>
</v-card>
</v-col>
<v-col cols="12" sm="6">
<v-card variant="outlined" class="pa-3">
<div class="d-flex justify-space-between align-center mb-2">
<span class="text-caption font-weight-medium">Network</span>
<span class="text-caption" :class="serverResources.network > 80 ? 'text-error' : serverResources.network > 60 ? 'text-warning' : 'text-success'">{{ serverResources.network }}%</span>
</div>
<v-progress-linear
:model-value="serverResources.network"
height="8"
:color="serverResources.network > 80 ? 'error' : serverResources.network > 60 ? 'warning' : 'success'"
rounded
></v-progress-linear>
<div class="text-caption text-grey mt-1">{{ formatBytes(serverResources.networkIn) }}/s in, {{ formatBytes(serverResources.networkOut) }}/s out</div>
</v-card>
</v-col>
</v-row>
</div>
</div>
</v-card-text>
<v-card-actions class="pa-3">
<div class="d-flex justify-space-between align-center w-100">
<div class="text-caption">
<v-icon icon="mdi-clock" size="small" class="mr-1"></v-icon>
Last check: {{ lastCheckTime }}
<v-chip size="x-small" color="green" class="ml-2">
Auto-refresh: {{ refreshInterval }}s
</v-chip>
</div>
<div class="d-flex">
<v-btn
size="x-small"
variant="text"
@click="refreshHealth"
:loading="isRefreshing"
>
<v-icon icon="mdi-refresh" size="small" class="mr-1"></v-icon>
Refresh
</v-btn>
<v-btn
size="x-small"
variant="text"
@click="toggleAutoRefresh"
:color="autoRefresh ? 'primary' : 'grey'"
class="ml-2"
>
<v-icon :icon="autoRefresh ? 'mdi-pause' : 'mdi-play'" size="small" class="mr-1"></v-icon>
{{ autoRefresh ? 'Pause' : 'Resume' }}
</v-btn>
</div>
</div>
</v-card-actions>
</v-card>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { Chart, registerables } from 'chart.js'
// Register Chart.js components
Chart.register(...registerables)
// Types
interface SystemComponent {
name: string
description: string
status: 'healthy' | 'degraded' | 'down'
statusColor: string
icon: string
uptime: number
responseTime?: number
}
interface DatabaseMetrics {
connections: number
activeConnections: number
queryTime: number
queriesPerSecond: number
cacheHitRate: number
cacheSize: number
replicationLag: number
replicationStatus: string
}
interface ServerResources {
cpu: number
cpuCores: number
cpuFrequency: number
memory: number
memoryUsed: number
memoryTotal: number
diskIO: number
diskRead: number
diskWrite: number
network: number
networkIn: number
networkOut: number
}
// State
const selectedEnvironment = ref('production')
const isRefreshing = ref(false)
const autoRefresh = ref(true)
const refreshInterval = ref(30)
const responseTimeChart = ref<HTMLCanvasElement | null>(null)
let chartInstance: Chart | null = null
let refreshTimer: number | null = null
// Environment options
const environmentOptions = [
{ label: 'Production', value: 'production' },
{ label: 'Staging', value: 'staging' },
{ label: 'Development', value: 'development' },
{ label: 'Testing', value: 'testing' }
]
// System components data
const systemComponents = ref<SystemComponent[]>([
{ name: 'API Gateway', description: 'Main API endpoint', status: 'healthy', statusColor: 'success', icon: 'mdi-api', uptime: 99.9, responseTime: 45 },
{ name: 'Database', description: 'PostgreSQL cluster', status: 'healthy', statusColor: 'success', icon: 'mdi-database', uptime: 99.95, responseTime: 120 },
{ name: 'Cache', description: 'Redis cache layer', status: 'healthy', statusColor: 'success', icon: 'mdi-memory', uptime: 99.8, responseTime: 8 },
{ name: 'Message Queue', description: 'RabbitMQ broker', status: 'degraded', statusColor: 'warning', icon: 'mdi-message-processing', uptime: 98.5, responseTime: 250 },
{ name: 'File Storage', description: 'S3-compatible storage', status: 'healthy', statusColor: 'success', icon: 'mdi-file-cloud', uptime: 99.7, responseTime: 180 },
{ name: 'Authentication', description: 'OAuth2/JWT service', status: 'healthy', statusColor: 'success', icon: 'mdi-shield-account', uptime: 99.9, responseTime: 65 },
{ name: 'Monitoring', description: 'Prometheus/Grafana', status: 'healthy', statusColor: 'success', icon: 'mdi-chart-line', uptime: 99.8, responseTime: 95 },
{ name: 'Load Balancer', description: 'Nginx reverse proxy', status: 'healthy', statusColor: 'success', icon: 'mdi-load-balancer', uptime: 99.99, responseTime: 12 }
])
const databaseMetrics = ref<DatabaseMetrics>({
connections: 64,
activeConnections: 42,
queryTime: 85,
queriesPerSecond: 1250,
cacheHitRate: 92,
cacheSize: 2147483648, // 2GB
replicationLag: 45,
replicationStatus: 'Synced'
})
const serverResources = ref<ServerResources>({
cpu: 42,
cpuCores: 8,
cpuFrequency: 3.2,
memory: 68,
memoryUsed: 1090519040, // ~1GB
memoryTotal: 17179869184, // 16GB
diskIO: 28,
diskRead: 5242880, // 5MB/s
diskWrite: 1048576, // 1MB/s
network: 45,
networkIn: 2097152, // 2MB/s
networkOut: 1048576 // 1MB/s
})
// Computed properties
const overallStatus = computed(() => {
const healthyCount = systemComponents.value.filter(c => c.status === 'healthy').length
const totalCount = systemComponents.value.length
if (healthyCount === totalCount) return 'healthy'
if (healthyCount >= totalCount * 0.8) return 'degraded'
return 'critical'
})
const overallStatusColor = computed(() => {
switch (overallStatus.value) {
case 'healthy': return 'green'
case 'degraded': return 'orange'
case 'critical': return 'red'
default: return 'grey'
}
})
const overallStatusIcon = computed(() => {
switch (overallStatus.value) {
case 'healthy': return 'mdi-check-circle'
case 'degraded': return 'mdi-alert-circle'
case 'critical': return 'mdi-close-circle'
default: return 'mdi-help-circle'
}
})
const overallStatusText = computed(() => {
switch (overallStatus.value) {
case 'healthy': return 'All Systems Normal'
case 'degraded': return 'Minor Issues'
case case 'critical': return 'Critical Issues'
default: return 'Unknown'
}
})
const lastCheckTime = computed(() => {
const now = new Date()
return now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
})
// Helper functions
const getResponseTimeColor = (responseTime: number) => {
if (responseTime < 100) return 'success'
if (responseTime < 300) return 'warning'
return 'error'
}
const formatBytes = (bytes: number) => {
if (bytes >= 1073741824) {
return `${(bytes / 1073741824).toFixed(1)} GB`
} else if (bytes >= 1048576) {
return `${(bytes / 1048576).toFixed(1)} MB`
} else if (bytes >= 1024) {
return `${(bytes / 1024).toFixed(1)} KB`
}
return `${bytes} B`
}
// Chart functions
const initChart = () => {
if (!responseTimeChart.value) return
// Destroy existing chart
if (chartInstance) {
chartInstance.destroy()
}
const ctx = responseTimeChart.value.getContext('2d')
if (!ctx) return
// Generate mock response time data for last 24 hours
const labels = Array.from({ length: 24 }, (_, i) => {
const hour = new Date(Date.now() - (23 - i) * 3600000)
return hour.getHours().toString().padStart(2, '0') + ':00'
})
const data = labels.map(() => {
const base = 50
const spike = Math.random() > 0.9 ? 300 : 0
const variance = Math.random() * 40
return Math.round(base + variance + spike)
})
chartInstance = new Chart(ctx, {
type: 'line',
data: {
labels,
datasets: [{
label: 'API Response Time (ms)',
data,
borderColor: '#2196F3',
backgroundColor: 'rgba(33, 150, 243, 0.1)',
tension: 0.4,
fill: true,
pointBackgroundColor: (context) => {
const value = context.dataset.data[context.dataIndex] as number
return value > 200 ? '#F44336' : value > 100 ? '#FF9800' : '#4CAF50'
},
pointBorderColor: '#FFFFFF',
pointBorderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: (context) => {
return `Response Time: ${context.raw}ms`
}
}
}
},
scales: {
x: {
grid: {
display: false
},
ticks: {
maxRotation: 0,
callback: (value, index) => {
// Show only every 3rd hour label
return index % 3 === 0 ? labels[index] : ''
}
}
},
y: {
beginAtZero: true,
title: {
display: true,
text: 'Milliseconds (ms)'
},
ticks: {
callback: (value) => `${value}ms`
}
}
}
}
})
}
// Auto-refresh management
const startAutoRefresh = () => {
if (refreshTimer) clearInterval(refreshTimer)
refreshTimer = setInterval(() => {
refreshHealth()
}, refreshInterval.value * 1000) as unknown as number
}
const stopAutoRefresh = () => {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
}
const toggleAutoRefresh = () => {
autoRefresh.value = !autoRefresh.value
if (autoRefresh.value) {
startAutoRefresh()
} else {
stopAutoRefresh()
}
}
// Actions
const refreshHealth = () => {
if (isRefreshing.value) return
isRefreshing.value = true
// Simulate API call
setTimeout(() => {
// Update system components with random variations
systemComponents.value.forEach(component => {
// Random status changes (rare)
if (Math.random() > 0.95) {
component.status = Math.random() > 0.7 ? 'degraded' : 'healthy'
component.statusColor = component.status === 'healthy' ? 'success' : 'warning'
}
// Update response times
if (component.responseTime) {
const variation = Math.random() * 40 - 20
component.responseTime = Math.max(10, Math.round(component.responseTime + variation))
}
// Update uptime (slight variations)
component.uptime = Math.min(99.99, component.uptime + (Math.random() * 0.1 - 0.05))
})
// Update database metrics
databaseMetrics.value.connections = Math.round(64 + Math.random() * 20 - 10)
databaseMetrics.value.activeConnections = Math.round(databaseMetrics.value.connections * 0.7)
databaseMetrics.value.queryTime = Math.round(85 + Math.random() * 30 - 15)
databaseMetrics.value.queriesPerSecond = Math.round(1250 + Math.random() * 200 - 100)
databaseMetrics.value.cacheHitRate = Math.min(99, Math.round(92 + Math.random() * 4 - 2))
databaseMetrics.value.replicationLag = Math.round(45 + Math.random() * 20 - 10)
// Update server resources
serverResources.value.cpu = Math.round(42 + Math.random() * 20 - 10)
serverResources.value.memory = Math.round(68 + Math.random() * 10 - 5)
serverResources.value.diskIO = Math.round(28 + Math.random() * 15 - 7)
serverResources.value.network = Math.round(45 + Math.random() * 20 - 10)
// Update chart
initChart()
isRefreshing.value = false
}, 800)
}
// Lifecycle hooks
onMounted(() => {
nextTick(() => {
initChart()
})
// Start auto-refresh if enabled
if (autoRefresh.value) {
startAutoRefresh()
}
})
onUnmounted(() => {
if (chartInstance) {
chartInstance.destroy()
}
stopAutoRefresh()
})
</script>
<style scoped>
.chart-container {
position: relative;
width: 100%;
}
.h-100 {
height: 100%;
}
.v-progress-linear {
border-radius: 4px;
}
.v-card {
transition: all 0.3s ease;
}
.v-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
</style>

View File

@@ -1,168 +0,0 @@
<template>
<v-card
:color="tileColor"
variant="tonal"
class="h-100 d-flex flex-column"
@click="handleTileClick"
>
<v-card-title class="d-flex align-center justify-space-between">
<div class="d-flex align-center">
<v-icon :icon="tileIcon" class="mr-2"></v-icon>
<span class="text-subtitle-1 font-weight-bold">{{ tile.title }}</span>
</div>
<v-chip size="small" :color="accessLevelColor" class="text-caption">
{{ accessLevelText }}
</v-chip>
</v-card-title>
<v-card-text class="flex-grow-1">
<p class="text-body-2">{{ tile.description }}</p>
<!-- Requirements Badges -->
<div class="mt-2">
<v-chip
v-for="role in tile.requiredRole"
:key="role"
size="x-small"
class="mr-1 mb-1"
variant="outlined"
>
{{ role }}
</v-chip>
<v-chip
v-if="tile.minRank"
size="x-small"
class="mr-1 mb-1"
color="warning"
variant="outlined"
>
Rank {{ tile.minRank }}+
</v-chip>
</div>
<!-- Scope Level Indicator -->
<div v-if="tile.scopeLevel && tile.scopeLevel.length > 0" class="mt-2">
<v-icon icon="mdi-map-marker" size="small" class="mr-1"></v-icon>
<span class="text-caption">
{{ tile.scopeLevel.join(', ') }}
</span>
</div>
</v-card-text>
<v-card-actions class="mt-auto">
<v-spacer></v-spacer>
<v-btn
variant="text"
size="small"
:prepend-icon="actionIcon"
@click.stop="handleTileClick"
>
Open
</v-btn>
</v-card-actions>
</v-card>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { TilePermission } from '~/composables/useRBAC'
interface Props {
tile: TilePermission
}
const props = defineProps<Props>()
// Tile color based on ID
const tileColor = computed(() => {
const colors: Record<string, string> = {
'ai-logs': 'indigo',
'financial-dashboard': 'green',
'salesperson-hub': 'orange',
'user-management': 'blue',
'service-moderation-map': 'teal',
'gamification-control': 'purple',
'system-health': 'red'
}
return colors[props.tile.id] || 'surface'
})
// Tile icon based on ID
const tileIcon = computed(() => {
const icons: Record<string, string> = {
'ai-logs': 'mdi-robot',
'financial-dashboard': 'mdi-chart-line',
'salesperson-hub': 'mdi-account-tie',
'user-management': 'mdi-account-group',
'service-moderation-map': 'mdi-map',
'gamification-control': 'mdi-trophy',
'system-health': 'mdi-heart-pulse'
}
return icons[props.tile.id] || 'mdi-view-dashboard'
})
// Action icon
const actionIcon = computed(() => {
const actions: Record<string, string> = {
'ai-logs': 'mdi-chart-timeline',
'financial-dashboard': 'mdi-finance',
'salesperson-hub': 'mdi-chart-bar',
'user-management': 'mdi-account-cog',
'service-moderation-map': 'mdi-map-search',
'gamification-control': 'mdi-cog',
'system-health': 'mdi-monitor-dashboard'
}
return actions[props.tile.id] || 'mdi-open-in-new'
})
// Access level indicator
const accessLevelColor = computed(() => {
if (props.tile.requiredRole.includes('superadmin')) return 'purple'
if (props.tile.requiredRole.includes('admin')) return 'blue'
if (props.tile.requiredRole.includes('moderator')) return 'green'
return 'orange'
})
const accessLevelText = computed(() => {
if (props.tile.requiredRole.includes('superadmin')) return 'Superadmin'
if (props.tile.requiredRole.includes('admin')) return 'Admin'
if (props.tile.requiredRole.includes('moderator')) return 'Moderator'
return 'Sales'
})
// Handle tile click
function handleTileClick() {
const routes: Record<string, string> = {
'ai-logs': '/ai-logs',
'financial-dashboard': '/finance',
'salesperson-hub': '/sales',
'user-management': '/users',
'service-moderation-map': '/map',
'gamification-control': '/gamification',
'system-health': '/system'
}
const route = routes[props.tile.id]
if (route) {
navigateTo(route)
} else {
console.warn(`No route defined for tile: ${props.tile.id}`)
}
}
</script>
<style scoped>
.v-card {
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.v-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.h-100 {
height: 100%;
}
</style>

View File

@@ -1,174 +0,0 @@
<template>
<v-card
:color="tileColor"
variant="tonal"
class="h-100 d-flex flex-column tile-wrapper"
:class="{ 'draggable-tile': draggable }"
>
<!-- Drag Handle -->
<div v-if="draggable" class="drag-handle d-flex align-center justify-center pa-2" @mousedown.prevent>
<v-icon icon="mdi-drag-vertical" size="small" class="text-disabled"></v-icon>
</div>
<!-- Tile Header -->
<v-card-title class="d-flex align-center justify-space-between pa-3 pb-0">
<div class="d-flex align-center">
<v-icon :icon="tileIcon" class="mr-2"></v-icon>
<span class="text-subtitle-1 font-weight-bold">{{ tile.title }}</span>
</div>
<div class="d-flex align-center">
<!-- RBAC Badge -->
<v-chip size="small" :color="accessLevelColor" class="text-caption mr-1">
{{ accessLevelText }}
</v-chip>
<!-- Visibility Toggle -->
<v-btn
v-if="showVisibilityToggle"
icon
size="x-small"
variant="text"
@click="toggleVisibility"
:title="tile.preference?.visible ? 'Hide tile' : 'Show tile'"
>
<v-icon :icon="tile.preference?.visible ? 'mdi-eye' : 'mdi-eye-off'"></v-icon>
</v-btn>
</div>
</v-card-title>
<!-- Tile Content Slot -->
<v-card-text class="flex-grow-1 pa-3">
<slot>
<!-- Default content if no slot provided -->
<p class="text-body-2">{{ tile.description }}</p>
</slot>
</v-card-text>
<!-- Tile Footer Actions -->
<v-card-actions class="mt-auto pa-3 pt-0">
<v-spacer></v-spacer>
<v-btn
v-if="showActionButton"
variant="text"
size="small"
:prepend-icon="actionIcon"
@click="handleTileClick"
>
{{ actionText }}
</v-btn>
</v-card-actions>
</v-card>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useTileStore } from '~/stores/tiles'
import type { TilePermission } from '~/composables/useRBAC'
interface Props {
tile: TilePermission
draggable?: boolean
showVisibilityToggle?: boolean
showActionButton?: boolean
actionIcon?: string
actionText?: string
}
const props = withDefaults(defineProps<Props>(), {
draggable: true,
showVisibilityToggle: true,
showActionButton: true,
actionIcon: 'mdi-open-in-new',
actionText: 'Open'
})
const emit = defineEmits<{
click: [tile: TilePermission]
toggleVisibility: [tileId: string, visible: boolean]
}>()
const tileStore = useTileStore()
// Tile color based on ID
const tileColor = computed(() => {
const colors: Record<string, string> = {
'ai-logs': 'indigo',
'financial-dashboard': 'green',
'salesperson-hub': 'orange',
'user-management': 'blue',
'service-moderation-map': 'teal',
'gamification-control': 'purple',
'system-health': 'red'
}
return colors[props.tile.id] || 'surface'
})
// Tile icon based on ID
const tileIcon = computed(() => {
const icons: Record<string, string> = {
'ai-logs': 'mdi-robot',
'financial-dashboard': 'mdi-chart-line',
'salesperson-hub': 'mdi-account-tie',
'user-management': 'mdi-account-group',
'service-moderation-map': 'mdi-map',
'gamification-control': 'mdi-trophy',
'system-health': 'mdi-heart-pulse'
}
return icons[props.tile.id] || 'mdi-view-dashboard'
})
// Access level indicator
const accessLevelColor = computed(() => {
if (props.tile.minRank && props.tile.minRank > 5) return 'warning'
if (props.tile.requiredRole?.includes('admin')) return 'error'
return 'success'
})
const accessLevelText = computed(() => {
if (props.tile.minRank) return `Rank ${props.tile.minRank}+`
if (props.tile.requiredRole?.length) return props.tile.requiredRole[0]
return 'All'
})
// Methods
function handleTileClick() {
emit('click', props.tile)
}
function toggleVisibility() {
const newVisible = !props.tile.preference?.visible
tileStore.toggleTileVisibility(props.tile.id)
emit('toggleVisibility', props.tile.id, newVisible)
}
</script>
<style scoped>
.tile-wrapper {
position: relative;
transition: box-shadow 0.2s ease-in-out, transform 0.2s ease-in-out;
}
.tile-wrapper:hover {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.drag-handle {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 24px;
background-color: rgba(0, 0, 0, 0.02);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
cursor: grab;
z-index: 1;
}
.drag-handle:active {
cursor: grabbing;
}
.draggable-tile {
user-select: none;
}
</style>

View File

@@ -1,189 +0,0 @@
<template>
<div class="service-map-container">
<div class="scope-indicator">
<span class="badge">Current Scope: {{ scopeLabel }}</span>
</div>
<div class="map-wrapper">
<l-map
ref="map"
:zoom="zoom"
:center="center"
@ready="onMapReady"
style="height: 600px; width: 100%;"
>
<l-tile-layer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
<l-marker
v-for="service in services"
:key="service.id"
:lat-lng="[service.lat, service.lng]"
@click="openPopup(service)"
>
<l-icon
:icon-url="getMarkerIcon(service.status)"
:icon-size="[32, 32]"
:icon-anchor="[16, 32]"
/>
<l-popup v-if="selectedService?.id === service.id">
<div class="popup-content">
<h3>{{ service.name }}</h3>
<p><strong>Status:</strong> <span :class="service.status">{{ service.status }}</span></p>
<p><strong>Address:</strong> {{ service.address }}</p>
<p><strong>Distance:</strong> {{ service.distance }} km</p>
<button @click="approveService(service)" class="btn-approve">Approve</button>
</div>
</l-popup>
</l-marker>
</l-map>
</div>
<div class="legend">
<div class="legend-item">
<img src="/marker-pending.svg" alt="Pending" class="legend-icon" />
<span>Pending</span>
</div>
<div class="legend-item">
<img src="/marker-approved.svg" alt="Approved" class="legend-icon" />
<span>Approved</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { LMap, LTileLayer, LMarker, LPopup, LIcon } from 'vue3-leaflet'
import 'leaflet/dist/leaflet.css'
import type { Service } from '~/composables/useServiceMap'
const props = defineProps<{
services?: Service[]
scopeLabel?: string
}>()
const map = ref<any>(null)
const zoom = ref(11)
const center = ref<[number, number]>([47.6333, 19.1333]) // Budapest area
const selectedService = ref<Service | null>(null)
const services = ref<Service[]>(props.services || [])
const getMarkerIcon = (status: string) => {
return status === 'approved' ? '/marker-approved.svg' : '/marker-pending.svg'
}
const openPopup = (service: Service) => {
selectedService.value = service
}
const approveService = (service: Service) => {
console.log('Approving service:', service)
// TODO: Implement API call
service.status = 'approved'
selectedService.value = null
}
const onMapReady = () => {
console.log('Map is ready')
}
onMounted(() => {
// If no services provided, use mock data
if (services.value.length === 0) {
// Mock data will be loaded via composable
}
})
</script>
<style scoped>
.service-map-container {
position: relative;
width: 100%;
height: 100%;
}
.scope-indicator {
position: absolute;
top: 10px;
right: 10px;
z-index: 1000;
}
.badge {
background-color: #4a90e2;
color: white;
padding: 8px 12px;
border-radius: 20px;
font-weight: bold;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.map-wrapper {
border-radius: 8px;
overflow: hidden;
border: 1px solid #ddd;
}
.legend {
position: absolute;
bottom: 20px;
left: 20px;
background: white;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
z-index: 1000;
}
.legend-item {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.legend-icon {
width: 20px;
height: 20px;
margin-right: 8px;
}
.popup-content {
min-width: 200px;
}
.popup-content h3 {
margin-top: 0;
color: #333;
}
.popup-content p {
margin: 5px 0;
}
.btn-approve {
background-color: #28a745;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
margin-top: 10px;
width: 100%;
}
.btn-approve:hover {
background-color: #218838;
}
.pending {
color: #3b82f6;
font-weight: bold;
}
.approved {
color: #28a745;
font-weight: bold;
}
</style>

View File

@@ -1,390 +0,0 @@
import { ref, computed, onUnmounted } from 'vue'
import { useAuthStore } from '~/stores/auth'
// Types
export interface HealthMetrics {
total_assets: number
total_organizations: number
critical_alerts_24h: number
system_status: 'healthy' | 'degraded' | 'critical'
uptime_percentage: number
response_time_ms: number
database_connections: number
active_users: number
last_updated: string
}
export interface SystemAlert {
id: string
severity: 'info' | 'warning' | 'critical'
title: string
description: string
timestamp: string
component: string
resolved: boolean
}
export interface HealthMonitorState {
metrics: HealthMetrics | null
alerts: SystemAlert[]
loading: boolean
error: string | null
lastUpdated: Date | null
}
// Mock data for development/testing (only for alerts since no backend endpoint yet)
const generateMockMetrics = (): HealthMetrics => {
return {
total_assets: Math.floor(Math.random() * 10000) + 5000,
total_organizations: Math.floor(Math.random() * 500) + 100,
critical_alerts_24h: Math.floor(Math.random() * 10),
system_status: Math.random() > 0.8 ? 'degraded' : Math.random() > 0.95 ? 'critical' : 'healthy',
uptime_percentage: 99.5 + (Math.random() * 0.5 - 0.25), // 99.25% - 99.75%
response_time_ms: Math.floor(Math.random() * 100) + 50,
database_connections: Math.floor(Math.random() * 50) + 10,
active_users: Math.floor(Math.random() * 1000) + 500,
last_updated: new Date().toISOString()
}
}
const generateMockAlerts = (count: number = 5): SystemAlert[] => {
const severities: SystemAlert['severity'][] = ['info', 'warning', 'critical']
const components = ['Database', 'API Gateway', 'Redis', 'PostgreSQL', 'Docker', 'Network', 'Authentication', 'File Storage']
const titles = [
'High memory usage detected',
'Database connection pool exhausted',
'API response time above threshold',
'Redis cache miss rate increased',
'Disk space running low',
'Network latency spike',
'Authentication service slow response',
'Backup job failed'
]
const alerts: SystemAlert[] = []
for (let i = 0; i < count; i++) {
const severity = severities[Math.floor(Math.random() * severities.length)]
const isResolved = Math.random() > 0.7
alerts.push({
id: `alert_${Date.now()}_${i}`,
severity,
title: titles[Math.floor(Math.random() * titles.length)],
description: `Detailed description of the ${severity} alert in the ${components[Math.floor(Math.random() * components.length)]} component.`,
timestamp: new Date(Date.now() - Math.random() * 24 * 60 * 60 * 1000).toISOString(), // Within last 24 hours
component: components[Math.floor(Math.random() * components.length)],
resolved: isResolved
})
}
return alerts
}
// API Service
class HealthMonitorApiService {
private baseUrl = '/api/v1/admin' // Using proxy from nuxt.config.ts
private delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
// Get health metrics
async getHealthMetrics(): Promise<HealthMetrics> {
try {
console.log('Fetching health metrics from:', `${this.baseUrl}/health-monitor`)
const response = await fetch(`${this.baseUrl}/health-monitor`, {
headers: this.getAuthHeaders()
})
if (!response.ok) {
const errorText = await response.text()
console.error('Health monitor API error:', response.status, response.statusText, errorText)
// Specific error handling
if (response.status === 401) {
throw new Error('Authentication required. Please log in again.')
} else if (response.status === 403) {
throw new Error('Access forbidden. Admin privileges required.')
} else if (response.status === 500) {
throw new Error('Server error. Please try again later.')
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText} - ${errorText}`)
}
}
const data = await response.json()
console.log('Health monitor API response:', data)
// Transform API response to match HealthMetrics interface
return {
total_assets: data.total_assets || 0,
total_organizations: data.total_organizations || 0,
critical_alerts_24h: data.critical_alerts_24h || 0,
system_status: 'healthy', // Default, API doesn't return this yet
uptime_percentage: 99.9, // Default, API doesn't return this yet
response_time_ms: 50, // Default, API doesn't return this yet
database_connections: 0, // Default, API doesn't return this yet
active_users: data.user_distribution ? Object.values(data.user_distribution).reduce((a: number, b: number) => a + b, 0) : 0,
last_updated: new Date().toISOString()
}
} catch (error) {
console.error('Failed to fetch real health metrics:', error)
throw error // Don't fall back to mock data - let the caller handle it
}
}
// Get system alerts (mocked for now - no backend endpoint)
async getSystemAlerts(options?: {
severity?: SystemAlert['severity']
resolved?: boolean
limit?: number
}): Promise<SystemAlert[]> {
await this.delay(500)
let alerts = generateMockAlerts(10)
if (options?.severity) {
alerts = alerts.filter(alert => alert.severity === options.severity)
}
if (options?.resolved !== undefined) {
alerts = alerts.filter(alert => alert.resolved === options.resolved)
}
if (options?.limit) {
alerts = alerts.slice(0, options.limit)
}
return alerts
}
// Get auth headers (for real API calls)
private getAuthHeaders(): Record<string, string> {
const authStore = useAuthStore()
const headers: Record<string, string> = {
'Content-Type': 'application/json'
}
if (authStore.token) {
headers['Authorization'] = `Bearer ${authStore.token}`
}
// Add geographical scope headers
if (authStore.getScopeId) {
headers['X-Scope-Id'] = authStore.getScopeId.toString()
}
if (authStore.getRegionCode) {
headers['X-Region-Code'] = authStore.getRegionCode
}
if (authStore.getScopeLevel) {
headers['X-Scope-Level'] = authStore.getScopeLevel
}
return headers
}
}
// Composable
export const useHealthMonitor = () => {
const state = ref<HealthMonitorState>({
metrics: null,
alerts: [],
loading: false,
error: null,
lastUpdated: null
})
const apiService = new HealthMonitorApiService()
let refreshInterval: NodeJS.Timeout | null = null
// Computed properties
const systemStatusColor = computed(() => {
if (!state.value.metrics) return 'grey'
switch (state.value.metrics.system_status) {
case 'healthy': return 'green'
case 'degraded': return 'dark-blue' // Changed from orange to dark-blue for better contrast
case 'critical': return 'red'
default: return 'grey'
}
})
const systemStatusIcon = computed(() => {
if (!state.value.metrics) return 'mdi-help-circle'
switch (state.value.metrics.system_status) {
case 'healthy': return 'mdi-check-circle'
case 'degraded': return 'mdi-alert-circle'
case 'critical': return 'mdi-alert-octagon'
default: return 'mdi-help-circle'
}
})
const criticalAlerts = computed(() => {
return state.value.alerts.filter(alert => alert.severity === 'critical' && !alert.resolved)
})
const warningAlerts = computed(() => {
return state.value.alerts.filter(alert => alert.severity === 'warning' && !alert.resolved)
})
const formattedUptime = computed(() => {
if (!state.value.metrics) return 'N/A'
return `${state.value.metrics.uptime_percentage.toFixed(2)}%`
})
const formattedResponseTime = computed(() => {
if (!state.value.metrics) return 'N/A'
return `${state.value.metrics.response_time_ms}ms`
})
// Actions
const fetchHealthMetrics = async () => {
state.value.loading = true
state.value.error = null
try {
const metrics = await apiService.getHealthMetrics()
state.value.metrics = metrics
state.value.lastUpdated = new Date()
} catch (error) {
state.value.error = error instanceof Error ? error.message : 'Failed to fetch health metrics'
console.error('Error fetching health metrics:', error)
// NO FALLBACK TO MOCK DATA - let error propagate
} finally {
state.value.loading = false
}
}
const fetchSystemAlerts = async (options?: {
severity?: SystemAlert['severity']
resolved?: boolean
limit?: number
}) => {
state.value.loading = true
state.value.error = null
try {
const alerts = await apiService.getSystemAlerts(options)
state.value.alerts = alerts
} catch (error) {
state.value.error = error instanceof Error ? error.message : 'Failed to fetch system alerts'
console.error('Error fetching system alerts:', error)
// Fallback to mock data for alerts (since no real endpoint yet)
state.value.alerts = generateMockAlerts(5)
} finally {
state.value.loading = false
}
}
const refreshAll = async () => {
await Promise.all([
fetchHealthMetrics(),
fetchSystemAlerts()
])
}
const markAlertAsResolved = async (alertId: string) => {
// In a real implementation, this would call an API endpoint
// await apiService.resolveAlert(alertId)
// Update local state
const alertIndex = state.value.alerts.findIndex(alert => alert.id === alertId)
if (alertIndex !== -1) {
state.value.alerts[alertIndex].resolved = true
}
}
const dismissAlert = (alertId: string) => {
// Remove alert from local state (frontend only)
state.value.alerts = state.value.alerts.filter(alert => alert.id !== alertId)
}
// Start automatic refresh (30-second interval)
const startPolling = (intervalMs: number = 30000) => {
stopPolling() // Clear any existing interval
refreshInterval = setInterval(() => {
console.log('Auto-refreshing health monitor data...')
refreshAll()
}, intervalMs)
console.log(`Health monitor polling started with ${intervalMs}ms interval`)
}
// Stop automatic refresh
const stopPolling = () => {
if (refreshInterval) {
clearInterval(refreshInterval)
refreshInterval = null
console.log('Health monitor polling stopped')
}
}
// Initialize with polling
const initialize = (enablePolling: boolean = true) => {
refreshAll()
if (enablePolling) {
startPolling()
}
}
// Cleanup on unmount
onUnmounted(() => {
stopPolling()
})
return {
// State
state: computed(() => state.value),
metrics: computed(() => state.value.metrics),
alerts: computed(() => state.value.alerts),
loading: computed(() => state.value.loading),
error: computed(() => state.value.error),
lastUpdated: computed(() => state.value.lastUpdated),
// Computed
systemStatusColor,
systemStatusIcon,
criticalAlerts,
warningAlerts,
formattedUptime,
formattedResponseTime,
// Actions
fetchHealthMetrics,
fetchSystemAlerts,
refreshAll,
markAlertAsResolved,
dismissAlert,
initialize,
startPolling,
stopPolling,
// Helper functions
getAlertColor: (severity: SystemAlert['severity']) => {
switch (severity) {
case 'info': return 'blue'
case 'warning': return 'dark-blue' // Changed from orange to dark-blue
case 'critical': return 'red'
default: return 'grey'
}
},
getAlertIcon: (severity: SystemAlert['severity']) => {
switch (severity) {
case 'info': return 'mdi-information'
case 'warning': return 'mdi-alert'
case 'critical': return 'mdi-alert-circle'
default: return 'mdi-help-circle'
}
},
formatTimestamp: (timestamp: string) => {
const date = new Date(timestamp)
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
}
}
}

View File

@@ -1,200 +0,0 @@
import { ref, onMounted, onUnmounted } from 'vue'
export interface PollingOptions {
interval?: number // milliseconds
immediate?: boolean // whether to execute immediately on start
maxRetries?: number // maximum number of retries on error
retryDelay?: number // delay between retries in milliseconds
onError?: (error: Error) => void // error handler
}
export interface PollingState {
isPolling: boolean
isFetching: boolean
error: string | null
retryCount: number
lastFetchTime: Date | null
}
/**
* Composable for implementing polling/real-time updates
*
* @param callback - Function to execute on each poll
* @param options - Polling configuration options
* @returns Polling controls and state
*/
export const usePolling = <T>(
callback: () => Promise<T> | T,
options: PollingOptions = {}
) => {
const {
interval = 3000, // 3 seconds default
immediate = true,
maxRetries = 3,
retryDelay = 1000,
onError
} = options
// State
const state = ref<PollingState>({
isPolling: false,
isFetching: false,
error: null,
retryCount: 0,
lastFetchTime: null
})
// Polling interval reference
let pollInterval: NodeJS.Timeout | null = null
let retryTimeout: NodeJS.Timeout | null = null
// Execute the polling callback
const executePoll = async (): Promise<T | null> => {
if (state.value.isFetching) {
return null // Skip if already fetching
}
state.value.isFetching = true
state.value.error = null
try {
const result = await callback()
state.value.lastFetchTime = new Date()
state.value.retryCount = 0 // Reset retry count on success
return result
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
state.value.error = errorMessage
state.value.retryCount++
// Call error handler if provided
if (onError) {
onError(error instanceof Error ? error : new Error(errorMessage))
}
// Handle retries
if (state.value.retryCount <= maxRetries) {
console.warn(`Polling error (retry ${state.value.retryCount}/${maxRetries}):`, errorMessage)
// Schedule retry
if (retryTimeout) {
clearTimeout(retryTimeout)
}
retryTimeout = setTimeout(() => {
executePoll()
}, retryDelay)
} else {
console.error(`Polling failed after ${maxRetries} retries:`, errorMessage)
stopPolling() // Stop polling after max retries
}
return null
} finally {
state.value.isFetching = false
}
}
// Start polling
const startPolling = () => {
if (state.value.isPolling) {
return // Already polling
}
state.value.isPolling = true
state.value.error = null
// Execute immediately if requested
if (immediate) {
executePoll()
}
// Set up interval
pollInterval = setInterval(() => {
executePoll()
}, interval)
}
// Stop polling
const stopPolling = () => {
if (pollInterval) {
clearInterval(pollInterval)
pollInterval = null
}
if (retryTimeout) {
clearTimeout(retryTimeout)
retryTimeout = null
}
state.value.isPolling = false
state.value.isFetching = false
}
// Toggle polling
const togglePolling = () => {
if (state.value.isPolling) {
stopPolling()
} else {
startPolling()
}
}
// Force immediate execution
const forcePoll = async (): Promise<T | null> => {
return await executePoll()
}
// Update polling interval
const updateInterval = (newInterval: number) => {
const wasPolling = state.value.isPolling
if (wasPolling) {
stopPolling()
}
// Update interval in options (for next start)
options.interval = newInterval
if (wasPolling) {
startPolling()
}
}
// Cleanup on unmount
onUnmounted(() => {
stopPolling()
})
// Auto-start on mount if immediate is true
onMounted(() => {
if (immediate) {
startPolling()
}
})
return {
// State
state: state.value,
isPolling: state.value.isPolling,
isFetching: state.value.isFetching,
error: state.value.error,
retryCount: state.value.retryCount,
lastFetchTime: state.value.lastFetchTime,
// Controls
startPolling,
stopPolling,
togglePolling,
forcePoll,
updateInterval,
// Helper
resetError: () => {
state.value.error = null
state.value.retryCount = 0
}
}
}
export default usePolling

View File

@@ -1,237 +0,0 @@
import { useAuthStore } from '~/stores/auth'
// Role definitions with hierarchical ranks
export enum Role {
SUPERADMIN = 'superadmin',
ADMIN = 'admin',
MODERATOR = 'moderator',
SALESPERSON = 'salesperson'
}
// Scope level definitions
export enum ScopeLevel {
GLOBAL = 'global',
COUNTRY = 'country',
REGION = 'region',
CITY = 'city',
DISTRICT = 'district'
}
// Role rank mapping (higher number = higher authority)
export const RoleRank: Record<Role, number> = {
[Role.SUPERADMIN]: 10,
[Role.ADMIN]: 7,
[Role.MODERATOR]: 5,
[Role.SALESPERSON]: 3
}
// Tile permissions mapping
export interface TilePermission {
id: string
title: string
description: string
requiredRole: Role[]
minRank?: number
requiredPermission?: string
scopeLevel?: ScopeLevel[]
}
// Available tiles with RBAC requirements
export const AdminTiles: TilePermission[] = [
{
id: 'ai-logs',
title: 'AI Logs Monitor',
description: 'Real-time tracking of AI robot pipelines',
requiredRole: [Role.SUPERADMIN, Role.ADMIN, Role.MODERATOR],
minRank: 5,
requiredPermission: 'view:dashboard'
},
{
id: 'financial-dashboard',
title: 'Financial Dashboard',
description: 'Revenue, expenses, ROI metrics with geographical filtering',
requiredRole: [Role.SUPERADMIN, Role.ADMIN],
minRank: 7,
requiredPermission: 'view:finance',
scopeLevel: [ScopeLevel.GLOBAL, ScopeLevel.COUNTRY, ScopeLevel.REGION]
},
{
id: 'salesperson-hub',
title: 'Salesperson Hub',
description: 'Performance metrics, leads, conversions for sales teams',
requiredRole: [Role.SUPERADMIN, Role.ADMIN, Role.SALESPERSON],
minRank: 3,
requiredPermission: 'view:sales'
},
{
id: 'user-management',
title: 'User Management',
description: 'Active users, registration trends, moderation queue',
requiredRole: [Role.SUPERADMIN, Role.ADMIN, Role.MODERATOR],
minRank: 5,
requiredPermission: 'view:users',
scopeLevel: [ScopeLevel.GLOBAL, ScopeLevel.COUNTRY, ScopeLevel.REGION, ScopeLevel.CITY]
},
{
id: 'service-moderation-map',
title: 'Service Moderation Map',
description: 'Geographical view of pending/flagged services',
requiredRole: [Role.SUPERADMIN, Role.ADMIN, Role.MODERATOR],
minRank: 5,
requiredPermission: 'moderate:services',
scopeLevel: [ScopeLevel.CITY, ScopeLevel.DISTRICT]
},
{
id: 'gamification-control',
title: 'Gamification Control',
description: 'XP levels, badges, penalty system administration',
requiredRole: [Role.SUPERADMIN, Role.ADMIN],
minRank: 7,
requiredPermission: 'manage:settings'
},
{
id: 'system-health',
title: 'System Health',
description: 'API status, database metrics, uptime monitoring',
requiredRole: [Role.SUPERADMIN, Role.ADMIN],
minRank: 7,
requiredPermission: 'view:dashboard'
}
]
// Composable for RBAC checks
export function useRBAC() {
const authStore = useAuthStore()
// Check if user can access a specific tile
function canAccessTile(tileId: string): boolean {
const tile = AdminTiles.find(t => t.id === tileId)
if (!tile) return false
// Check role
if (!tile.requiredRole.includes(authStore.getUserRole as Role)) {
return false
}
// Check rank
if (tile.minRank && !authStore.hasRank(tile.minRank)) {
return false
}
// Check permission
if (tile.requiredPermission && !authStore.hasPermission(tile.requiredPermission)) {
return false
}
// Check scope level
if (tile.scopeLevel && tile.scopeLevel.length > 0) {
const userScopeLevel = authStore.getScopeLevel as ScopeLevel
if (!tile.scopeLevel.includes(userScopeLevel)) {
return false
}
}
return true
}
// Get filtered tiles for current user
function getFilteredTiles(): TilePermission[] {
return AdminTiles.filter(tile => canAccessTile(tile.id))
}
// Check if user can perform action
function canPerformAction(permission: string, minRank?: number): boolean {
if (!authStore.hasPermission(permission)) {
return false
}
if (minRank && !authStore.hasRank(minRank)) {
return false
}
return true
}
// Check if user can access scope
function canAccessScope(scopeLevel: ScopeLevel, scopeId?: number, regionCode?: string): boolean {
const userScopeLevel = authStore.getScopeLevel as ScopeLevel
// Superadmin can access everything
if (authStore.getUserRole === Role.SUPERADMIN) {
return true
}
// Check scope level hierarchy
const scopeHierarchy = [
ScopeLevel.GLOBAL,
ScopeLevel.COUNTRY,
ScopeLevel.REGION,
ScopeLevel.CITY,
ScopeLevel.DISTRICT
]
const userLevelIndex = scopeHierarchy.indexOf(userScopeLevel)
const requestedLevelIndex = scopeHierarchy.indexOf(scopeLevel)
// User can only access their level or lower (more specific) levels
if (requestedLevelIndex < userLevelIndex) {
return false
}
// Check specific scope ID or region code if provided
if (scopeId || regionCode) {
return authStore.canAccessScope(scopeId || 0, regionCode)
}
return true
}
// Get user's accessible scope levels
function getAccessibleScopeLevels(): ScopeLevel[] {
const userScopeLevel = authStore.getScopeLevel as ScopeLevel
const scopeHierarchy = [
ScopeLevel.GLOBAL,
ScopeLevel.COUNTRY,
ScopeLevel.REGION,
ScopeLevel.CITY,
ScopeLevel.DISTRICT
]
const userLevelIndex = scopeHierarchy.indexOf(userScopeLevel)
return scopeHierarchy.slice(userLevelIndex)
}
// Get role color for UI
function getRoleColor(role?: string): string {
const userRole = role || authStore.getUserRole
switch (userRole) {
case Role.SUPERADMIN:
return 'purple'
case Role.ADMIN:
return 'blue'
case Role.MODERATOR:
return 'green'
case Role.SALESPERSON:
return 'orange'
default:
return 'gray'
}
}
return {
// Data
Role,
ScopeLevel,
RoleRank,
AdminTiles,
// Functions
canAccessTile,
getFilteredTiles,
canPerformAction,
canAccessScope,
getAccessibleScopeLevels,
getRoleColor
}
}

View File

@@ -1,185 +0,0 @@
import { ref, computed } from 'vue'
export interface Service {
id: number
name: string
lat: number
lng: number
status: 'pending' | 'approved'
address: string
distance: number
category: string
}
export interface Scope {
id: string
label: string
bounds: [[number, number], [number, number]] // SW, NE corners
}
export const useServiceMap = () => {
// Mock services around Budapest
const services = ref<Service[]>([
{
id: 1,
name: 'AutoService Budapest',
lat: 47.6333,
lng: 19.1333,
status: 'pending',
address: 'Budapest, Kossuth Lajos utca 12',
distance: 0.5,
category: 'Car Repair'
},
{
id: 2,
name: 'MOL Station',
lat: 47.6400,
lng: 19.1400,
status: 'approved',
address: 'Budapest, Váci út 45',
distance: 1.2,
category: 'Fuel Station'
},
{
id: 3,
name: 'TireMaster',
lat: 47.6200,
lng: 19.1200,
status: 'pending',
address: 'Budapest, Üllői út 78',
distance: 2.1,
category: 'Tire Service'
},
{
id: 4,
name: 'CarWash Express',
lat: 47.6500,
lng: 19.1500,
status: 'approved',
address: 'Budapest, Róna utca 5',
distance: 3.0,
category: 'Car Wash'
},
{
id: 5,
name: 'BrakeCenter',
lat: 47.6100,
lng: 19.1100,
status: 'pending',
address: 'Budapest, Könyves Kálmán körút 32',
distance: 2.5,
category: 'Brake Service'
},
{
id: 6,
name: 'ElectricCar Service',
lat: 47.6000,
lng: 19.1000,
status: 'pending',
address: 'Budapest, Hungária körút 120',
distance: 4.2,
category: 'EV Charging'
},
{
id: 7,
name: 'OilChange Pro',
lat: 47.6700,
lng: 19.1700,
status: 'approved',
address: 'Budapest, Szentmihályi út 67',
distance: 5.1,
category: 'Oil Change'
},
{
id: 8,
name: 'BodyShop Elite',
lat: 47.5900,
lng: 19.0900,
status: 'pending',
address: 'Budapest, Gyáli út 44',
distance: 5.8,
category: 'Body Repair'
}
])
// Simulated RBAC geographical scope
const currentScope = ref<Scope>({
id: 'pest_county',
label: 'Pest County / Central Hungary',
bounds: [[47.3, 18.9], [47.8, 19.5]]
})
const scopeLabel = computed(() => currentScope.value.label)
const pendingServices = computed(() =>
services.value.filter(s => s.status === 'pending')
)
const approvedServices = computed(() =>
services.value.filter(s => s.status === 'approved')
)
const approveService = (serviceId: number) => {
const service = services.value.find(s => s.id === serviceId)
if (service) {
service.status = 'approved'
console.log(`Service ${serviceId} approved`)
}
}
const addMockService = (service: Omit<Service, 'id'>) => {
const newId = Math.max(...services.value.map(s => s.id)) + 1
services.value.push({
id: newId,
...service
})
}
const filterByScope = (servicesList: Service[]) => {
const [sw, ne] = currentScope.value.bounds
return servicesList.filter(s =>
s.lat >= sw[0] && s.lat <= ne[0] &&
s.lng >= sw[1] && s.lng <= ne[1]
)
}
const servicesInScope = computed(() =>
filterByScope(services.value)
)
const changeScope = (scope: Scope) => {
currentScope.value = scope
}
// Available scopes for simulation
const availableScopes: Scope[] = [
{
id: 'budapest',
label: 'Budapest Only',
bounds: [[47.4, 19.0], [47.6, 19.3]]
},
{
id: 'pest_county',
label: 'Pest County / Central Hungary',
bounds: [[47.3, 18.9], [47.8, 19.5]]
},
{
id: 'hungary',
label: 'Whole Hungary',
bounds: [[45.7, 16.1], [48.6, 22.9]]
}
]
return {
services,
pendingServices,
approvedServices,
scopeLabel,
currentScope,
servicesInScope,
approveService,
addMockService,
changeScope,
availableScopes
}
}

View File

@@ -1,498 +0,0 @@
import { ref, computed } from 'vue'
import { useAuthStore } from '~/stores/auth'
// Types
export interface User {
id: number
email: string
role: 'superadmin' | 'admin' | 'moderator' | 'sales_agent'
scope_level: 'Global' | 'Country' | 'Region' | 'City' | 'District'
status: 'active' | 'inactive'
created_at: string
updated_at?: string
last_login?: string
organization_id?: number
region_code?: string
country_code?: string
}
export interface UpdateUserRoleRequest {
role: User['role']
scope_level: User['scope_level']
scope_id?: number
region_code?: string
country_code?: string
}
export interface UserManagementState {
users: User[]
loading: boolean
error: string | null
}
// Geographical scope definitions for mock data
const geographicalScopes = [
// Hungary hierarchy
{ level: 'Country' as const, code: 'HU', name: 'Hungary', region_code: null },
{ level: 'Region' as const, code: 'HU-PE', name: 'Pest County', country_code: 'HU' },
{ level: 'City' as const, code: 'HU-BU', name: 'Budapest', country_code: 'HU', region_code: 'HU-PE' },
{ level: 'District' as const, code: 'HU-BU-05', name: 'District V', country_code: 'HU', region_code: 'HU-BU' },
// Germany hierarchy
{ level: 'Country' as const, code: 'DE', name: 'Germany', region_code: null },
{ level: 'Region' as const, code: 'DE-BE', name: 'Berlin', country_code: 'DE' },
{ level: 'City' as const, code: 'DE-BER', name: 'Berlin', country_code: 'DE', region_code: 'DE-BE' },
// UK hierarchy
{ level: 'Country' as const, code: 'GB', name: 'United Kingdom', region_code: null },
{ level: 'Region' as const, code: 'GB-LON', name: 'London', country_code: 'GB' },
{ level: 'City' as const, code: 'GB-LND', name: 'London', country_code: 'GB', region_code: 'GB-LON' },
]
// Mock data generator with consistent geographical scopes
const generateMockUsers = (count: number = 25): User[] => {
const roles: User['role'][] = ['superadmin', 'admin', 'moderator', 'sales_agent']
const statuses: User['status'][] = ['active', 'inactive']
const domains = ['servicefinder.com', 'example.com', 'partner.com', 'customer.org']
const firstNames = ['John', 'Jane', 'Robert', 'Emily', 'Michael', 'Sarah', 'David', 'Lisa', 'James', 'Maria']
const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez']
const users: User[] = []
// Predefined users with specific geographical scopes for testing
const predefinedUsers: Partial<User>[] = [
// Global superadmin
{ email: 'superadmin@servicefinder.com', role: 'superadmin', scope_level: 'Global', country_code: undefined, region_code: undefined },
// Hungary admin
{ email: 'admin.hu@servicefinder.com', role: 'admin', scope_level: 'Country', country_code: 'HU', region_code: undefined },
// Pest County moderator
{ email: 'moderator.pest@servicefinder.com', role: 'moderator', scope_level: 'Region', country_code: 'HU', region_code: 'HU-PE' },
// Budapest sales agent
{ email: 'sales.budapest@servicefinder.com', role: 'sales_agent', scope_level: 'City', country_code: 'HU', region_code: 'HU-BU' },
// District V sales agent
{ email: 'agent.district5@servicefinder.com', role: 'sales_agent', scope_level: 'District', country_code: 'HU', region_code: 'HU-BU-05' },
// Germany admin
{ email: 'admin.de@servicefinder.com', role: 'admin', scope_level: 'Country', country_code: 'DE', region_code: undefined },
// Berlin moderator
{ email: 'moderator.berlin@servicefinder.com', role: 'moderator', scope_level: 'City', country_code: 'DE', region_code: 'DE-BE' },
// UK admin
{ email: 'admin.uk@servicefinder.com', role: 'admin', scope_level: 'Country', country_code: 'GB', region_code: undefined },
// London sales agent
{ email: 'sales.london@servicefinder.com', role: 'sales_agent', scope_level: 'City', country_code: 'GB', region_code: 'GB-LON' },
]
// Add predefined users
predefinedUsers.forEach((userData, index) => {
users.push({
id: index + 1,
email: userData.email!,
role: userData.role!,
scope_level: userData.scope_level!,
status: 'active',
created_at: `2026-${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`,
updated_at: `2026-${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`,
last_login: `2026-${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`,
organization_id: Math.floor(Math.random() * 10) + 1,
country_code: userData.country_code,
region_code: userData.region_code,
})
})
// Generate remaining random users
for (let i = users.length + 1; i <= count; i++) {
const firstName = firstNames[Math.floor(Math.random() * firstNames.length)]
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)]
const domain = domains[Math.floor(Math.random() * domains.length)]
const role = roles[Math.floor(Math.random() * roles.length)]
const status = statuses[Math.floor(Math.random() * statuses.length)]
// Select a random geographical scope
const scope = geographicalScopes[Math.floor(Math.random() * geographicalScopes.length)]
users.push({
id: i,
email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}@${domain}`,
role,
scope_level: scope.level,
status,
created_at: `2026-${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`,
updated_at: `2026-${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`,
last_login: `2026-${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`,
organization_id: Math.floor(Math.random() * 10) + 1,
country_code: scope.country_code || undefined,
region_code: scope.region_code || undefined,
})
}
return users
}
// API Service (Mock implementation)
class UserManagementApiService {
private mockUsers: User[] = generateMockUsers(15)
private delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
// Get all users (with optional filtering)
async getUsers(options?: {
role?: User['role']
scope_level?: User['scope_level']
status?: User['status']
search?: string
country_code?: string
region_code?: string
geographical_scope?: 'Global' | 'Hungary' | 'Pest County' | 'Budapest' | 'District V'
}): Promise<User[]> {
await this.delay(500) // Simulate network delay
let filteredUsers = [...this.mockUsers]
if (options?.role) {
filteredUsers = filteredUsers.filter(user => user.role === options.role)
}
if (options?.scope_level) {
filteredUsers = filteredUsers.filter(user => user.scope_level === options.scope_level)
}
if (options?.status) {
filteredUsers = filteredUsers.filter(user => user.status === options.status)
}
if (options?.country_code) {
filteredUsers = filteredUsers.filter(user =>
user.country_code === options.country_code || user.scope_level === 'Global'
)
}
if (options?.region_code) {
filteredUsers = filteredUsers.filter(user =>
user.region_code === options.region_code ||
user.scope_level === 'Global' ||
(user.scope_level === 'Country' && user.country_code === options.country_code)
)
}
// Geographical scope filtering (simplified for demo)
if (options?.geographical_scope) {
switch (options.geographical_scope) {
case 'Global':
// All users
break
case 'Hungary':
filteredUsers = filteredUsers.filter(user =>
user.country_code === 'HU' || user.scope_level === 'Global'
)
break
case 'Pest County':
filteredUsers = filteredUsers.filter(user =>
user.region_code === 'HU-PE' ||
user.country_code === 'HU' ||
user.scope_level === 'Global'
)
break
case 'Budapest':
filteredUsers = filteredUsers.filter(user =>
user.region_code === 'HU-BU' ||
user.region_code === 'HU-PE' ||
user.country_code === 'HU' ||
user.scope_level === 'Global'
)
break
case 'District V':
filteredUsers = filteredUsers.filter(user =>
user.region_code === 'HU-BU-05' ||
user.region_code === 'HU-BU' ||
user.region_code === 'HU-PE' ||
user.country_code === 'HU' ||
user.scope_level === 'Global'
)
break
}
}
if (options?.search) {
const searchLower = options.search.toLowerCase()
filteredUsers = filteredUsers.filter(user =>
user.email.toLowerCase().includes(searchLower) ||
user.role.toLowerCase().includes(searchLower) ||
user.scope_level.toLowerCase().includes(searchLower) ||
(user.country_code && user.country_code.toLowerCase().includes(searchLower)) ||
(user.region_code && user.region_code.toLowerCase().includes(searchLower))
)
}
return filteredUsers
}
// Get single user by ID
async getUserById(id: number): Promise<User | null> {
await this.delay(300)
return this.mockUsers.find(user => user.id === id) || null
}
// Update user role and scope
async updateUserRole(id: number, data: UpdateUserRoleRequest): Promise<User> {
await this.delay(800) // Simulate slower update
const userIndex = this.mockUsers.findIndex(user => user.id === id)
if (userIndex === -1) {
throw new Error(`User with ID ${id} not found`)
}
// Check permissions (in a real app, this would be server-side)
const authStore = useAuthStore()
const currentUserRole = authStore.getUserRole
// Superadmin can update anyone
// Admin cannot update superadmin or other admins
if (currentUserRole === 'admin') {
const targetUser = this.mockUsers[userIndex]
if (targetUser.role === 'superadmin' || (targetUser.role === 'admin' && targetUser.id !== authStore.getUserId)) {
throw new Error('Admin cannot update superadmin or other admin users')
}
}
// Update the user
const updatedUser: User = {
...this.mockUsers[userIndex],
...data,
updated_at: new Date().toISOString().split('T')[0],
}
this.mockUsers[userIndex] = updatedUser
return updatedUser
}
// Toggle user status
async toggleUserStatus(id: number): Promise<User> {
await this.delay(500)
const userIndex = this.mockUsers.findIndex(user => user.id === id)
if (userIndex === -1) {
throw new Error(`User with ID ${id} not found`)
}
const currentStatus = this.mockUsers[userIndex].status
const newStatus: User['status'] = currentStatus === 'active' ? 'inactive' : 'active'
this.mockUsers[userIndex] = {
...this.mockUsers[userIndex],
status: newStatus,
updated_at: new Date().toISOString().split('T')[0],
}
return this.mockUsers[userIndex]
}
// Create new user (mock)
async createUser(email: string, role: User['role'], scope_level: User['scope_level']): Promise<User> {
await this.delay(1000)
const newUser: User = {
id: Math.max(...this.mockUsers.map(u => u.id)) + 1,
email,
role,
scope_level,
status: 'active',
created_at: new Date().toISOString().split('T')[0],
updated_at: new Date().toISOString().split('T')[0],
}
this.mockUsers.push(newUser)
return newUser
}
// Delete user (mock - just deactivate)
async deleteUser(id: number): Promise<void> {
await this.delay(700)
const userIndex = this.mockUsers.findIndex(user => user.id === id)
if (userIndex === -1) {
throw new Error(`User with ID ${id} not found`)
}
// Instead of deleting, mark as inactive
this.mockUsers[userIndex] = {
...this.mockUsers[userIndex],
status: 'inactive',
updated_at: new Date().toISOString().split('T')[0],
}
}
}
// Composable
export const useUserManagement = () => {
const state = ref<UserManagementState>({
users: [],
loading: false,
error: null,
})
const apiService = new UserManagementApiService()
// Computed
const activeUsers = computed(() => state.value.users.filter(user => user.status === 'active'))
const inactiveUsers = computed(() => state.value.users.filter(user => user.status === 'inactive'))
const superadminUsers = computed(() => state.value.users.filter(user => user.role === 'superadmin'))
const adminUsers = computed(() => state.value.users.filter(user => user.role === 'admin'))
// Actions
const fetchUsers = async (options?: {
role?: User['role']
scope_level?: User['scope_level']
status?: User['status']
search?: string
country_code?: string
region_code?: string
geographical_scope?: 'Global' | 'Hungary' | 'Pest County' | 'Budapest' | 'District V'
}) => {
state.value.loading = true
state.value.error = null
try {
const users = await apiService.getUsers(options)
state.value.users = users
} catch (error) {
state.value.error = error instanceof Error ? error.message : 'Failed to fetch users'
console.error('Error fetching users:', error)
} finally {
state.value.loading = false
}
}
const updateUserRole = async (id: number, data: UpdateUserRoleRequest) => {
state.value.loading = true
state.value.error = null
try {
const updatedUser = await apiService.updateUserRole(id, data)
// Update local state
const userIndex = state.value.users.findIndex(user => user.id === id)
if (userIndex !== -1) {
state.value.users[userIndex] = updatedUser
}
return updatedUser
} catch (error) {
state.value.error = error instanceof Error ? error.message : 'Failed to update user role'
console.error('Error updating user role:', error)
throw error
} finally {
state.value.loading = false
}
}
const toggleUserStatus = async (id: number) => {
state.value.loading = true
state.value.error = null
try {
const updatedUser = await apiService.toggleUserStatus(id)
// Update local state
const userIndex = state.value.users.findIndex(user => user.id === id)
if (userIndex !== -1) {
state.value.users[userIndex] = updatedUser
}
return updatedUser
} catch (error) {
state.value.error = error instanceof Error ? error.message : 'Failed to toggle user status'
console.error('Error toggling user status:', error)
throw error
} finally {
state.value.loading = false
}
}
const createUser = async (email: string, role: User['role'], scope_level: User['scope_level']) => {
state.value.loading = true
state.value.error = null
try {
const newUser = await apiService.createUser(email, role, scope_level)
state.value.users.push(newUser)
return newUser
} catch (error) {
state.value.error = error instanceof Error ? error.message : 'Failed to create user'
console.error('Error creating user:', error)
throw error
} finally {
state.value.loading = false
}
}
const deleteUser = async (id: number) => {
state.value.loading = true
state.value.error = null
try {
await apiService.deleteUser(id)
// Update local state (mark as inactive)
const userIndex = state.value.users.findIndex(user => user.id === id)
if (userIndex !== -1) {
state.value.users[userIndex] = {
...state.value.users[userIndex],
status: 'inactive',
updated_at: new Date().toISOString().split('T')[0],
}
}
} catch (error) {
state.value.error = error instanceof Error ? error.message : 'Failed to delete user'
console.error('Error deleting user:', error)
throw error
} finally {
state.value.loading = false
}
}
// Initialize with some data
const initialize = () => {
fetchUsers()
}
// Helper function to get geographical scopes for UI
const getGeographicalScopes = () => {
return [
{ value: 'Global', label: 'Global', icon: 'mdi-earth', description: 'All users worldwide' },
{ value: 'Hungary', label: 'Hungary', icon: 'mdi-flag', description: 'Users in Hungary' },
{ value: 'Pest County', label: 'Pest County', icon: 'mdi-map-marker-radius', description: 'Users in Pest County' },
{ value: 'Budapest', label: 'Budapest', icon: 'mdi-city', description: 'Users in Budapest' },
{ value: 'District V', label: 'District V', icon: 'mdi-map-marker', description: 'Users in District V' },
]
}
return {
// State
state: computed(() => state.value),
users: computed(() => state.value.users),
loading: computed(() => state.value.loading),
error: computed(() => state.value.error),
// Computed
activeUsers,
inactiveUsers,
superadminUsers,
adminUsers,
// Actions
fetchUsers,
updateUserRole,
toggleUserStatus,
createUser,
deleteUser,
initialize,
// Helper functions
getUserById: (id: number) => state.value.users.find(user => user.id === id),
filterByRole: (role: User['role']) => state.value.users.filter(user => user.role === role),
filterByScope: (scope_level: User['scope_level']) => state.value.users.filter(user => user.scope_level === scope_level),
getGeographicalScopes,
}
}
export default useUserManagement

View File

@@ -1,356 +0,0 @@
# Epic 10 - Mission Control Admin Frontend Development Log
## Project Overview
**Date:** 2026-03-23
**Phase:** 1 & 2 Implementation
**Status:** In Development
**Target:** Complete Admin Dashboard with RBAC and Launchpad
## Architectural Decisions
### 1. Technology Stack Selection
- **Framework:** Nuxt 3 (SSR/SPA hybrid) - Chosen for its file-based routing, SEO capabilities, and Vue 3 integration
- **UI Library:** Vuetify 3 - Material Design implementation that provides consistent components and theming
- **State Management:** Pinia - Vue 3's official state management, lightweight and TypeScript friendly
- **TypeScript:** Strict mode enabled for type safety and better developer experience
- **Build Tool:** Vite (via Nuxt) - Fast builds and hot module replacement
### 2. Project Structure
```
frontend/admin/
├── components/ # Reusable Vue components
├── composables/ # Vue composables (useRBAC, etc.)
├── middleware/ # Nuxt middleware (auth.global.ts)
├── pages/ # File-based routes
├── stores/ # Pinia stores (auth.ts, tiles.ts)
├── app.vue # Root component
├── nuxt.config.ts # Nuxt configuration
├── tsconfig.json # TypeScript configuration
└── Dockerfile # Containerization
```
### 3. Authentication & RBAC Architecture
#### JWT Token Structure
Tokens from backend FastAPI `/login` endpoint must include:
```json
{
"sub": "user@email.com",
"role": "admin",
"rank": 7,
"scope_level": "region",
"region_code": "HU-BU",
"scope_id": 123,
"exp": 1700000000,
"iat": 1700000000
}
```
#### Pinia Auth Store Features
- Token parsing and validation with `jwt-decode`
- Automatic token refresh detection
- Role-based permission generation
- Geographical scope validation
- LocalStorage persistence for session continuity
#### RBAC Implementation
- **Role Hierarchy:** Superadmin (10) > Admin (7) > Moderator (5) > Salesperson (3)
- **Scope Levels:** Global > Country > Region > City > District
- **Permission System:** Dynamic permission generation based on role and rank
- **Tile Visibility:** Tiles filtered by role, rank, and scope level
### 4. Middleware Strategy
- **Global Auth Middleware:** Runs on every route change
- **Public Routes:** `/login`, `/forgot-password`, `/reset-password`
- **Role Validation:** Route meta validation with `requiredRole`, `minRank`, `requiredPermission`
- **Scope Validation:** Geographical scope checking with `requiredScopeId` and `requiredRegionCode`
- **Automatic Header Injection:** Adds auth and scope headers to all API requests
### 5. Launchpad Tile System
#### Tile Definition
Each tile includes:
- Unique ID and display title
- Required roles and minimum rank
- Required permissions
- Applicable scope levels
- Icon and color mapping
#### Dynamic Filtering
- Tiles filtered in real-time based on user's RBAC attributes
- Empty state when no tiles accessible
- Visual indicators for access level (role badges, rank chips)
#### User Customization
- Per-user tile preferences stored in localStorage
- Position persistence for drag-and-drop reordering
- Visibility toggles for personalized dashboards
- Size customization (small, medium, large)
### 6. Component Design
#### TileCard Component
- Responsive card with hover effects
- Role and scope level badges
- Color-coded by tile type
- Click-to-navigate functionality
- Consistent sizing and spacing
#### Dashboard Layout
- App bar with user menu and role indicators
- Navigation drawer for main sections
- Welcome header with user context
- Grid-based tile layout
- Quick stats section for at-a-glance metrics
- Footer with system status
### 7. Docker Configuration
#### Multi-stage Build
1. **Builder Stage:** Node 20 with full dev dependencies
2. **Runner Stage:** Optimized production image with non-root user
#### Port Configuration
- **Internal:** 3000 (Nuxt default)
- **External:** 8502 (mapped in docker-compose.yml)
- **API Proxy:** `NUXT_PUBLIC_API_BASE_URL=http://sf_api:8000`
#### Volume Strategy
- Development: Hot-reload with mounted source code
- Production: Built assets only for smaller image size
### 8. API Integration Strategy
#### Headers Injection
All authenticated requests automatically include:
- `Authorization: Bearer <token>`
- `X-Scope-Id: <scope_id>`
- `X-Region-Code: <region_code>`
- `X-Scope-Level: <scope_level>`
#### Error Handling
- Token expiration detection and auto-logout
- Permission denied redirects to `/unauthorized`
- Network error handling with user feedback
### 9. Security Considerations
#### Client-side Security
- No sensitive data in client-side code
- Token storage in localStorage with expiration checks
- Role validation on both client and server
- XSS protection through Vue's template system
#### Geographical Isolation
- Scope validation before data display
- Region-based data filtering at API level
- Visual indicators for current scope context
### 10. Performance Optimizations
#### Code Splitting
- Route-based code splitting via Nuxt
- Component lazy loading where appropriate
- Vendor chunk optimization
#### Asset Optimization
- Vuetify tree-shaking in production
- CSS purging for unused styles
- Image optimization pipeline
#### Caching Strategy
- LocalStorage for user preferences
- Token validation caching
- Tile configuration caching per session
## Implementation Status
### ✅ Completed Phase 1
1. Project initialization with Nuxt 3 + Vuetify 3
2. Docker configuration and docker-compose integration
3. Pinia auth store with JWT parsing
4. Global authentication middleware
5. RBAC composable with role/scope validation
6. Basic dashboard layout
### ✅ Completed Phase 2
1. Launchpad tile system with dynamic filtering
2. TileCard component with role-based styling
3. User preference store for tile customization
4. Geographical scope integration
5. Complete dashboard with stats and navigation
### 🔄 Pending for Phase 3
1. Geographical map integration (Leaflet/Vue3-leaflet)
2. Individual tile pages (AI Logs, Finance, Users, etc.)
3. User management interface
4. Real-time data updates
5. Comprehensive testing suite
## Known Issues & TODOs
### Immediate TODOs
1. Install dependencies (`npm install` in container)
2. Create login page component
3. Implement API service with axios interceptors
4. Add error boundary components
5. Create unauthorized/404 pages
### Technical Debt
1. TypeScript strict mode configuration needs refinement
2. Vuetify theme customization for brand colors
3. Internationalization (i18n) setup
4. E2E testing with Cypress
5. Performance benchmarking
## Deployment Notes
### Environment Variables
```bash
NUXT_PUBLIC_API_BASE_URL=http://localhost:8000
NODE_ENV=production
NUXT_HOST=0.0.0.0
NUXT_PORT=3000
```
### Build Commands
```bash
# Development
npm run dev
# Production build
npm run build
npm run preview
# Docker build
docker build -t sf-admin-frontend .
```
### Health Checks
- `/api/health` endpoint for container health checks
- Docker HEALTHCHECK directive in Dockerfile
- Log aggregation for monitoring
## Ticket #113: RBAC Implementation & Role Management System
**Date:** 2026-03-23
**Status:** In Progress
**Assigned:** Fast Coder
**Gitea Issue:** #113
### Task Breakdown
#### Task 1: User Management Interface (RBAC Admin)
1. Create `/users` page accessible only by Superadmin and Admin ranks
2. Build Vuetify Data Table with columns: Email, Current Role, Scope Level, Status
3. Create "Edit Role" dialog for changing UserRole and scope_level
4. Implement API composable with mock service (fallback when backend endpoints not available)
#### Task 2: Live "Gold Vehicle" AI Logs Tile (Launchpad)
1. Create "AI Logs Monitor" tile component for Launchpad
2. Implement polling mechanism (3-second intervals) using Vue's onMounted and setInterval
3. Fetch data from `/api/v1/vehicles/recent-activity` with mock fallback
4. Display real-time log entries with visual feedback
#### Task 3: Connect Existing API
1. Create API client for GET `/api/v1/admin/health-monitor`
2. Display metrics on System Health tile: total_assets, total_organizations, critical_alerts_24h
3. Ensure proper error handling and loading states
### Implementation Plan
#### Component Structure
```
frontend/admin/
├── pages/users.vue # User management page
├── components/UserDataTable.vue # Vuetify data table component
├── components/EditRoleDialog.vue # Role editing dialog
├── components/AiLogsTile.vue # AI Logs tile for Launchpad
├── composables/useUserManagement.ts # User management API composable
├── composables/useAiLogs.ts # AI logs polling composable
├── composables/useHealthMonitor.ts # Health monitor API composable
└── stores/users.ts # Pinia store for user data
```
#### API Integration Strategy
- **Mock Services:** Implement fallback mock data for development/testing
- **Real API:** Switch to real endpoints when backend is ready
- **Error Handling:** Graceful degradation with user notifications
- **Type Safety:** Full TypeScript interfaces for all API responses
#### RBAC Protection
- Route-level protection via middleware
- Component-level guards using `useRBAC` composable
- Visual indicators for unauthorized access attempts
### Progress Tracking
- [x] Ticket #113 set to "In Progress" via Gitea manager
- [x] User Management page created
- [x] Vuetify Data Table implemented
- [x] Edit Role dialog completed
- [x] API composables with mock services
- [x] AI Logs Tile component
- [x] Polling mechanism implemented
- [x] Health monitor API integration
- [x] System Health tile updated
- [x] Comprehensive testing
- [x] Ticket closure with technical summary
## Epic 10 - Ticket 2: Launchpad UI & Modular Tile System (#114)
**Date:** 2026-03-23
**Status:** In Progress
**Goal:** Upgrade the static Launchpad into a dynamic, drag-and-drop Grid system where users can rearrange their authorized tiles.
### Task Breakdown
#### Task 1: Drag-and-Drop Grid Implementation
1. Install vuedraggable (or equivalent Vue 3 compatible drag-and-drop grid system)
2. Refactor the Launchpad (Dashboard.vue) to use a draggable grid layout for the tiles
3. Ensure the grid is responsive (e.g., 1 column on mobile, multiple on desktop)
#### Task 2: Modular Tile Component Framework
1. Create a base TileWrapper.vue component that handles the drag handle (icon), title bar, and RBAC visibility checks
2. Wrap the existing AiLogsTile and SystemHealthTile inside this new wrapper
#### Task 3: Layout Persistence & "Reset to Default"
1. Update the Pinia store (usePreferencesStore) to handle layout state
2. Maintain a defaultLayout array (hardcoded standard order) and a userLayout array
3. Persist the userLayout to localStorage so the custom layout survives page reloads
4. Add a "Restore Default Layout" (Alapértelmezett elrendezés) UI button on the Launchpad that resets userLayout back to defaultLayout
### Implementation Plan
#### Component Structure Updates
```
frontend/admin/
├── components/TileWrapper.vue # Base tile wrapper with drag handle
├── components/AiLogsTile.vue # Updated to use wrapper
├── components/SystemHealthTile.vue # Updated to use wrapper
├── stores/preferences.ts # New store for layout preferences
└── pages/dashboard.vue # Updated with draggable grid
```
#### Technical Specifications
- **Drag & Drop Library:** `vuedraggable@next` (Vue 3 compatible)
- **Grid System:** CSS Grid with responsive breakpoints
- **State Persistence:** localStorage with fallback to default
- **RBAC Integration:** Tile visibility controlled via `useRBAC` composable
- **Default Layout:** Hardcoded array of tile IDs in order of appearance
### Progress Tracking
- [x] Ticket #114 set to "In Progress" via Gitea manager
- [x] TODO lista létrehozása development_log.md fájlban
- [x] vuedraggable csomag telepítése (v4.1.0 for Vue 3)
- [x] Dashboard.vue átalakítása draggable grid-re (Draggable component integration)
- [x] TileWrapper.vue alapkomponens létrehozása (with drag handle, RBAC badges, visibility toggle)
- [x] Meglévő tile-ok becsomagolása TileWrapper-be (TileCard wrapped in TileWrapper)
- [x] Pinia store frissítése layout kezeléshez (added defaultLayout and isLayoutModified computed properties)
- [x] Layout persistencia localStorage-ban (existing loadPreferences/savePreferences enhanced)
- [x] "Restore Default Layout" gomb implementálása (button with conditional display based on isLayoutModified)
- [x] Tesztelés és finomhangolás
- [x] Gitea Ticket #114 lezárása (Ticket closed with technical summary)
## Conclusion
The Epic 10 Admin Frontend Phase 1 & 2 implementation establishes a solid foundation for the Mission Control dashboard. The architecture supports the core requirements of geographical RBAC isolation, modular launchpad tiles, and role-based access control. The system is ready for integration with the backend FastAPI services and can be extended with additional tiles and features as specified in the epic specification.

View File

@@ -1,69 +0,0 @@
{
"navigation": {
"dashboard": "Dashboard",
"users": "Users",
"map": "Map",
"settings": "Settings",
"logout": "Logout",
"welcome": "Welcome",
"role_management": "Role Management",
"geographical_scopes": "Geographical Scopes"
},
"tiles": {
"ai_logs": "AI Logs",
"financial": "Financial",
"sales": "Sales",
"system_health": "System Health",
"service_map": "Service Map",
"moderation": "Moderation"
},
"general": {
"save": "Save",
"cancel": "Cancel",
"edit": "Edit",
"delete": "Delete",
"confirm": "Confirm",
"role": "Role",
"scope": "Scope",
"status": "Status",
"actions": "Actions",
"search": "Search",
"filter": "Filter",
"refresh": "Refresh",
"loading": "Loading...",
"no_data": "No data available",
"error": "Error",
"success": "Success",
"warning": "Warning",
"info": "Info",
"settings": "Settings"
},
"dashboard": {
"title": "Admin Dashboard",
"subtitle": "Monitor and manage your service ecosystem",
"welcome_title": "Welcome to Mission Control",
"welcome_subtitle": "Real-time oversight for {scopeLevel} level administration",
"total_users": "Total Users",
"active_services": "Active Services",
"pending_requests": "Pending Requests",
"system_status": "System Status"
},
"users": {
"title": "User Management",
"add_user": "Add User",
"username": "Username",
"email": "Email",
"created_at": "Created At",
"last_login": "Last Login",
"active": "Active",
"inactive": "Inactive"
},
"login": {
"title": "Login to Admin",
"username": "Username",
"password": "Password",
"remember_me": "Remember me",
"forgot_password": "Forgot password?",
"sign_in": "Sign In"
}
}

View File

@@ -1,69 +0,0 @@
{
"navigation": {
"dashboard": "Irányítópult",
"users": "Felhasználók",
"map": "Térkép",
"settings": "Beállítások",
"logout": "Kijelentkezés",
"welcome": "Üdvözöljük",
"role_management": "Szerepkör Kezelés",
"geographical_scopes": "Földrajzi Hatáskörök"
},
"tiles": {
"ai_logs": "AI Naplók",
"financial": "Pénzügyi",
"sales": "Értékesítés",
"system_health": "Rendszerállapot",
"service_map": "Szolgáltatási Térkép",
"moderation": "Moderálás"
},
"general": {
"save": "Mentés",
"cancel": "Mégse",
"edit": "Szerkesztés",
"delete": "Törlés",
"confirm": "Megerősítés",
"role": "Szerepkör",
"scope": "Hatáskör",
"status": "Állapot",
"actions": "Műveletek",
"search": "Keresés",
"filter": "Szűrés",
"refresh": "Frissítés",
"loading": "Betöltés...",
"no_data": "Nincs elérhető adat",
"error": "Hiba",
"success": "Siker",
"warning": "Figyelmeztetés",
"info": "Információ",
"settings": "Beállítások"
},
"dashboard": {
"title": "Admin Irányítópult",
"subtitle": "Figyelje és kezelje szolgáltatási ökoszisztémáját",
"welcome_title": "Üdvözöljük a Mission Control-ban",
"welcome_subtitle": "Valós idejű felügyelet {scopeLevel} szintű adminisztrációhoz",
"total_users": "Összes felhasználó",
"active_services": "Aktív szolgáltatások",
"pending_requests": "Függőben lévő kérések",
"system_status": "Rendszerállapot"
},
"users": {
"title": "Felhasználókezelés",
"add_user": "Felhasználó hozzáadása",
"username": "Felhasználónév",
"email": "E-mail",
"created_at": "Létrehozva",
"last_login": "Utolsó bejelentkezés",
"active": "Aktív",
"inactive": "Inaktív"
},
"login": {
"title": "Bejelentkezés az Adminba",
"username": "Felhasználónév",
"password": "Jelszó",
"remember_me": "Emlékezz rám",
"forgot_password": "Elfelejtette a jelszavát?",
"sign_in": "Bejelentkezés"
}
}

View File

@@ -1,83 +0,0 @@
import { useAuthStore } from '~/stores/auth'
export default defineNuxtRouteMiddleware((to, from) => {
// Skip auth checks on server-side (SSR) - localStorage not available
if (process.server) {
return
}
const authStore = useAuthStore()
const nuxtApp = useNuxtApp()
// Public routes that don't require authentication
const publicRoutes = ['/login', '/forgot-password', '/reset-password']
// Check if route requires authentication
const requiresAuth = !publicRoutes.includes(to.path)
// If route requires auth and user is not authenticated, redirect to login
if (requiresAuth && !authStore.isAuthenticated) {
return navigateTo('/login')
}
// If user is authenticated and trying to access login page, redirect to dashboard
if (to.path === '/login' && authStore.isAuthenticated) {
return navigateTo('/dashboard')
}
// Check role-based access for protected routes
if (requiresAuth && authStore.isAuthenticated) {
const routeMeta = to.meta || {}
const requiredRole = routeMeta.requiredRole as string | undefined
const minRank = routeMeta.minRank as number | undefined
const requiredPermission = routeMeta.requiredPermission as string | undefined
// Check role requirement
if (requiredRole && authStore.getUserRole !== requiredRole) {
console.warn(`Access denied: Route requires role ${requiredRole}, user has ${authStore.getUserRole}`)
return navigateTo('/unauthorized')
}
// Check rank requirement
if (minRank !== undefined && !authStore.hasRank(minRank)) {
console.warn(`Access denied: Route requires rank ${minRank}, user has rank ${authStore.getUserRank}`)
return navigateTo('/unauthorized')
}
// Check permission requirement
if (requiredPermission && !authStore.hasPermission(requiredPermission)) {
console.warn(`Access denied: Route requires permission ${requiredPermission}`)
return navigateTo('/unauthorized')
}
// Check geographical scope for scoped routes
const requiredScopeId = routeMeta.requiredScopeId as number | undefined
const requiredRegionCode = routeMeta.requiredRegionCode as string | undefined
if (requiredScopeId || requiredRegionCode) {
if (!authStore.canAccessScope(requiredScopeId || 0, requiredRegionCode)) {
console.warn(`Access denied: User cannot access requested scope`)
return navigateTo('/unauthorized')
}
}
}
// Add auth headers to all API requests if authenticated
if (process.client && authStore.isAuthenticated && authStore.token) {
const { $api } = nuxtApp
if ($api && $api.defaults) {
$api.defaults.headers.common['Authorization'] = `Bearer ${authStore.token}`
// Add geographical scope headers for backend filtering
if (authStore.getScopeId) {
$api.defaults.headers.common['X-Scope-Id'] = authStore.getScopeId.toString()
}
if (authStore.getRegionCode) {
$api.defaults.headers.common['X-Region-Code'] = authStore.getRegionCode
}
if (authStore.getScopeLevel) {
$api.defaults.headers.common['X-Scope-Level'] = authStore.getScopeLevel
}
}
}
})

View File

@@ -1,61 +0,0 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-11-01',
devtools: { enabled: false },
modules: [
'@pinia/nuxt',
'@nuxtjs/tailwindcss',
'vuetify-nuxt-module',
'@nuxtjs/i18n'
],
i18n: {
locales: [
{ code: 'en', iso: 'en-US', file: 'en.json', name: 'English' },
{ code: 'hu', iso: 'hu-HU', file: 'hu.json', name: 'Magyar' }
],
defaultLocale: 'hu',
lazy: true,
langDir: 'locales',
strategy: 'no_prefix'
},
vuetify: {
moduleOptions: {
/* module specific options */
},
vuetifyOptions: {
/* vuetify options */
}
},
css: ['vuetify/lib/styles/main.sass', '@mdi/font/css/materialdesignicons.min.css'],
build: {
transpile: ['vuetify'],
},
vite: {
define: {
'process.env.DEBUG': false,
},
server: {
allowedHosts: ['admin.servicefinder.hu']
},
},
runtimeConfig: {
public: {
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || 'http://localhost:8000',
appName: 'Service Finder Admin',
appVersion: '1.0.0'
}
},
// Nitro proxy configuration for Docker networking
routeRules: {
'/api/**': {
proxy: 'http://sf_api:8000/api/**',
// Add CORS headers for development
cors: true,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
}
}
}
})

View File

@@ -1,40 +0,0 @@
{
"name": "sf-admin-ui",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"devDependencies": {
"@nuxt/devtools": "latest",
"@nuxtjs/i18n": "^8.5.6",
"@nuxtjs/tailwindcss": "^6.8.0",
"@types/node": "^20.11.24",
"@unhead/vue": "^1.8.9",
"@vuetify/loader-shared": "^2.1.2",
"nuxt": "^3.11.0",
"sass-embedded": "^1.83.4",
"typescript": "^5.3.3",
"unhead": "^1.8.9",
"vuetify-nuxt-module": "^0.4.12"
},
"dependencies": {
"@mdi/font": "^7.4.47",
"@pinia/nuxt": "^0.5.1",
"axios": "^1.6.7",
"chart.js": "^4.4.1",
"jwt-decode": "^4.0.0",
"leaflet": "^1.9.4",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-chartjs": "^5.2.0",
"vue-router": "^4.2.5",
"vue3-leaflet": "^1.0.19",
"vuedraggable": "^4.1.0",
"vuetify": "^3.5.13"
}
}

View File

@@ -1,760 +0,0 @@
<template>
<v-app>
<!-- App Bar -->
<v-app-bar color="primary" prominent>
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
<v-toolbar-title class="text-h5 font-weight-bold">
<v-icon icon="mdi-rocket-launch" class="mr-2"></v-icon>
{{ t('dashboard.title') }}
<v-chip class="ml-2" :color="roleColor" size="small">
{{ userRole }} {{ scopeLevel }}
</v-chip>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Language Switcher -->
<v-menu>
<template v-slot:activator="{ props }">
<v-btn icon v-bind="props" class="mr-2">
<v-icon icon="mdi-translate"></v-icon>
</v-btn>
</template>
<v-list>
<v-list-item @click="locale = 'hu'">
<v-list-item-title :class="{ 'font-weight-bold': locale === 'hu' }">
🇭🇺 Magyar
</v-list-item-title>
</v-list-item>
<v-list-item @click="locale = 'en'">
<v-list-item-title :class="{ 'font-weight-bold': locale === 'en' }">
🇬🇧 English
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<!-- User Menu -->
<v-menu>
<template v-slot:activator="{ props }">
<v-btn icon v-bind="props">
<v-avatar size="40" color="secondary">
<v-icon icon="mdi-account"></v-icon>
</v-avatar>
</v-btn>
</template>
<v-list>
<v-list-item>
<v-list-item-title class="font-weight-bold">
{{ userEmail }}
</v-list-item-title>
<v-list-item-subtitle>
Rank: {{ userRank }} Scope ID: {{ scopeId }}
</v-list-item-subtitle>
</v-list-item>
<v-divider></v-divider>
<v-list-item @click="navigateTo('/profile')">
<v-list-item-title>
<v-icon icon="mdi-account-cog" class="mr-2"></v-icon>
{{ t('general.settings') }}
</v-list-item-title>
</v-list-item>
<v-list-item @click="logout">
<v-list-item-title class="text-error">
<v-icon icon="mdi-logout" class="mr-2"></v-icon>
{{ t('navigation.logout') }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-app-bar>
<!-- Navigation Drawer -->
<v-navigation-drawer v-model="drawer" temporary>
<v-list>
<v-list-item prepend-icon="mdi-view-dashboard" :title="t('navigation.dashboard')" value="dashboard" @click="navigateTo('/dashboard')"></v-list-item>
<v-list-item prepend-icon="mdi-cog" :title="t('navigation.settings')" value="settings" @click="navigateTo('/settings')"></v-list-item>
<v-list-item prepend-icon="mdi-shield-account" :title="t('navigation.role_management')" value="roles" @click="navigateTo('/roles')"></v-list-item>
<v-list-item prepend-icon="mdi-map" :title="t('navigation.geographical_scopes')" value="scopes" @click="navigateTo('/scopes')"></v-list-item>
</v-list>
<template v-slot:append>
<v-list>
<v-list-item>
<v-list-item-title class="text-caption text-disabled">
Service Finder Admin v{{ appVersion }}
</v-list-item-title>
</v-list-item>
</v-list>
</template>
</v-navigation-drawer>
<!-- Main Content -->
<v-main>
<v-container fluid class="pa-6">
<!-- Welcome Header -->
<v-row class="mb-6">
<v-col cols="12">
<v-card color="primary" variant="tonal" class="pa-4">
<v-card-title class="text-h4 font-weight-bold">
<v-icon icon="mdi-rocket" class="mr-2"></v-icon>
{{ t('dashboard.welcome_title') }}
</v-card-title>
<v-card-subtitle class="text-h6">
{{ t('dashboard.welcome_subtitle', { scopeLevel }) }}
</v-card-subtitle>
<v-card-text>
<v-chip class="mr-2" color="success">
<v-icon icon="mdi-check-circle" class="mr-1"></v-icon>
Authenticated as {{ userRole }}
</v-chip>
<v-chip class="mr-2" color="info">
<v-icon icon="mdi-map-marker" class="mr-1"></v-icon>
Scope: {{ regionCode || 'Global' }}
</v-chip>
<v-chip color="warning">
<v-icon icon="mdi-shield-star" class="mr-1"></v-icon>
Rank: {{ userRank }}
</v-chip>
<!-- Layout Controls -->
<v-btn
v-if="tileStore.isLayoutModified"
class="ml-2"
color="warning"
size="small"
variant="outlined"
@click="restoreDefaultLayout"
:loading="isRestoringLayout"
>
<v-icon icon="mdi-restore" class="mr-1"></v-icon>
Restore Default Layout
</v-btn>
<v-tooltip v-else location="bottom">
<template v-slot:activator="{ props }">
<v-chip
v-bind="props"
class="ml-2"
color="success"
size="small"
variant="outlined"
>
<v-icon icon="mdi-check-all" class="mr-1"></v-icon>
Default Layout Active
</v-chip>
</template>
<span>Your dashboard layout matches the default configuration</span>
</v-tooltip>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- Launchpad Section -->
<v-row class="mb-4">
<v-col cols="12">
<div class="d-flex align-center justify-space-between">
<v-card-title class="text-h5 font-weight-bold pa-0">
<v-icon icon="mdi-view-grid" class="mr-2"></v-icon>
Launchpad
</v-card-title>
<v-btn variant="tonal" color="primary" prepend-icon="mdi-cog">
Customize Tiles
</v-btn>
</div>
<v-card-subtitle class="pa-0">
Role-based dashboard with {{ filteredTiles.length }} accessible tiles
</v-card-subtitle>
</v-col>
</v-row>
<!-- Dynamic Tiles Grid with Drag & Drop -->
<Draggable
v-model="draggableTiles"
tag="v-row"
item-key="id"
class="drag-container"
@end="onDragEnd"
:component-data="{ class: 'drag-row' }"
:animation="200"
:ghost-class="'ghost-tile'"
:chosen-class="'chosen-tile'"
>
<template #item="{ element: tile }">
<v-col
cols="12"
sm="6"
md="4"
lg="3"
class="drag-col"
>
<TileWrapper :tile="tile" @click="handleTileClick">
<template #default>
<p class="text-body-2">{{ tile.description }}</p>
<!-- Requirements Badges -->
<div class="mt-2">
<v-chip
v-for="role in tile.requiredRole"
:key="role"
size="x-small"
class="mr-1 mb-1"
variant="outlined"
>
{{ role }}
</v-chip>
<v-chip
v-if="tile.minRank"
size="x-small"
class="mr-1 mb-1"
color="warning"
variant="outlined"
>
Rank {{ tile.minRank }}+
</v-chip>
</div>
<!-- Scope Level Indicator -->
<div v-if="tile.scopeLevel && tile.scopeLevel.length > 0" class="mt-2">
<v-icon icon="mdi-map-marker" size="small" class="mr-1"></v-icon>
<span class="text-caption">
{{ tile.scopeLevel.join(', ') }}
</span>
</div>
</template>
</TileWrapper>
</v-col>
</template>
<!-- Empty State -->
<template #footer v-if="draggableTiles.length === 0">
<v-col cols="12">
<v-card class="pa-8 text-center">
<v-icon icon="mdi-lock" size="64" class="mb-4 text-disabled"></v-icon>
<v-card-title class="text-h5">
No Tiles Available
</v-card-title>
<v-card-text>
Your current role ({{ userRole }}) doesn't have access to any dashboard tiles.
Contact your administrator for additional permissions.
</v-card-text>
</v-card>
</v-col>
</template>
</Draggable>
<!-- Quick Stats -->
<v-row class="mt-8">
<v-col cols="12">
<v-card-title class="text-h5 font-weight-bold pa-0">
<v-icon icon="mdi-chart-line" class="mr-2"></v-icon>
System Health Dashboard
<v-btn
icon="mdi-refresh"
size="small"
variant="text"
class="ml-2"
@click="healthMonitor.refreshAll"
:loading="healthMonitor.loading"
></v-btn>
</v-card-title>
<v-card-subtitle class="pa-0">
Real-time system metrics from health-monitor API
<v-chip
v-if="healthMonitor.metrics"
:color="healthMonitor.systemStatusColor"
size="small"
class="ml-2"
>
<v-icon :icon="healthMonitor.systemStatusIcon" size="small" class="mr-1"></v-icon>
{{ healthMonitor.metrics?.system_status?.toUpperCase() || 'LOADING' }}
</v-chip>
</v-card-subtitle>
</v-col>
<!-- Total Assets -->
<v-col cols="12" md="3">
<v-card
class="pa-4"
elevation="3"
rounded="xl"
:color="healthMonitor.loading ? 'grey-lighten-4' : 'surface'"
:loading="healthMonitor.loading && !healthMonitor.metrics"
>
<v-card-title class="text-h6 d-flex align-center">
<v-icon icon="mdi-car" class="mr-2" color="indigo-darken-2"></v-icon>
Total Vehicles
<v-spacer></v-spacer>
<v-progress-circular
v-if="healthMonitor.loading && !healthMonitor.metrics"
indeterminate
size="20"
width="2"
color="indigo"
></v-progress-circular>
</v-card-title>
<v-card-text class="text-h3 font-weight-bold text-indigo-darken-2">
{{ healthMonitor.metrics?.total_assets?.toLocaleString() || '--' }}
</v-card-text>
<v-card-subtitle class="text-caption">
<v-icon icon="mdi-database" size="small" class="mr-1"></v-icon>
Seeded vehicles in database
</v-card-subtitle>
<v-divider class="my-2"></v-divider>
<div class="d-flex align-center mt-2">
<v-icon icon="mdi-check-circle" color="success" size="small" class="mr-1"></v-icon>
<span class="text-caption text-disabled">Live from PostgreSQL</span>
</div>
</v-card>
</v-col>
<!-- Total Organizations -->
<v-col cols="12" md="3">
<v-card
class="pa-4"
elevation="3"
rounded="xl"
:color="healthMonitor.loading ? 'grey-lighten-4' : 'surface'"
:loading="healthMonitor.loading && !healthMonitor.metrics"
>
<v-card-title class="text-h6 d-flex align-center">
<v-icon icon="mdi-office-building" class="mr-2" color="teal-darken-2"></v-icon>
Organizations
<v-spacer></v-spacer>
<v-progress-circular
v-if="healthMonitor.loading && !healthMonitor.metrics"
indeterminate
size="20"
width="2"
color="teal"
></v-progress-circular>
</v-card-title>
<v-card-text class="text-h3 font-weight-bold text-teal-darken-2">
{{ healthMonitor.metrics?.total_organizations?.toLocaleString() || '--' }}
</v-card-text>
<v-card-subtitle class="text-caption">
<v-icon icon="mdi-domain" size="small" class="mr-1"></v-icon>
Registered business entities
</v-card-subtitle>
<v-divider class="my-2"></v-divider>
<div class="d-flex align-center mt-2">
<v-icon icon="mdi-check-circle" color="success" size="small" class="mr-1"></v-icon>
<span class="text-caption text-disabled">Real API data</span>
</div>
</v-card>
</v-col>
<!-- Active Users -->
<v-col cols="12" md="3">
<v-card
class="pa-4"
elevation="3"
rounded="xl"
:color="healthMonitor.loading ? 'grey-lighten-4' : 'surface'"
:loading="healthMonitor.loading && !healthMonitor.metrics"
>
<v-card-title class="text-h6 d-flex align-center">
<v-icon icon="mdi-account-group" class="mr-2" color="emerald-darken-2"></v-icon>
Active Users
<v-spacer></v-spacer>
<v-progress-circular
v-if="healthMonitor.loading && !healthMonitor.metrics"
indeterminate
size="20"
width="2"
color="emerald"
></v-progress-circular>
</v-card-title>
<v-card-text class="text-h3 font-weight-bold text-emerald-darken-2">
{{ healthMonitor.metrics?.active_users?.toLocaleString() || '--' }}
</v-card-text>
<v-card-subtitle class="text-caption">
<v-icon icon="mdi-account" size="small" class="mr-1"></v-icon>
Total registered users
</v-card-subtitle>
<v-divider class="my-2"></v-divider>
<div class="d-flex align-center mt-2">
<v-icon icon="mdi-check-circle" color="success" size="small" class="mr-1"></v-icon>
<span class="text-caption text-disabled">Including superadmin</span>
</div>
</v-card>
</v-col>
<!-- Critical Alerts -->
<v-col cols="12" md="3">
<v-card
class="pa-4"
elevation="3"
rounded="xl"
:color="healthMonitor.loading ? 'grey-lighten-4' : 'surface'"
:loading="healthMonitor.loading && !healthMonitor.metrics"
:border="healthMonitor.metrics?.critical_alerts_24h ? 'left' : false"
:color-border="healthMonitor.metrics?.critical_alerts_24h ? 'error' : 'success'"
>
<v-card-title class="text-h6 d-flex align-center">
<v-icon
:icon="healthMonitor.metrics?.critical_alerts_24h ? 'mdi-alert-octagon' : 'mdi-shield-check'"
class="mr-2"
:color="healthMonitor.metrics?.critical_alerts_24h ? 'error' : 'success'"
></v-icon>
System Health
<v-spacer></v-spacer>
<v-progress-circular
v-if="healthMonitor.loading && !healthMonitor.metrics"
indeterminate
size="20"
width="2"
:color="healthMonitor.metrics?.critical_alerts_24h ? 'error' : 'success'"
></v-progress-circular>
</v-card-title>
<v-card-text
class="text-h3 font-weight-bold"
:class="healthMonitor.metrics?.critical_alerts_24h ? 'text-error' : 'text-success'"
>
{{ healthMonitor.metrics?.critical_alerts_24h || 0 }}
</v-card-text>
<v-card-subtitle class="text-caption">
<v-icon
:icon="healthMonitor.metrics?.critical_alerts_24h ? 'mdi-alert' : 'mdi-check-circle'"
size="small"
class="mr-1"
:color="healthMonitor.metrics?.critical_alerts_24h ? 'error' : 'success'"
></v-icon>
<span :class="healthMonitor.metrics?.critical_alerts_24h ? 'text-error' : 'text-success'">
{{ healthMonitor.metrics?.critical_alerts_24h ? 'Critical alerts' : 'All systems operational' }}
</span>
</v-card-subtitle>
<v-divider class="my-2"></v-divider>
<div class="d-flex align-center mt-2">
<v-icon
:icon="healthMonitor.metrics?.critical_alerts_24h ? 'mdi-clock-alert' : 'mdi-clock-check'"
:color="healthMonitor.metrics?.critical_alerts_24h ? 'warning' : 'success'"
size="small"
class="mr-1"
></v-icon>
<span class="text-caption text-disabled">Last 24 hours</span>
</div>
</v-card>
</v-col>
</v-row>
<!-- Performance Metrics Row -->
<v-row class="mt-4">
<v-col cols="12" md="4">
<v-card
class="pa-4"
elevation="2"
rounded="lg"
color="surface-variant"
>
<v-card-title class="text-h6 d-flex align-center">
<v-icon icon="mdi-speedometer" class="mr-2" color="deep-purple"></v-icon>
System Uptime
</v-card-title>
<v-card-text class="text-h2 font-weight-bold text-deep-purple-darken-2">
{{ healthMonitor.formattedUptime }}
</v-card-text>
<v-card-subtitle class="text-caption">
<v-icon icon="mdi-heart-pulse" size="small" class="mr-1"></v-icon>
Service availability
</v-card-subtitle>
<v-divider class="my-2"></v-divider>
<div class="d-flex align-center justify-space-between">
<span class="text-caption">Response time:</span>
<span class="text-caption font-weight-medium" :class="getResponseTimeClass(healthMonitor.metrics?.response_time_ms)">
{{ healthMonitor.formattedResponseTime }}
<v-icon
:icon="getResponseTimeIcon(healthMonitor.metrics?.response_time_ms)"
size="small"
class="ml-1"
:color="getResponseTimeColor(healthMonitor.metrics?.response_time_ms)"
></v-icon>
</span>
</div>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card
class="pa-4"
elevation="2"
rounded="lg"
color="surface-variant"
>
<v-card-title class="text-h6 d-flex align-center">
<v-icon icon="mdi-database" class="mr-2" color="blue-grey"></v-icon>
Database Status
</v-card-title>
<v-card-text class="text-h2 font-weight-bold" :class="getDbConnectionClass(healthMonitor.metrics?.database_connections)">
{{ healthMonitor.metrics?.database_connections || '0' }}
</v-card-text>
<v-card-subtitle class="text-caption">
<v-icon icon="mdi-connection" size="small" class="mr-1"></v-icon>
Active connections
</v-card-subtitle>
<v-divider class="my-2"></v-divider>
<div class="d-flex align-center">
<v-icon
:icon="getDbStatusIcon(healthMonitor.metrics?.database_connections)"
:color="getDbStatusColor(healthMonitor.metrics?.database_connections)"
size="small"
class="mr-1"
></v-icon>
<span class="text-caption" :class="getDbStatusTextClass(healthMonitor.metrics?.database_connections)">
{{ getDbStatusText(healthMonitor.metrics?.database_connections) }}
</span>
</div>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card
class="pa-4"
elevation="2"
rounded="lg"
color="surface-variant"
>
<v-card-title class="text-h6 d-flex align-center">
<v-icon icon="mdi-update" class="mr-2" color="amber"></v-icon>
Data Freshness
</v-card-title>
<v-card-text class="text-h2 font-weight-bold text-amber-darken-2">
{{ healthMonitor.lastUpdated ? formatTime(healthMonitor.lastUpdated) : 'Never' }}
</v-card-text>
<v-card-subtitle class="text-caption">
<v-icon icon="mdi-clock-outline" size="small" class="mr-1"></v-icon>
Last API sync
</v-card-subtitle>
<v-divider class="my-2"></v-divider>
<div class="d-flex align-center justify-space-between">
<span class="text-caption">Auto-refresh:</span>
<v-chip size="x-small" color="info" variant="outlined">
<v-icon icon="mdi-autorenew" size="x-small" class="mr-1"></v-icon>
30s
</v-chip>
</div>
</v-card>
</v-col>
</v-row>
</v-container>
</v-main>
<!-- Footer -->
<v-footer app color="surface" class="px-4">
<v-spacer></v-spacer>
<div class="text-caption text-disabled">
Geographical Scope: {{ regionCode || 'Global' }} •
Last sync: {{ new Date().toLocaleTimeString() }} •
<v-icon icon="mdi-circle-small" class="mx-1" color="success"></v-icon>
All systems operational
</div>
</v-footer>
</v-app>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useAuthStore } from '~/stores/auth'
import { useRBAC } from '~/composables/useRBAC'
import { useHealthMonitor } from '~/composables/useHealthMonitor'
import { useTileStore } from '~/stores/tiles'
import TileCard from '~/components/TileCard.vue'
import Draggable from 'vuedraggable'
import { useI18n } from 'vue-i18n'
// i18n
const { t, locale } = useI18n()
// State
const drawer = ref(false)
const appVersion = '1.0.0'
const tileStore = useTileStore()
const isRestoringLayout = ref(false)
const draggableTiles = ref<any[]>([])
// Stores and composables
const authStore = useAuthStore()
const rbac = useRBAC()
const healthMonitor = useHealthMonitor()
// Computed properties
const userEmail = computed(() => authStore.user?.email || '')
const userRole = computed(() => authStore.getUserRole || '')
const userRank = computed(() => authStore.getUserRank || 0)
const scopeLevel = computed(() => authStore.getScopeLevel || '')
const regionCode = computed(() => authStore.getRegionCode || '')
const scopeId = computed(() => authStore.getScopeId || 0)
const roleColor = computed(() => rbac.getRoleColor())
const filteredTiles = computed(() => tileStore.visibleTiles)
// Watch for changes to filteredTiles and update draggableTiles
watch(filteredTiles, (newTiles) => {
draggableTiles.value = [...newTiles]
}, { immediate: true })
// Methods
function logout() {
authStore.logout()
navigateTo('/login')
}
// Drag & Drop handling
function onDragEnd() {
const tileIds = draggableTiles.value.map(tile => tile.id)
tileStore.updateTilePositions(tileIds)
}
// Tile click handling
function handleTileClick(tile: any) {
const routes: Record<string, string> = {
'ai-logs': '/ai-logs',
'financial-dashboard': '/finance',
'salesperson-hub': '/sales',
'user-management': '/users',
'service-moderation-map': '/moderation-map',
'gamification-control': '/gamification',
'system-health': '/system'
}
const route = routes[tile.id]
if (route) {
navigateTo(route)
} else {
console.warn(`No route defined for tile: ${tile.id}`)
}
}
// Restore default layout
async function restoreDefaultLayout() {
isRestoringLayout.value = true
try {
tileStore.resetPreferences()
// Show success message
console.log('Layout restored to default')
} catch (error) {
console.error('Failed to restore layout:', error)
} finally {
isRestoringLayout.value = false
}
}
// Helper functions
const getDbConnectionClass = (connections: number | undefined) => {
if (!connections) return 'text-grey'
if (connections > 40) return 'text-error'
if (connections > 20) return 'text-warning'
return 'text-success'
}
const formatTime = (value: any) => {
if (!value) return 'N/A';
try {
const d = new Date(value);
// Check if it's a valid date
if (isNaN(d.getTime())) return String(value);
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
} catch (e) {
return 'Invalid Time';
}
}
// Response time helper functions
const getResponseTimeClass = (responseTime: number | undefined) => {
if (!responseTime) return 'text-grey'
if (responseTime < 100) return 'text-success'
if (responseTime < 300) return 'text-warning'
return 'text-error'
}
const getResponseTimeIcon = (responseTime: number | undefined) => {
if (!responseTime) return 'mdi-help-circle'
if (responseTime < 100) return 'mdi-check'
if (responseTime < 300) return 'mdi-alert'
return 'mdi-alert-circle'
}
const getResponseTimeColor = (responseTime: number | undefined) => {
if (!responseTime) return 'grey'
if (responseTime < 100) return 'success'
if (responseTime < 300) return 'warning'
return 'error'
}
// Database status helper functions
const getDbStatusIcon = (connections: number | undefined) => {
if (!connections) return 'mdi-database-off'
if (connections > 40) return 'mdi-alert-circle'
if (connections > 20) return 'mdi-alert'
return 'mdi-check-circle'
}
const getDbStatusColor = (connections: number | undefined) => {
if (!connections) return 'grey'
if (connections > 40) return 'error'
if (connections > 20) return 'warning'
return 'success'
}
const getDbStatusTextClass = (connections: number | undefined) => {
if (!connections) return 'text-grey'
if (connections > 40) return 'text-error'
if (connections > 20) return 'text-warning'
return 'text-success'
}
const getDbStatusText = (connections: number | undefined) => {
if (!connections) return 'No data'
if (connections > 40) return 'High load'
if (connections > 20) return 'Moderate load'
return 'Normal load'
}
// Lifecycle
onMounted(() => {
console.log('Dashboard mounted for user:', userEmail.value)
// Initialize health monitor data
healthMonitor.initialize()
// Load tile preferences
tileStore.loadPreferences()
})
</script>
<style scoped>
.v-main {
background-color: #f5f5f5;
}
.v-card {
transition: transform 0.2s ease-in-out;
}
.v-card:hover {
transform: translateY(-4px);
}
/* Drag & Drop Styles */
.drag-container {
min-height: 200px;
}
.drag-row {
display: flex;
flex-wrap: wrap;
margin: -12px;
}
.drag-col {
padding: 12px;
}
.ghost-tile {
opacity: 0.5;
background-color: #e0e0e0;
border-radius: 8px;
}
.chosen-tile {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
transform: scale(1.02);
z-index: 10;
}
</style>

View File

@@ -1,15 +0,0 @@
<template>
<div>
<!-- Redirect to dashboard using Nuxt's navigateTo -->
<p>Redirecting to dashboard...</p>
</div>
</template>
<script setup lang="ts">
import { navigateTo } from '#app'
// Redirect to dashboard on mount
onMounted(() => {
navigateTo('/dashboard')
})
</script>

View File

@@ -1,223 +0,0 @@
<template>
<v-app>
<v-main class="d-flex align-center justify-center" style="min-height: 100vh; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<v-card width="400" class="pa-6" elevation="12">
<v-card-title class="text-h4 font-weight-bold text-center mb-4">
<v-icon icon="mdi-rocket-launch" class="mr-2" size="40"></v-icon>
Mission Control
</v-card-title>
<v-card-subtitle class="text-center mb-6">
Service Finder Admin Dashboard
</v-card-subtitle>
<v-form @submit.prevent="handleLogin" ref="loginForm">
<v-text-field
v-model="email"
label="Email"
type="email"
prepend-icon="mdi-email"
:rules="emailRules"
required
class="mb-4"
></v-text-field>
<v-text-field
v-model="password"
label="Password"
:type="showPassword ? 'text' : 'password'"
prepend-icon="mdi-lock"
:append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
@click:append-inner="showPassword = !showPassword"
:rules="passwordRules"
required
class="mb-2"
></v-text-field>
<div class="d-flex justify-end mb-4">
<v-btn variant="text" size="small" @click="navigateTo('/forgot-password')">
Forgot Password?
</v-btn>
</div>
<v-btn
type="submit"
color="primary"
size="large"
block
:loading="isLoading"
class="mb-4"
>
<v-icon icon="mdi-login" class="mr-2"></v-icon>
Sign In
</v-btn>
<!-- Real API Login Only - No Dev Bypass -->
<v-alert
v-if="error"
type="error"
variant="tonal"
class="mb-4"
>
{{ error }}
</v-alert>
<v-divider class="my-4"></v-divider>
<div class="text-center">
<p class="text-caption text-disabled">
Demo Credentials
</p>
<v-chip-group class="justify-center">
<v-chip size="small" variant="outlined" @click="setDemoCredentials('superadmin')">
Superadmin
</v-chip>
<v-chip size="small" variant="outlined" @click="setDemoCredentials('admin')">
Admin
</v-chip>
<v-chip size="small" variant="outlined" @click="setDemoCredentials('tester')">
Tester
</v-chip>
</v-chip-group>
</div>
</v-form>
</v-card>
<!-- Footer -->
<v-footer absolute class="px-4" color="transparent">
<v-spacer></v-spacer>
<div class="text-caption text-white">
Service Finder Admin v1.0.0 Epic 10 - Mission Control
</div>
</v-footer>
</v-main>
</v-app>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useAuthStore } from '~/stores/auth'
import { navigateTo } from '#app'
// State
const email = ref('')
const password = ref('')
const showPassword = ref(false)
const isLoading = ref(false)
const error = ref('')
const loginForm = ref()
// Validation rules
const emailRules = [
(v: string) => !!v || 'Email is required',
(v: string) => /.+@.+\..+/.test(v) || 'Email must be valid'
]
const passwordRules = [
(v: string) => !!v || 'Password is required',
(v: string) => v.length >= 6 || 'Password must be at least 6 characters'
]
// Store
const authStore = useAuthStore()
// Demo credentials - Using real credentials from the task
const demoCredentials = {
superadmin: {
email: 'superadmin@profibot.hu',
password: 'Superadmin123!',
role: 'superadmin',
rank: 100,
scope_level: 'global'
},
admin: {
email: 'admin@profibot.hu',
password: 'Admin123!',
role: 'admin',
rank: 50,
scope_level: 'global'
},
tester: {
email: 'tester_pro@profibot.hu',
password: 'Tester123!',
role: 'tester',
rank: 30,
scope_level: 'global'
}
}
// Set demo credentials
function setDemoCredentials(role: keyof typeof demoCredentials) {
const creds = demoCredentials[role]
email.value = creds.email
password.value = creds.password
// Show role info
error.value = `Demo ${role} credentials loaded. Role: ${creds.role}, Rank: ${creds.rank}, Scope: ${creds.scope_level}`
}
// Handle login - REAL API AUTHENTICATION ONLY
async function handleLogin() {
// Validate form
const { valid } = await loginForm.value.validate()
if (!valid) return
isLoading.value = true
error.value = ''
try {
// Debug: Log the input values
console.log('Attempting login with:', email.value, password.value)
console.log('Email type:', typeof email.value, 'Password type:', typeof password.value)
// For demo purposes, simulate login with demo credentials
const role = Object.keys(demoCredentials).find(key =>
demoCredentials[key as keyof typeof demoCredentials].email === email.value
)
if (role) {
const creds = demoCredentials[role as keyof typeof demoCredentials]
// In development mode, use the auth store's login function which has the mock bypass
// This will trigger the dev mode bypass in auth.ts for admin@servicefinder.com
const success = await authStore.login(email.value, password.value)
if (!success) {
error.value = 'Invalid credentials. Please try again.'
} else {
// Redirect to dashboard on successful login
await navigateTo('/dashboard')
}
} else {
// Simulate API call for real credentials
const success = await authStore.login(email.value, password.value)
if (!success) {
error.value = 'Invalid credentials. Please try again.'
} else {
// Redirect to dashboard on successful login
await navigateTo('/dashboard')
}
}
} catch (err) {
console.error('Login error:', err)
error.value = err instanceof Error ? err.message : 'Login failed'
} finally {
isLoading.value = false
}
}
</script>
<style scoped>
.v-card {
border-radius: 16px;
}
.v-chip {
cursor: pointer;
}
.v-chip:hover {
transform: translateY(-2px);
transition: transform 0.2s ease;
}
</style>

View File

@@ -1,311 +0,0 @@
<template>
<div class="moderation-map-page">
<div class="page-header">
<h1>Geographical Service Map</h1>
<p class="subtitle">Visualize and moderate services within your geographical scope</p>
</div>
<div class="controls">
<div class="scope-selector">
<label for="scope">Change Scope:</label>
<select id="scope" v-model="selectedScopeId" @change="onScopeChange">
<option v-for="scope in availableScopes" :key="scope.id" :value="scope.id">
{{ scope.label }}
</option>
</select>
<button @click="refreshData" class="btn-refresh">Refresh Data</button>
</div>
<div class="stats">
<div class="stat-card">
<span class="stat-label">Total Services</span>
<span class="stat-value">{{ services.length }}</span>
</div>
<div class="stat-card">
<span class="stat-label">Pending</span>
<span class="stat-value pending">{{ pendingServices.length }}</span>
</div>
<div class="stat-card">
<span class="stat-label">Approved</span>
<span class="stat-value approved">{{ approvedServices.length }}</span>
</div>
<div class="stat-card">
<span class="stat-label">In Scope</span>
<span class="stat-value">{{ servicesInScope.length }}</span>
</div>
</div>
</div>
<div class="map-container">
<ServiceMap
:services="servicesInScope"
:scope-label="scopeLabel"
/>
</div>
<div class="service-list">
<h2>Services in Scope</h2>
<table class="service-table">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Address</th>
<th>Distance</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="service in servicesInScope" :key="service.id">
<td>{{ service.name }}</td>
<td>
<span :class="service.status" class="status-badge">
{{ service.status }}
</span>
</td>
<td>{{ service.address }}</td>
<td>{{ service.distance }} km</td>
<td>
<button
@click="approveService(service.id)"
:disabled="service.status === 'approved'"
class="btn-action"
>
{{ service.status === 'approved' ? 'Approved' : 'Approve' }}
</button>
<button @click="zoomToService(service)" class="btn-action secondary">
View on Map
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import ServiceMap from '~/components/map/ServiceMap.vue'
import { useServiceMap, type Service } from '~/composables/useServiceMap'
const {
services,
pendingServices,
approvedServices,
scopeLabel,
currentScope,
servicesInScope,
approveService: approveServiceComposable,
changeScope,
availableScopes
} = useServiceMap()
const selectedScopeId = ref(currentScope.value.id)
const onScopeChange = () => {
const scope = availableScopes.find(s => s.id === selectedScopeId.value)
if (scope) {
changeScope(scope)
}
}
const refreshData = () => {
// In a real app, this would fetch fresh data from API
console.log('Refreshing data...')
}
const zoomToService = (service: Service) => {
// This would zoom the map to the service location
console.log('Zooming to service:', service)
// In a real implementation, we would emit an event to the ServiceMap component
}
const approveService = (serviceId: number) => {
approveServiceComposable(serviceId)
}
</script>
<style scoped>
.moderation-map-page {
padding: 20px;
max-width: 1400px;
margin: 0 auto;
}
.page-header {
margin-bottom: 30px;
}
.page-header h1 {
font-size: 2.5rem;
color: #333;
margin-bottom: 8px;
}
.subtitle {
color: #666;
font-size: 1.1rem;
}
.controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 20px;
}
.scope-selector {
display: flex;
align-items: center;
gap: 10px;
}
.scope-selector label {
font-weight: bold;
}
.scope-selector select {
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #ccc;
background: white;
font-size: 1rem;
}
.btn-refresh {
background-color: #4a90e2;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
.btn-refresh:hover {
background-color: #3a7bc8;
}
.stats {
display: flex;
gap: 15px;
}
.stat-card {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px 20px;
min-width: 120px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.stat-label {
display: block;
font-size: 0.9rem;
color: #666;
margin-bottom: 5px;
}
.stat-value {
display: block;
font-size: 1.8rem;
font-weight: bold;
color: #333;
}
.stat-value.pending {
color: #3b82f6;
}
.stat-value.approved {
color: #28a745;
}
.map-container {
margin-bottom: 30px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.service-list {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.service-list h2 {
margin-top: 0;
margin-bottom: 20px;
color: #333;
}
.service-table {
width: 100%;
border-collapse: collapse;
}
.service-table thead {
background-color: #f8f9fa;
}
.service-table th {
padding: 12px 15px;
text-align: left;
font-weight: bold;
color: #495057;
border-bottom: 2px solid #dee2e6;
}
.service-table td {
padding: 12px 15px;
border-bottom: 1px solid #dee2e6;
}
.status-badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 0.85rem;
font-weight: bold;
text-transform: uppercase;
}
.status-badge.pending {
background-color: #dbeafe;
color: #1d4ed8;
}
.status-badge.approved {
background-color: #d4edda;
color: #155724;
}
.btn-action {
background-color: #28a745;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
margin-right: 8px;
}
.btn-action:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
.btn-action.secondary {
background-color: #6c757d;
}
.btn-action:hover:not(:disabled) {
opacity: 0.9;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<circle cx="16" cy="16" r="14" fill="#28a745" stroke="#fff" stroke-width="2"/>
<path d="M12 16l4 4 8-8" stroke="#fff" stroke-width="3" fill="none" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 266 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<circle cx="16" cy="16" r="14" fill="#ffc107" stroke="#fff" stroke-width="2"/>
<path d="M12 16h8" stroke="#fff" stroke-width="3" fill="none" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 260 B

View File

@@ -1,239 +0,0 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { jwtDecode } from 'jwt-decode'
export interface JwtPayload {
sub: string
role: string
rank: number
scope_level: string
region_code?: string
scope_id?: number
exp: number
iat: number
}
export interface User {
id: string
email: string
role: string
rank: number
scope_level: string
region_code?: string
scope_id?: number
permissions: string[]
}
export const useAuthStore = defineStore('auth', () => {
// State
const token = ref<string | null>(null)
const user = ref<User | null>(null)
const isAuthenticated = computed(() => !!token.value && !isTokenExpired())
const isLoading = ref(false)
const error = ref<string | null>(null)
// Initialize token from localStorage only on client side
if (typeof window !== 'undefined') {
token.value = localStorage.getItem('admin_token')
}
// Getters
const getUserRole = computed(() => user.value?.role || '')
const getUserRank = computed(() => user.value?.rank || 0)
const getScopeLevel = computed(() => user.value?.scope_level || '')
const getRegionCode = computed(() => user.value?.region_code || '')
const getScopeId = computed(() => user.value?.scope_id || 0)
const getPermissions = computed(() => user.value?.permissions || [])
// Check if token is expired
function isTokenExpired(): boolean {
if (!token.value) return true
try {
const decoded = jwtDecode<JwtPayload>(token.value)
return Date.now() >= decoded.exp * 1000
} catch {
return true
}
}
// Parse token and set user
function parseToken(): void {
if (!token.value) {
user.value = null
return
}
try {
const decoded = jwtDecode<JwtPayload>(token.value)
// Map JWT claims to user object
user.value = {
id: decoded.sub,
email: decoded.sub, // Assuming sub is email
role: decoded.role,
rank: decoded.rank,
scope_level: decoded.scope_level,
region_code: decoded.region_code,
scope_id: decoded.scope_id,
permissions: generatePermissions(decoded.role, decoded.rank)
}
error.value = null
} catch (err) {
console.error('Failed to parse token:', err)
error.value = 'Invalid token format'
user.value = null
// Clear invalid token from storage
token.value = null
if (typeof window !== 'undefined') {
localStorage.removeItem('admin_token')
}
}
}
// Generate permissions based on role and rank
function generatePermissions(role: string, rank: number): string[] {
const permissions: string[] = []
// Base permissions based on role
switch (role) {
case 'superadmin':
permissions.push('*')
break
case 'admin':
permissions.push('view:dashboard', 'manage:users', 'manage:services', 'view:finance')
if (rank >= 5) permissions.push('manage:settings')
break
case 'moderator':
permissions.push('view:dashboard', 'moderate:services', 'view:users')
break
case 'salesperson':
permissions.push('view:dashboard', 'view:sales', 'manage:leads')
break
}
// Add geographical scope permissions
permissions.push(`scope:${role}`)
return permissions
}
// Check if user has permission
function hasPermission(permission: string): boolean {
if (!user.value) return false
if (user.value.permissions.includes('*')) return true
return user.value.permissions.includes(permission)
}
// Check if user has required role rank
function hasRank(minRank: number): boolean {
return user.value?.rank >= minRank
}
// Check if user can access scope
function canAccessScope(requestedScopeId: number, requestedRegionCode?: string): boolean {
if (!user.value) return false
// Superadmin can access everything
if (user.value.role === 'superadmin') return true
// Check scope_id match
if (user.value.scope_id && user.value.scope_id === requestedScopeId) return true
// Check region_code match
if (user.value.region_code && requestedRegionCode) {
return user.value.region_code === requestedRegionCode
}
return false
}
// Login action - REAL API AUTHENTICATION ONLY
async function login(email: string, password: string): Promise<boolean> {
isLoading.value = true
error.value = null
try {
// Debug: Log what we're sending
console.log('Auth store: Attempting login for', email)
console.log('Auth store: Password length', password.length)
// Prepare URL-encoded form data for OAuth2 password grant (as per FastAPI auth endpoint)
// FastAPI's OAuth2PasswordRequestForm expects application/x-www-form-urlencoded
// Use explicit string encoding to guarantee FastAPI accepts it (Nuxt's $fetch messes up URLSearchParams)
const bodyString = `username=${encodeURIComponent(email)}&password=${encodeURIComponent(password)}`;
console.log('Auth store: Body string created', bodyString)
// Call real backend login endpoint using $fetch (Nuxt's fetch)
// $fetch automatically throws on non-2xx responses, so we just need to catch
const data = await $fetch('/api/v1/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: bodyString
})
console.log('Auth login API response:', data)
// Extract token
const accessToken = data.access_token
if (!accessToken) {
throw new Error('No access token in response')
}
// Store token safely (SSR-safe)
if (typeof window !== 'undefined') {
localStorage.setItem('admin_token', accessToken)
}
token.value = accessToken
parseToken()
return true
} catch (err) {
console.error('Auth store: Login catch block error:', err)
error.value = err instanceof Error ? err.message : 'Login failed'
return false
} finally {
isLoading.value = false
}
}
// Logout action
function logout(): void {
token.value = null
user.value = null
if (typeof window !== 'undefined') {
localStorage.removeItem('admin_token')
}
}
// Initialize store
if (token.value) {
parseToken()
}
return {
// State
token,
user,
isAuthenticated,
isLoading,
error,
// Getters
getUserRole,
getUserRank,
getScopeLevel,
getRegionCode,
getScopeId,
getPermissions,
// Actions
login,
logout,
hasPermission,
hasRank,
canAccessScope,
parseToken
}
})

View File

@@ -1,204 +0,0 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useAuthStore } from './auth'
import { useRBAC, type TilePermission } from '~/composables/useRBAC'
export interface UserTilePreference {
tileId: string
visible: boolean
position: number
size: 'small' | 'medium' | 'large'
}
export const useTileStore = defineStore('tiles', () => {
const authStore = useAuthStore()
const rbac = useRBAC()
// State
const userPreferences = ref<Record<string, UserTilePreference>>({})
const isLoading = ref(false)
// Initialize from localStorage
function loadPreferences() {
if (typeof window === 'undefined') return
const userId = authStore.user?.id
if (!userId) return
const stored = localStorage.getItem(`tile_preferences_${userId}`)
if (stored) {
try {
userPreferences.value = JSON.parse(stored)
} catch (err) {
console.error('Failed to parse tile preferences:', err)
userPreferences.value = {}
}
}
}
// Save to localStorage
function savePreferences() {
if (typeof window === 'undefined') return
const userId = authStore.user?.id
if (!userId) return
localStorage.setItem(`tile_preferences_${userId}`, JSON.stringify(userPreferences.value))
}
// Get default layout (sorted by tile ID for consistency)
const defaultLayout = computed(() => {
const filtered = rbac.getFilteredTiles()
return filtered.map((tile, index) => ({
tileId: tile.id,
visible: true,
position: index,
size: 'medium' as const
}))
})
// Check if layout has been modified from default
const isLayoutModified = computed(() => {
const currentPrefs = Object.values(userPreferences.value)
const defaultPrefs = defaultLayout.value
if (currentPrefs.length !== defaultPrefs.length) return true
// Check if any preference differs from default
for (const defaultPref of defaultPrefs) {
const currentPref = userPreferences.value[defaultPref.tileId]
if (!currentPref) return true
if (currentPref.visible !== defaultPref.visible ||
currentPref.position !== defaultPref.position ||
currentPref.size !== defaultPref.size) {
return true
}
}
return false
})
// Get user's accessible tiles with preferences
const accessibleTiles = computed(() => {
const filtered = rbac.getFilteredTiles()
return filtered.map(tile => {
const pref = userPreferences.value[tile.id] || {
tileId: tile.id,
visible: true,
position: 0,
size: 'medium' as const
}
return {
...tile,
preference: pref
}
}).sort((a, b) => a.preference.position - b.preference.position)
})
// Get visible tiles only
const visibleTiles = computed(() => {
return accessibleTiles.value.filter(tile => tile.preference.visible)
})
// Update tile preference
function updateTilePreference(tileId: string, updates: Partial<UserTilePreference>) {
const current = userPreferences.value[tileId] || {
tileId,
visible: true,
position: Object.keys(userPreferences.value).length,
size: 'medium'
}
userPreferences.value[tileId] = {
...current,
...updates
}
savePreferences()
}
// Toggle tile visibility
function toggleTileVisibility(tileId: string) {
const current = userPreferences.value[tileId]
updateTilePreference(tileId, {
visible: !(current?.visible ?? true)
})
}
// Update tile positions (for drag and drop)
function updateTilePositions(tileIds: string[]) {
tileIds.forEach((tileId, index) => {
updateTilePreference(tileId, { position: index })
})
}
// Reset to default preferences
function resetPreferences() {
const userId = authStore.user?.id
if (userId) {
localStorage.removeItem(`tile_preferences_${userId}`)
}
userPreferences.value = {}
// Reinitialize with default positions
const tiles = rbac.getFilteredTiles()
tiles.forEach((tile, index) => {
userPreferences.value[tile.id] = {
tileId: tile.id,
visible: true,
position: index,
size: 'medium'
}
})
savePreferences()
}
// Get tile size class for grid
function getTileSizeClass(size: 'small' | 'medium' | 'large'): string {
switch (size) {
case 'small': return 'cols-12 sm-6 md-4 lg-3'
case 'medium': return 'cols-12 sm-6 md-6 lg-4'
case 'large': return 'cols-12 md-12 lg-8'
default: return 'cols-12 sm-6 md-4 lg-3'
}
}
// Initialize when auth changes
authStore.$subscribe(() => {
if (authStore.isAuthenticated) {
loadPreferences()
} else {
userPreferences.value = {}
}
})
// Initial load
if (authStore.isAuthenticated) {
loadPreferences()
}
return {
// State
userPreferences,
isLoading,
// Getters
accessibleTiles,
visibleTiles,
defaultLayout,
isLayoutModified,
// Actions
updateTilePreference,
toggleTileVisibility,
updateTilePositions,
resetPreferences,
getTileSizeClass,
loadPreferences,
savePreferences
}
})

View File

@@ -1,4 +0,0 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

View File

@@ -0,0 +1,14 @@
# docker-compose.yml (Frontend kiegészítés a meglévő mellé)
services:
sf_frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: sf_frontend
networks:
- sf_net
ports:
- "8503:80" # A fejlesztői gépeden ezen éred el
depends_on:
- sf_api
restart: unless-stopped

View File

@@ -1,173 +0,0 @@
# Smart Vehicle Registration Component - Implementation Report
## 📋 Áttekintés
A SmartVehicleRegistration komponens sikeresen implementálva lett a Service Finder frontend rendszerében. A komponens egy modern, 3 lépéses varázsló, amely lehetővé teszi a felhasználók számára, hogy könnyedén regisztrálják járműveiket a rendszerbe.
## 🎯 Főbb Jellemzők
### 1. **3 Lépéses Varázsló**
- **1. lépés**: Jármű osztályozás (személygépkocsi, motorkerékpár, tehergépkocsi, busz, különleges jármű)
- **2. lépés**: Katalógus automatikus kiegészítés (márka → modell → generáció → motor)
- **3. lépés**: Egyedi adatok (rendszám, VIN, futásteljesítmény, szín)
### 2. **Progresszív Indikátor**
- Dinamikus progressz mutató a kitöltött mezők alapján
- Automatikus számítás minden változásnál
- Lépésjelzők vizuális kiemeléssel
### 3. **Duális UI Skin**
- **B2B (Fleet) mód**: Kék színséma, professzionális megjelenés
- **B2C (Personal) mód**: Zöld színséma, barátságos megjelenés
- Automatikus váltás az `appModeStore` alapján
### 4. **API Integráció**
- Katalógus API végpontok használata:
- `/catalog/makes` - Márkák listázása
- `/catalog/models` - Modellek listázása
- `/catalog/generations` - Generációk listázása
- `/catalog/engines` - Motorváltozatok listázása
## 🔧 Módosított Fájlok
### 1. **`frontend/src/stores/garageStore.js`**
- Frissített `addVehicle` action
- "Thick Asset" payload küldése a backendnek
- Váltás `/assets/vehicles``/api/v1/assets` végpontra
- Hibakezelés: 409 (duplikátum), 429 (rate limit), 400, 403
### 2. **`frontend/src/components/actions/SmartVehicleRegistration.vue`**
- Teljesen új komponens létrehozva
- Vue 3 Composition API használata
- Pinia store-ok integrációja
- Reszponzív Tailwind CSS design
### 3. **`frontend/src/components/actions/QuickActionsFAB.vue`**
- Integráció a SmartVehicleRegistration komponenssel
- Régi AddVehicleModal cseréje
- Megfelelő importok és változónevek frissítése
## 📊 Technikai Adatok
### Payload Struktúra (Thick Asset)
```javascript
{
// Alap azonosítás
vin: string | null,
licensePlate: string,
catalogId: number | null,
organizationId: number,
// Thick Asset mezők
brand: string,
model: string,
vehicleClass: string,
fuelType: string | null,
year: number | null,
currentMileage: number,
color: string,
// További metaadatok
status: 'draft',
generation: string,
engine: string
}
```
### Validációs Szabályok
1. **1. lépés**: Kötelező jármű osztály kiválasztása
2. **2. lépés**: Minden katalógus mező kitöltése (márka, modell, generáció, motor)
3. **3. lépés**: Kötelező rendszám megadása
## 🚀 Használati Útmutató
### 1. Komponens Megnyitása
```javascript
// QuickActionsFAB komponensben
const showSmartRegistration = ref(false)
// Megnyitás
function openSmartRegistration() {
showSmartRegistration.value = true
}
```
### 2. Események Kezelése
```javascript
// Sikeres regisztráció
@success="handleRegistrationSuccess"
// Modal bezárása
@close="handleModalClose"
```
### 3. UI Mód Váltás
```javascript
// Automatikus az appModeStore alapján
const appModeStore = useAppModeStore()
const isFleetMode = computed(() => appModeStore.mode === 'fleet')
```
## 🧪 Tesztelési Forgatókönyvek
### 1. **Alap regisztráció**
- Jármű osztály kiválasztása
- Katalógusból választás
- Kötelező adatok megadása
- Sikeres beküldés
### 2. **Hibakezelés**
- Duplikált VIN szám
- API rate limit túllépés
- Érvénytelen adatok
- Hálózati hiba
### 3. **UI Teszt**
- B2B/B2C mód váltás
- Reszponzivitás (mobil/asztali)
- Progressz indikátor működése
- Lépés navigáció
## 🔍 Teljesítmény Optimalizálások
1. **Lazy Loading**: Katalógus adatok csak szükség esetén
2. **Debounced API Calls**: Gyakori változásoknál optimalizált hívások
3. **Memoizált Computed Properties**: Ismétlődő számítások cache-elése
4. **Egyirányú Adatáramlás**: Predictable state management
## 📈 Metrikák és Nyomon Követés
### 1. **Felhasználói Metrikák**
- Átlagos kitöltési idő
- Lépésenkénti dropout rate
- Sikeres regisztrációk aránya
### 2. **Technikai Metrikák**
- API válaszidők
- Komponens betöltési idő
- Memória használat
## 🛠️ Jövőbeli Fejlesztések
1. **OCR Integráció**: Rendszám automatikus felismerése
2. **Képfeltöltés**: Jármű fotók hozzáadása
3. **Offline Mód**: Hálózat nélküli működés
4. **Többnyelvűség**: Teljes lokalizáció támogatása
## ✅ Ellenőrzési Lista
- [x] GarageStore.js frissítve Thick Asset payload-dal
- [x] SmartVehicleRegistration.vue komponens létrehozva
- [x] 3 lépéses varázsló implementálva
- [x] Progressz indikátor működik
- [x] Duális UI skin B2B/B2C módokhoz
- [x] QuickActionsFAB integráció
- [x] API végpontok tesztelve
- [x] Hibakezelés implementálva
- [x] Dokumentáció elkészítve
## 🎉 Következtetés
A SmartVehicleRegistration komponens sikeresen integrálva lett a Service Finder rendszerébe. A modern, felhasználóbarát felület jelentősen javítja a járműregisztráció élményét, miközben a "Thick Asset" architektúra biztosítja az adatok konzisztenciáját és a backend szinkronizációt.
A komponens készen áll a termelési környezetben való használatra, teljes mértékben kompatibilis a meglévő garageStore.js és appModeStore.js rendszerekkel.

13
frontend/index.html Executable file → Normal file
View File

@@ -4,19 +4,14 @@
<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>Service Finder | Premium Vehicle Management</title>
<!-- Google Fonts: Inter for premium typography -->
<title>Service Finder | Modern Vehicle Service Platform</title>
<meta name="description" content="Find trusted service providers for your vehicle with transparent pricing and reviews.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
* {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
}
</style>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

40
frontend/nginx.conf Executable file → Normal file
View File

@@ -1,29 +1,51 @@
worker_processes 4; # Itt korlátozzuk a 108 helyett!
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html; # Itt keresi a fájlokat
root /usr/share/nginx/html;
index index.html;
# Serve static files
location / {
try_files $uri $uri/ /index.html; # SPA (Vue/React) támogatás
try_files $uri $uri/ /index.html;
}
# API kérések továbbküldése a backendnek (opcionális, ha a frontend kéri)
# Proxy API requests to backend
location /api/ {
proxy_pass http://service_finder_api:8000/;
proxy_pass http://sf_api:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support if needed
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}

File diff suppressed because it is too large Load Diff

41
frontend/package.json Executable file → Normal file
View File

@@ -1,33 +1,32 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"name": "service-finder-frontend",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test:e2e": "node tests/automated_flow_test.js",
"test:ui": "playwright test",
"test:ui:headed": "playwright test --headed",
"test:ui:debug": "playwright test --debug",
"playwright:install": "playwright install"
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"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"
"axios": "^1.6.0",
"pinia": "^2.1.7",
"vue": "^3.4.0",
"vue-i18n": "^9.11.0",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"@playwright/test": "^1.50.0",
"autoprefixer": "^10.4.23",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"vite": "^7.2.4"
"@playwright/test": "^1.60.0",
"@vitejs/plugin-vue": "^5.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.0",
"autoprefixer": "^10.4.16",
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.19.0",
"postcss": "^8.4.32",
"tailwindcss": "^3.4.0",
"typescript": "~5.3.0",
"vite": "^5.0.0",
"vue-tsc": "^1.8.0"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,78 +0,0 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests/e2e',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://localhost:8503',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run dev',
// url: 'http://127.0.0.1:5173',
// reuseExistingServer: !process.env.CI,
// },
});

View File

@@ -0,0 +1,11 @@
import { defineConfig } from '@playwright/test';
export default defineConfig({
timeout: 60000,
use: {
connectOptions: {
// faktor01 Tailscale IP - Browserless headless Chrome service
wsEndpoint: 'ws://100.93.27.29:3030',
},
screenshot: 'on',
},
});

2
frontend/postcss.config.js Executable file → Normal file
View File

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

BIN
frontend/public/SF_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

BIN
frontend/public/garage1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 MiB

BIN
frontend/public/garage2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 MiB

BIN
frontend/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

BIN
frontend/public/sf_card.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

View File

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

Before

Width:  |  Height:  |  Size: 1.5 KiB

202
frontend/src/App.vue Executable file → Normal file
View File

@@ -1,197 +1,15 @@
<script setup>
import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/authStore'
import { useThemeStore } from '@/stores/themeStore'
import DailyQuizModal from '@/components/DailyQuizModal.vue'
import QuickActionsFAB from '@/components/actions/QuickActionsFAB.vue'
import { ref } from 'vue'
const router = useRouter()
const route = useRoute()
const authStore = useAuthStore()
const themeStore = useThemeStore()
const handleLogout = () => {
authStore.logout()
}
const toggleTheme = () => {
themeStore.toggleTheme()
}
// Legal modal state
const showLegalModal = ref(false)
const legalModalTitle = ref('')
const legalModalContent = ref('')
const openLegalModal = (type) => {
if (type === 'aszf') {
legalModalTitle.value = 'Általános Szerződési Feltételek (ÁSZF)'
legalModalContent.value = 'A jogi dokumentáció feltöltés alatt... A Service Finder szolgáltatás használatával Ön elfogadja az Általános Szerződési Feltételeket, amelyek meghatározzák a szolgáltatás használatának feltételeit, a felelősség korlátozását és a felhasználói jogokat.'
} else if (type === 'adatkezeles') {
legalModalTitle.value = 'Adatkezelési Tájékoztató (GDPR)'
legalModalContent.value = 'A jogi dokumentáció feltöltés alatt... A Service Finder tiszteletben tartja az Ön adatvédelmét. E tájékoztató részletezi, hogyan gyűjtjük, tároljuk és kezeljük személyes adatait az Európai Unió Általános Adatvédelmi Rendelete (GDPR) és a vonatkozó magyar jogszabályok értelmében.'
} else if (type === 'cookies') {
legalModalTitle.value = 'Sütik (Cookies) Használata'
legalModalContent.value = 'A jogi dokumentáció feltöltés alatt... Weboldalunk sütiket (cookies) használ a felhasználói élmény javítása, a funkciók működésének biztosítása és a forgalom elemzése érdekében. A sütik használatával kapcsolatos részletes információkat itt találja.'
}
showLegalModal.value = true
}
const closeLegalModal = () => {
showLegalModal.value = false
}
</script>
<template>
<div :class="['min-h-screen flex flex-col font-sans transition-all duration-500', themeStore.themeClasses.background, themeStore.themeClasses.text]">
<!-- Premium Navigation -->
<nav class="bg-gradient-to-r from-slate-800 to-slate-900 text-white p-4 shadow-xl flex justify-between items-center z-50 border-b border-slate-700/50 backdrop-blur-md">
<div class="flex items-center gap-3 cursor-pointer group" @click="router.push('/')">
<div class="p-2 rounded-xl bg-gradient-to-br from-blue-500 to-cyan-500 group-hover:from-blue-600 group-hover:to-cyan-600 transition-all duration-200">
<span class="text-2xl">🚗</span>
</div>
<div class="flex flex-col">
<span class="font-bold text-xl tracking-tight">Service Finder</span>
<span class="text-xs text-slate-300">Premium Vehicle Management</span>
</div>
</div>
<div class="space-x-6 hidden md:flex items-center">
<template v-if="authStore.isLoggedIn">
<router-link to="/" class="nav-link text-slate-200 hover:text-white font-medium transition-colors duration-200 hover:scale-105">Dashboard</router-link>
<router-link to="/expenses" class="nav-link text-slate-200 hover:text-white font-medium transition-colors duration-200 hover:scale-105">Költségek</router-link>
<router-link v-if="authStore.isAdmin" to="/admin" class="text-amber-300 font-bold hover:text-amber-200 transition-colors duration-200 hover:scale-105"> Admin</router-link>
<!-- User Role Badge -->
<div v-if="authStore.isTester" class="px-3 py-1.5 bg-gradient-to-r from-purple-600 to-pink-600 rounded-lg text-xs font-bold text-white border border-purple-500/50 shadow-md animate-pulse">
🧪 {{ authStore.displayName }}
</div>
<div v-else-if="authStore.isAdmin" class="px-3 py-1.5 bg-gradient-to-r from-amber-600 to-orange-600 rounded-lg text-xs font-bold text-white border border-amber-500/50 shadow-md">
{{ authStore.displayName }}
</div>
<button @click="toggleTheme" :class="['px-4 py-2.5 rounded-xl text-sm transition-all duration-200 active:scale-95 border shadow-md hover:shadow-lg', themeStore.isLuxury ? 'bg-gradient-to-r from-amber-700 to-amber-800 border-amber-600/50 text-white hover:from-amber-800 hover:to-amber-900' : 'bg-gradient-to-r from-orange-700 to-orange-800 border-orange-600/50 text-white hover:from-orange-800 hover:to-orange-900']">
{{ themeStore.isLuxury ? '🏛️ Luxury' : '🔧 Workshop' }}
</button>
<button @click="handleLogout" class="bg-gradient-to-r from-slate-700 to-slate-800 px-5 py-2.5 rounded-xl text-sm hover:from-slate-800 hover:to-slate-900 transition-all duration-200 active:scale-95 border border-slate-600/50 shadow-md hover:shadow-lg">
Kijelentkezés
</button>
</template>
<template v-else>
<router-link to="/login" class="text-slate-200 hover:text-white font-medium transition-colors duration-200">Bejelentkezés</router-link>
<router-link to="/register" class="bg-gradient-to-r from-blue-600 to-cyan-600 px-5 py-2.5 rounded-xl text-sm hover:from-blue-700 hover:to-cyan-700 transition-all duration-200 active:scale-95 shadow-md hover:shadow-lg">
Regisztráció
</router-link>
</template>
</div>
<!-- Mobile menu button -->
<button class="md:hidden p-2 rounded-lg bg-slate-700/50 hover:bg-slate-700 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</nav>
<!-- Main Content -->
<main class="flex-1 p-4 md:p-6 max-w-7xl mx-auto w-full">
<router-view />
</main>
<!-- Daily Quiz Modal -->
<DailyQuizModal v-if="authStore.isLoggedIn && route.path !== '/login' && route.path !== '/register' && route.path !== '/debug'" />
<!-- Quick Actions FAB -->
<QuickActionsFAB v-if="authStore.isLoggedIn && route.path !== '/login' && route.path !== '/register' && route.path !== '/debug'" />
<!-- Legal Modal -->
<div v-if="showLegalModal" class="fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm transition-all duration-300">
<div class="bg-gradient-to-br from-slate-800 to-slate-900 rounded-2xl shadow-2xl border border-slate-700/50 w-full max-w-2xl mx-4 overflow-hidden transform transition-all duration-300 scale-100">
<div class="p-6 border-b border-slate-700/50">
<div class="flex justify-between items-center">
<h3 class="text-xl font-bold text-white">{{ legalModalTitle }}</h3>
<button @click="closeLegalModal" class="text-slate-400 hover:text-white transition-colors duration-200 p-2 rounded-lg hover:bg-slate-700/50">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<div class="p-6 max-h-[60vh] overflow-y-auto">
<div class="prose prose-invert max-w-none">
<p class="text-slate-300 mb-4">{{ legalModalContent }}</p>
<div class="bg-slate-800/50 rounded-xl p-4 border border-slate-700/50 mt-6">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-full bg-gradient-to-r from-blue-500 to-cyan-500 flex items-center justify-center">
<span class="text-white font-bold"></span>
</div>
<div>
<p class="text-sm text-slate-400">Ez egy helykitöltő szöveg. A végleges jogi dokumentáció a termék bevezetésével együtt kerül feltöltésre.</p>
</div>
</div>
</div>
</div>
</div>
<div class="p-6 border-t border-slate-700/50 bg-slate-900/50">
<div class="flex justify-end">
<button @click="closeLegalModal" class="px-6 py-3 bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-700 hover:to-cyan-700 text-white font-medium rounded-xl transition-all duration-200 active:scale-95 shadow-md hover:shadow-lg">
Bezárás
</button>
</div>
</div>
</div>
</div>
<!-- Premium Footer -->
<footer class="mt-auto bg-gradient-to-r from-slate-800 to-slate-900 text-slate-300 p-6 border-t border-slate-700/50">
<div class="max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-center">
<div class="mb-4 md:mb-0">
<div class="flex items-center gap-2 mb-2">
<span class="text-2xl">🚗</span>
<span class="font-bold text-white">Service Finder</span>
</div>
<p class="text-sm text-slate-400">Premium vehicle management for individuals and businesses</p>
</div>
<div class="flex gap-6">
<button @click="openLegalModal('aszf')" class="text-slate-400 hover:text-white transition-colors duration-200 text-sm hover:underline">ÁSZF</button>
<button @click="openLegalModal('adatkezeles')" class="text-slate-400 hover:text-white transition-colors duration-200 text-sm hover:underline">Adatkezelési Tájékoztató</button>
<button @click="openLegalModal('cookies')" class="text-slate-400 hover:text-white transition-colors duration-200 text-sm hover:underline">Sütik</button>
<a href="#" class="text-slate-400 hover:text-white transition-colors duration-200 text-sm">Kapcsolat</a>
</div>
</div>
<div class="max-w-7xl mx-auto mt-6 pt-6 border-t border-slate-700/50 text-center text-xs text-slate-500">
© 2026 Service Finder. Minden jog fenntartva. Gépjármű-rajongók számára készült precízióval.
</div>
</footer>
</div>
<router-view />
</template>
<style scoped>
.nav-link {
position: relative;
padding: 0.5rem 0;
}
<script setup lang="ts">
import { onMounted } from 'vue'
import { useAuthStore } from './stores/auth'
.nav-link::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 2px;
background: linear-gradient(to right, #3b82f6, #06b6d4);
transition: width 0.3s ease;
}
const authStore = useAuthStore()
.nav-link:hover::after {
width: 100%;
}
/* Smooth transitions */
* {
transition-property: color, background-color, border-color, transform, box-shadow;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
</style>
onMounted(() => {
// Restore session from localStorage token on app start
authStore.init()
})
</script>

37
frontend/src/api/axios.ts Normal file
View File

@@ -0,0 +1,37 @@
import axios from 'axios'
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
})
// Request interceptor: attach JWT token from localStorage
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('access_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// Response interceptor: handle 401 Unauthorized (expired/invalid token)
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Token expired or invalid — clear it
localStorage.removeItem('access_token')
// Optionally redirect to login could be added here later
}
return Promise.reject(error)
}
)
export default api

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="623"
height="469"
viewBox="0 0 1600 1204"
version="1.1"
id="svg440"
sodipodi:docname="SF_csak_logo_ (1).svg"
inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs440" />
<sodipodi:namedview
id="namedview440"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.7206823"
inkscape:cx="390.83333"
inkscape:cy="234.5"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="1912"
inkscape:window-y="646"
inkscape:window-maximized="1"
inkscape:current-layer="svg440" />
<path
fill="#418890"
d="m 1200,296 16,8 20,11 22,14 17,12 16,13 12,11 21,21 9,11 12,15 13,19 9,14 10,18 13,27 6,16 11,30 6,25 5,31 2,30 v 36 l -1,21 -4,29 -5,24 -8,26 -8,21 -13,31 -10,18 -20,30 -3,6 1,5 9,10 13,16 8,5 3,1 2,-4 5,-1 h 11 l 13,5 13,10 11,12 11,11 7,8 25,25 7,8 7,7 7,8 18,18 8,7 9,9 v 2 h 2 l 7,8 9,9 7,8 11,11 7,8 7,10 8,17 2,10 v 15 l -3,12 -4,9 -6,10 -9,10 -10,9 -16,8 -9,2 h -23 l -13,-4 -14,-7 -13,-11 -11,-11 -7,-8 -11,-12 -9,-10 -7,-7 -7,-8 -13,-14 -7,-8 -14,-15 -16,-17 -7,-7 -7,-8 -15,-15 v -2 l -4,-2 -8,-8 -10,-13 -4,-10 -1,-23 -5,-8 -9,-10 -7,-8 -7,-6 -6,1 -15,9 -18,12 -19,11 -26,13 -26,10 -29,8 -21,4 -21,2 h -49 l -26,-4 -12,-1 -18,14 -16,12 -17,11 -25,14 -23,11 -26,10 -28,9 -50,11 -4,2 -13,40 -13,37 -10,27 -2,6 h -2 l -13,-35 -24,-71 -3,-6 v -2 h -11 l -26,-4 -19,-5 -38,-13 -25,-11 -20,-10 -3,-3 16,-12 16,-13 11,-9 13,-11 5,-4 6,2 16,6 26,8 24,5 22,3 12,1 h 24 l 23,-2 29,-5 21,-5 36,-12 20,-8 20,-10 5,-3 -1,-3 -22,-11 -18,-11 -12,-9 -11,-9 -24,-22 -7,-8 -9,-10 -2,-5 9,-9 8,-7 9,-11 12,-17 4,-7 7,6 13,13 11,9 13,11 20,14 19,11 16,8 25,10 24,7 19,4 15,2 h 40 l 26,-3 22,-5 24,-8 21,-9 19,-10 19,-12 17,-12 14,-12 9,-9 h 2 l 2,-4 8,-8 15,-20 12,-19 13,-24 11,-27 10,-34 6,-31 2,-16 v -52 l -4,-27 -6,-24 -10,-30 -9,-21 -11,-20 -13,-21 -13,-17 -12,-14 -17,-17 -11,-9 -13,-11 -18,-13 -22,-13 -25,-12 -1,-3 10,-18 13,-21 z"
id="path2"
style="display:inline" />
<path
fill="#13597b"
d="m 658,427 19,1 26,2 4,2 20,4 43,13 2,1 -1,4 -12,10 -14,11 -11,10 -8,7 -10,9 -16,15 -2,2 -17,-1 h -34 l -15,3 -13,5 -13,7 -9,8 -6,10 -3,9 v 11 l 4,12 9,10 12,7 20,8 32,8 43,10 25,7 18,6 20,9 14,8 13,10 9,8 9,11 9,13 8,18 4,15 2,10 1,13 v 13 l -2,18 -4,18 -6,16 -6,11 -10,13 -12,13 -14,11 -17,10 -21,9 -25,9 -25,5 -18,2 -20,1 h -23 l -24,-2 -38,-6 -48,-16 -23,-10 -17,-10 -24,-16 1,-4 41,-82 8,6 17,12 18,10 16,8 21,8 27,7 24,4 h 35 l 19,-3 16,-5 10,-5 8,-7 6,-12 2,-8 v -8 l -3,-12 -6,-10 -9,-8 -16,-8 -26,-8 -43,-10 -42,-12 -22,-8 -25,-12 -15,-10 -11,-9 -10,-10 -9,-14 -7,-15 -5,-16 -2,-10 -1,-13 v -12 l 3,-22 6,-20 7,-14 12,-17 9,-11 8,-8 17,-13 21,-12 24,-10 22,-6 23,-4 z"
id="path3"
style="display:inline" />
<path
fill="#418890"
d="m 775,90 2,1 12,36 14,41 12,35 23,4 31,5 24,7 36,12 23,10 32,16 20,12 15,10 -5,5 -16,10 -23,15 -18,11 -6,-1 -22,-12 -19,-8 -27,-9 -37,-10 -21,-4 -13,-2 -14,-1 h -47 l -27,3 -30,6 -31,9 -25,9 -24,11 -19,10 -19,12 -19,14 -13,11 -12,11 -28,28 -11,14 -15,20 -11,17 -14,24 -13,25 -11,29 -9,29 -8,36 -3,25 -1,14 v 55 l 3,27 6,31 4,19 v 4 l -16,12 -35,23 -24,15 -42,28 -46,30 -5,2 -6,-5 1,-7 6,-10 9,-11 15,-16 22,-22 8,-7 24,-24 8,-7 12,-12 -1,-9 -4,-21 -3,-27 -5,-5 -21,-11 -27,-11 -28,-11 -21,-9 -5,-2 v -2 l 17,-8 64,-25 22,-9 3,-5 7,-39 10,-40 15,-43 9,-20 13,-25 14,-24 13,-21 12,-16 7,-8 -1,-7 -11,-24 -17,-39 -5,-10 -3,-4 1,-2 11,5 39,16 27,13 3,3 4,-2 14,-12 20,-15 29,-19 22,-13 23,-12 15,-6 27,-10 31,-9 26,-6 34,-6 4,-3 12,-36 13,-37 z"
id="path4"
style="display:inline" />
<path
fill="#408790"
d="m 1241,171 1,2 -5,9 -12,19 -12,21 -8,13 -11,19 -15,24 -12,20 -31,51 -9,15 -11,18 -8,14 -16,26 -9,15 -17,28 -11,19 -10,17 -13,21 -16,27 -8,13 -11,19 -9,15 -17,29 -10,16 -10,19 -15,24 -11,18 -13,23 -9,15 -4,8 -3,1 -1,-4 -2,-27 -4,-29 -6,-25 -9,-24 -9,-22 -7,-12 -9,-12 -4,-5 1,-4 8,-10 7,1 6,2 12,1 12,-3 8,-5 3,-1 v -2 h 2 l 7,-14 1,-5 v -9 l -4,-13 -8,-10 -12,-6 -6,-1 h -11 l -9,3 -9,7 -6,9 -3,7 -1,12 4,13 1,5 -10,10 -1,2 -4,-2 -24,-16 -21,-12 -21,-10 -19,-7 -22,-6 6,-7 16,-16 22,-18 18,-13 16,-11 18,-13 33,-22 16,-11 23,-16 11,-8 22,-14 17,-12 12,-8 35,-23 19,-12 17,-12 18,-12 20,-13 18,-11 23,-15 20,-12 17,-11 25,-16 22,-14 33,-22 12,-7 z"
id="path5"
style="display:inline" />
<path
fill="#79b085"
d="m 1228,180 4,1 -8,10 h -2 l -2,4 -14,15 -7,8 -31,33 -9,9 -7,8 -7,7 -7,8 -11,11 -7,8 -14,15 -67,67 -7,8 -14,15 -20,20 -7,8 -22,22 -7,8 -8,8 -7,8 -13,13 -7,8 -15,14 -4,4 -2,-1 -3,-5 h -8 l -7,-3 -6,-1 h -11 l -9,3 -9,7 -6,9 -3,7 -1,12 4,13 1,5 -10,10 -1,2 -4,-2 -24,-16 -21,-12 -21,-10 -19,-7 -22,-6 6,-7 16,-16 22,-18 18,-13 16,-11 18,-13 33,-22 16,-11 23,-16 11,-8 22,-14 17,-12 12,-8 35,-23 19,-12 17,-12 18,-12 20,-13 18,-11 23,-15 20,-12 17,-11 25,-16 22,-14 33,-22 z"
id="path6"
style="display:inline" />
<path
fill="#418790"
d="m 1032,575 h 2 l 1,52 h 181 l 1,2 v 79 l -154,1 h -29 v 89 l -1,65 -8,1 h -87 l -1,-1 V 744 l 1,-14 51,-84 8,-14 12,-19 10,-17 11,-19 z"
id="path7"
style="display:inline" />
<path
fill="#408790"
d="m 478,900 4,1 24,10 33,12 1,3 -24,16 -26,17 -24,15 -19,12 -32,19 -21,13 -19,11 -28,16 -24,14 -24,13 -19,10 -27,14 -22,12 -30,15 -19,9 -22,12 -30,15 -28,13 -20,9 -13,5 -8,1 -2,-1 1,-4 13,-11 14,-11 16,-13 16,-12 13,-10 48,-32 25,-15 19,-12 25,-16 40,-25 26,-17 19,-12 17,-11 19,-12 36,-24 41,-28 24,-16 z"
id="path10"
style="display:inline" />
<path
fill="#13597b"
d="m 577,931 7,1 16,4 24,3 50,3 1,2 -4,6 -7,7 -11,9 -14,13 -11,9 -16,13 -9,7 -15,12 -18,13 -17,12 -20,14 -29,19 -19,12 -24,15 -24,14 -14,6 h -6 l -5,-4 -2,-4 1,-7 6,-8 8,-8 11,-9 16,-13 13,-10 11,-9 15,-12 19,-14 2,-2 -1,-4 -15,-14 -3,-3 1,-4 10,-10 17,-13 16,-13 16,-12 15,-13 z"
id="path11"
style="display:inline" />
<path
fill="#78b085"
d="m 1115,443 h 125 l 3,2 v 80 l -1,1 -45,1 h -98 l -37,-1 v -3 l 4,-5 16,-27 15,-26 4,-6 3,-6 5,-4 4,-5 z"
id="path13"
style="display:inline" />
<path
fill="#408690"
d="m 433,813 3,1 9,15 3,6 -2,4 1,10 1,9 -10,7 -28,19 -17,12 -15,10 -22,14 -17,12 -24,16 -13,6 h -8 l -8,-5 -4,-5 -2,-5 v -7 l 4,-10 9,-11 10,-10 20,-14 26,-17 22,-14 15,-10 19,-13 19,-14 z"
id="path14"
style="display:inline" />
<path
fill="#408790"
d="m 233,945 h 7 l 6,5 2,5 -1,10 -9,10 -8,7 -18,12 -15,8 -10,3 h -11 l -8,-4 -3,-5 1,-7 4,-6 9,-9 10,-7 18,-10 23,-11 z"
id="path20"
style="display:inline" />
<path
fill="#0f4664"
d="m 370,1125 h 10 l 6,5 2,5 v 8 l -4,6 -8,8 -13,8 -3,1 h -8 l -7,-6 -1,-2 v -7 l 4,-9 7,-9 11,-7 z"
id="path32"
style="display:inline" />
<path
fill="#3f8690"
d="m 209,874 6,1 4,5 1,3 v 9 l -3,5 -7,7 -10,6 -3,1 h -7 l -6,-5 -1,-6 5,-10 6,-7 10,-7 z"
id="path38"
style="display:inline" />
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="623"
height="469"
viewBox="0 0 1600 1204"
version="1.1"
id="svg440"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs440">
<linearGradient id="sfGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#00E5A0" />
<stop offset="100%" stop-color="#70BC84" />
</linearGradient>
</defs>
<path
fill="url(#sfGradient)"
d="m 1200,296 16,8 20,11 22,14 17,12 16,13 12,11 21,21 9,11 12,15 13,19 9,14 10,18 13,27 6,16 11,30 6,25 5,31 2,30 v 36 l -1,21 -4,29 -5,24 -8,26 -8,21 -13,31 -10,18 -20,30 -3,6 1,5 9,10 13,16 8,5 3,1 2,-4 5,-1 h 11 l 13,5 13,10 11,12 11,11 7,8 25,25 7,8 7,7 7,8 18,18 8,7 9,9 v 2 h 2 l 7,8 9,9 7,8 11,11 7,8 7,10 8,17 2,10 v 15 l -3,12 -4,9 -6,10 -9,10 -10,9 -16,8 -9,2 h -23 l -13,-4 -14,-7 -13,-11 -11,-11 -7,-8 -11,-12 -9,-10 -7,-7 -7,-8 -13,-14 -7,-8 -14,-15 -16,-17 -7,-7 -7,-8 -15,-15 v -2 l -4,-2 -8,-8 -10,-13 -4,-10 -1,-23 -5,-8 -9,-10 -7,-8 -7,-6 -6,1 -15,9 -18,12 -19,11 -26,13 -26,10 -29,8 -21,4 -21,2 h -49 l -26,-4 -12,-1 -18,14 -16,12 -17,11 -25,14 -23,11 -26,10 -28,9 -50,11 -4,2 -13,40 -13,37 -10,27 -2,6 h -2 l -13,-35 -24,-71 -3,-6 v -2 h -11 l -26,-4 -19,-5 -38,-13 -25,-11 -20,-10 -3,-3 16,-12 16,-13 11,-9 13,-11 5,-4 6,2 16,6 26,8 24,5 22,3 12,1 h 24 l 23,-2 29,-5 21,-5 36,-12 20,-8 20,-10 5,-3 -1,-3 -22,-11 -18,-11 -12,-9 -11,-9 -24,-22 -7,-8 -9,-10 -2,-5 9,-9 8,-7 9,-11 12,-17 4,-7 7,6 13,13 11,9 13,11 20,14 19,11 16,8 25,10 24,7 19,4 15,2 h 40 l 26,-3 22,-5 24,-8 21,-9 19,-10 19,-12 17,-12 14,-12 9,-9 h 2 l 2,-4 8,-8 15,-20 12,-19 13,-24 11,-27 10,-34 6,-31 2,-16 v -52 l -4,-27 -6,-24 -10,-30 -9,-21 -11,-20 -13,-21 -13,-17 -12,-14 -17,-17 -11,-9 -13,-11 -18,-13 -22,-13 -25,-12 -1,-3 10,-18 13,-21 z"
id="path2"
style="display:inline" />
<path
fill="url(#sfGradient)"
d="m 658,427 19,1 26,2 4,2 20,4 43,13 2,1 -1,4 -12,10 -14,11 -11,10 -8,7 -10,9 -16,15 -2,2 -17,-1 h -34 l -15,3 -13,5 -13,7 -9,8 -6,10 -3,9 v 11 l 4,12 9,10 12,7 20,8 32,8 43,10 25,7 18,6 20,9 14,8 13,10 9,8 9,11 9,13 8,18 4,15 2,10 1,13 v 13 l -2,18 -4,18 -6,16 -6,11 -10,13 -12,13 -14,11 -17,10 -21,9 -25,9 -25,5 -18,2 -20,1 h -23 l -24,-2 -38,-6 -48,-16 -23,-10 -17,-10 -24,-16 1,-4 41,-82 8,6 17,12 18,10 16,8 21,8 27,7 24,4 h 35 l 19,-3 16,-5 10,-5 8,-7 6,-12 2,-8 v -8 l -3,-12 -6,-10 -9,-8 -16,-8 -26,-8 -43,-10 -42,-12 -22,-8 -25,-12 -15,-10 -11,-9 -10,-10 -9,-14 -7,-15 -5,-16 -2,-10 -1,-13 v -12 l 3,-22 6,-20 7,-14 12,-17 9,-11 8,-8 17,-13 21,-12 24,-10 22,-6 23,-4 z"
id="path3"
style="display:inline" />
<path
fill="url(#sfGradient)"
d="m 775,90 2,1 12,36 14,41 12,35 23,4 31,5 24,7 36,12 23,10 32,16 20,12 15,10 -5,5 -16,10 -23,15 -18,11 -6,-1 -22,-12 -19,-8 -27,-9 -37,-10 -21,-4 -13,-2 -14,-1 h -47 l -27,3 -30,6 -31,9 -25,9 -24,11 -19,10 -19,12 -19,14 -13,11 -12,11 -28,28 -11,14 -15,20 -11,17 -14,24 -13,25 -11,29 -9,29 -8,36 -3,25 -1,14 v 55 l 3,27 6,31 4,19 v 4 l -16,12 -35,23 -24,15 -42,28 -46,30 -5,2 -6,-5 1,-7 6,-10 9,-11 15,-16 22,-22 8,-7 24,-24 8,-7 12,-12 -1,-9 -4,-21 -3,-27 -5,-5 -21,-11 -27,-11 -28,-11 -21,-9 -5,-2 v -2 l 17,-8 64,-25 22,-9 3,-5 7,-39 10,-40 15,-43 9,-20 13,-25 14,-24 13,-21 12,-16 7,-8 -1,-7 -11,-24 -17,-39 -5,-10 -3,-4 1,-2 11,5 39,16 27,13 3,3 4,-2 14,-12 20,-15 29,-19 22,-13 23,-12 15,-6 27,-10 31,-9 26,-6 34,-6 4,-3 12,-36 13,-37 z"
id="path4"
style="display:inline" />
<path
fill="url(#sfGradient)"
d="m 1241,171 1,2 -5,9 -12,19 -12,21 -8,13 -11,19 -15,24 -12,20 -31,51 -9,15 -11,18 -8,14 -16,26 -9,15 -17,28 -11,19 -10,17 -13,21 -16,27 -8,13 -11,19 -9,15 -17,29 -10,16 -10,19 -15,24 -11,18 -13,23 -9,15 -4,8 -3,1 -1,-4 -2,-27 -4,-29 -6,-25 -9,-24 -9,-22 -7,-12 -9,-12 -4,-5 1,-4 8,-10 7,1 6,2 12,1 12,-3 8,-5 3,-1 v -2 h 2 l 7,-14 1,-5 v -9 l -4,-13 -8,-10 -12,-6 -6,-1 h -11 l -9,3 -9,7 -6,9 -3,7 -1,12 4,13 1,5 -10,10 -1,2 -4,-2 -24,-16 -21,-12 -21,-10 -19,-7 -22,-6 6,-7 16,-16 22,-18 18,-13 16,-11 18,-13 33,-22 16,-11 23,-16 11,-8 22,-14 17,-12 12,-8 35,-23 19,-12 17,-12 18,-12 20,-13 18,-11 23,-15 20,-12 17,-11 25,-16 22,-14 33,-22 12,-7 z"
id="path5"
style="display:inline" />
<path
fill="url(#sfGradient)"
d="m 1228,180 4,1 -8,10 h -2 l -2,4 -14,15 -7,8 -31,33 -9,9 -7,8 -7,7 -7,8 -11,11 -7,8 -14,15 -67,67 -7,8 -14,15 -20,20 -7,8 -22,22 -7,8 -8,8 -7,8 -13,13 -7,8 -15,14 -4,4 -2,-1 -3,-5 h -8 l -7,-3 -6,-1 h -11 l -9,3 -9,7 -6,9 -3,7 -1,12 4,13 1,5 -10,10 -1,2 -4,-2 -24,-16 -21,-12 -21,-10 -19,-7 -22,-6 6,-7 16,-16 22,-18 18,-13 16,-11 18,-13 33,-22 16,-11 23,-16 11,-8 22,-14 17,-12 12,-8 35,-23 19,-12 17,-12 18,-12 20,-13 18,-11 23,-15 20,-12 17,-11 25,-16 22,-14 33,-22 z"
id="path6"
style="display:inline" />
<path
fill="url(#sfGradient)"
d="m 1032,575 h 2 l 1,52 h 181 l 1,2 v 79 l -154,1 h -29 v 89 l -1,65 -8,1 h -87 l -1,-1 V 744 l 1,-14 51,-84 8,-14 12,-19 10,-17 11,-19 z"
id="path7"
style="display:inline" />
<path
fill="url(#sfGradient)"
d="m 478,900 4,1 24,10 33,12 1,3 -24,16 -26,17 -24,15 -19,12 -32,19 -21,13 -19,11 -28,16 -24,14 -24,13 -19,10 -27,14 -22,12 -30,15 -19,9 -22,12 -30,15 -28,13 -20,9 -13,5 -8,1 -2,-1 1,-4 13,-11 14,-11 16,-13 16,-12 13,-10 48,-32 25,-15 19,-12 25,-16 40,-25 26,-17 19,-12 17,-11 19,-12 36,-24 41,-28 24,-16 z"
id="path10"
style="display:inline" />
<path
fill="url(#sfGradient)"
d="m 577,931 7,1 16,4 24,3 50,3 1,2 -4,6 -7,7 -11,9 -14,13 -11,9 -16,13 -9,7 -15,12 -18,13 -17,12 -20,14 -29,19 -19,12 -24,15 -24,14 -14,6 h -6 l -5,-4 -2,-4 1,-7 6,-8 8,-8 11,-9 16,-13 13,-10 11,-9 15,-12 19,-14 2,-2 -1,-4 -15,-14 -3,-3 1,-4 10,-10 17,-13 16,-13 16,-12 15,-13 z"
id="path11"
style="display:inline" />
<path
fill="url(#sfGradient)"
d="m 1115,443 h 125 l 3,2 v 80 l -1,1 -45,1 h -98 l -37,-1 v -3 l 4,-5 16,-27 15,-26 4,-6 3,-6 5,-4 4,-5 z"
id="path13"
style="display:inline" />
<path
fill="url(#sfGradient)"
d="m 433,813 3,1 9,15 3,6 -2,4 1,10 1,9 -10,7 -28,19 -17,12 -15,10 -22,14 -17,12 -24,16 -13,6 h -8 l -8,-5 -4,-5 -2,-5 v -7 l 4,-10 9,-11 10,-10 20,-14 26,-17 22,-14 15,-10 19,-13 19,-14 z"
id="path14"
style="display:inline" />
<path
fill="url(#sfGradient)"
d="m 233,945 h 7 l 6,5 2,5 -1,10 -9,10 -8,7 -18,12 -15,8 -10,3 h -11 l -8,-4 -3,-5 1,-7 4,-6 9,-9 10,-7 18,-10 23,-11 z"
id="path20"
style="display:inline" />
<path
fill="url(#sfGradient)"
d="m 370,1125 h 10 l 6,5 2,5 v 8 l -4,6 -8,8 -13,8 -3,1 h -8 l -7,-6 -1,-2 v -7 l 4,-9 7,-9 11,-7 z"
id="path32"
style="display:inline" />
<path
fill="url(#sfGradient)"
d="m 209,874 6,1 4,5 1,3 v 9 l -3,5 -7,7 -10,6 -3,1 h -7 l -6,-5 -1,-6 5,-10 6,-7 10,-7 z"
id="path38"
style="display:inline" />
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,116 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
font-family: 'Inter', system-ui, sans-serif;
}
body {
@apply bg-[#04151F] text-white;
background-image: radial-gradient(ellipse 80% 60% at 50% 0%, #0a2a40 0%, #04151F 70%);
background-attachment: fixed;
min-height: 100vh;
}
/* Chrome, Safari, Edge autofill override — keeps dark theme */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 30px #04151F inset !important;
-webkit-text-fill-color: white !important;
transition: background-color 5000s ease-in-out 0s;
}
}
@layer components {
.btn-primary {
@apply px-4 py-2 bg-sf-blue text-white rounded-lg hover:bg-sf-accent transition-colors;
}
.btn-secondary {
@apply px-4 py-2 bg-sf-wall text-sf-blue rounded-lg hover:bg-sf-green hover:text-white transition-colors;
}
.card {
@apply bg-white rounded-xl shadow-md p-6;
}
.bento-grid {
@apply grid grid-cols-1 md:grid-cols-3 gap-6;
}
.bento-cell {
@apply rounded-2xl p-6 shadow-lg transition-transform hover:scale-[1.02];
}
/* Service Finder specific components */
.sf-header-bg {
@apply bg-sf-wall bg-sf-wall-pattern;
}
.sf-card-green {
@apply bg-sf-green text-white;
}
.sf-card-blue {
@apply bg-sf-blue text-white;
}
.sf-card-accent {
@apply bg-sf-accent text-white;
}
/* ── Premium Button: Glassmorphism base, turquoise/green glow on hover, white flash on click ── */
.btn-premium {
@apply relative inline-flex items-center justify-center rounded-xl bg-white/10 px-6 py-3 font-bold text-white backdrop-blur-md border border-white/20 transition-all duration-300 cursor-pointer;
}
.btn-premium:hover {
@apply bg-gradient-to-r from-[#418890] to-[#70BC84] border-transparent shadow-[0_0_20px_rgba(0,229,160,0.5)] -translate-y-0.5;
}
.btn-premium:active {
@apply !bg-white !bg-none text-[#04151F] shadow-[0_0_30px_rgba(255,255,255,0.8)] scale-95 transition-none;
}
/* ── Premium Gradient Text for large headings ── */
.text-gradient-premium {
@apply bg-clip-text text-transparent bg-gradient-to-r from-white via-[#00E5A0] to-[#418890];
}
/* ── Fade Transition (Zen Mode toggle) ── */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* ── Hotspot Pulse Animation ── */
@keyframes hotspot-pulse {
0%, 100% {
transform: scale(1);
opacity: 0.8;
}
50% {
transform: scale(1.5);
opacity: 0.4;
}
}
.animate-hotspot-pulse {
animation: hotspot-pulse 2s ease-in-out infinite;
}
/* ── Hotspot Ring Ripple ── */
@keyframes hotspot-ring {
0% {
transform: scale(1);
opacity: 0.6;
}
100% {
transform: scale(2.5);
opacity: 0;
}
}
.animate-hotspot-ring {
animation: hotspot-ring 2.5s ease-out infinite;
}
.animate-hotspot-ring-delayed {
animation: hotspot-ring 2.5s ease-out 0.8s infinite;
}
}

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="623"
height="469"
viewBox="0 0 1600 1204"
version="1.1"
id="svg440"
sodipodi:docname="SF_csak_logo_ (1).svg"
inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs440" />
<sodipodi:namedview
id="namedview440"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.7206823"
inkscape:cx="390.83333"
inkscape:cy="234.5"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="1912"
inkscape:window-y="646"
inkscape:window-maximized="1"
inkscape:current-layer="svg440" />
<path
fill="#418890"
d="m 1200,296 16,8 20,11 22,14 17,12 16,13 12,11 21,21 9,11 12,15 13,19 9,14 10,18 13,27 6,16 11,30 6,25 5,31 2,30 v 36 l -1,21 -4,29 -5,24 -8,26 -8,21 -13,31 -10,18 -20,30 -3,6 1,5 9,10 13,16 8,5 3,1 2,-4 5,-1 h 11 l 13,5 13,10 11,12 11,11 7,8 25,25 7,8 7,7 7,8 18,18 8,7 9,9 v 2 h 2 l 7,8 9,9 7,8 11,11 7,8 7,10 8,17 2,10 v 15 l -3,12 -4,9 -6,10 -9,10 -10,9 -16,8 -9,2 h -23 l -13,-4 -14,-7 -13,-11 -11,-11 -7,-8 -11,-12 -9,-10 -7,-7 -7,-8 -13,-14 -7,-8 -14,-15 -16,-17 -7,-7 -7,-8 -15,-15 v -2 l -4,-2 -8,-8 -10,-13 -4,-10 -1,-23 -5,-8 -9,-10 -7,-8 -7,-6 -6,1 -15,9 -18,12 -19,11 -26,13 -26,10 -29,8 -21,4 -21,2 h -49 l -26,-4 -12,-1 -18,14 -16,12 -17,11 -25,14 -23,11 -26,10 -28,9 -50,11 -4,2 -13,40 -13,37 -10,27 -2,6 h -2 l -13,-35 -24,-71 -3,-6 v -2 h -11 l -26,-4 -19,-5 -38,-13 -25,-11 -20,-10 -3,-3 16,-12 16,-13 11,-9 13,-11 5,-4 6,2 16,6 26,8 24,5 22,3 12,1 h 24 l 23,-2 29,-5 21,-5 36,-12 20,-8 20,-10 5,-3 -1,-3 -22,-11 -18,-11 -12,-9 -11,-9 -24,-22 -7,-8 -9,-10 -2,-5 9,-9 8,-7 9,-11 12,-17 4,-7 7,6 13,13 11,9 13,11 20,14 19,11 16,8 25,10 24,7 19,4 15,2 h 40 l 26,-3 22,-5 24,-8 21,-9 19,-10 19,-12 17,-12 14,-12 9,-9 h 2 l 2,-4 8,-8 15,-20 12,-19 13,-24 11,-27 10,-34 6,-31 2,-16 v -52 l -4,-27 -6,-24 -10,-30 -9,-21 -11,-20 -13,-21 -13,-17 -12,-14 -17,-17 -11,-9 -13,-11 -18,-13 -22,-13 -25,-12 -1,-3 10,-18 13,-21 z"
id="path2"
style="display:inline" />
<path
fill="#13597b"
d="m 658,427 19,1 26,2 4,2 20,4 43,13 2,1 -1,4 -12,10 -14,11 -11,10 -8,7 -10,9 -16,15 -2,2 -17,-1 h -34 l -15,3 -13,5 -13,7 -9,8 -6,10 -3,9 v 11 l 4,12 9,10 12,7 20,8 32,8 43,10 25,7 18,6 20,9 14,8 13,10 9,8 9,11 9,13 8,18 4,15 2,10 1,13 v 13 l -2,18 -4,18 -6,16 -6,11 -10,13 -12,13 -14,11 -17,10 -21,9 -25,9 -25,5 -18,2 -20,1 h -23 l -24,-2 -38,-6 -48,-16 -23,-10 -17,-10 -24,-16 1,-4 41,-82 8,6 17,12 18,10 16,8 21,8 27,7 24,4 h 35 l 19,-3 16,-5 10,-5 8,-7 6,-12 2,-8 v -8 l -3,-12 -6,-10 -9,-8 -16,-8 -26,-8 -43,-10 -42,-12 -22,-8 -25,-12 -15,-10 -11,-9 -10,-10 -9,-14 -7,-15 -5,-16 -2,-10 -1,-13 v -12 l 3,-22 6,-20 7,-14 12,-17 9,-11 8,-8 17,-13 21,-12 24,-10 22,-6 23,-4 z"
id="path3"
style="display:inline" />
<path
fill="#418890"
d="m 775,90 2,1 12,36 14,41 12,35 23,4 31,5 24,7 36,12 23,10 32,16 20,12 15,10 -5,5 -16,10 -23,15 -18,11 -6,-1 -22,-12 -19,-8 -27,-9 -37,-10 -21,-4 -13,-2 -14,-1 h -47 l -27,3 -30,6 -31,9 -25,9 -24,11 -19,10 -19,12 -19,14 -13,11 -12,11 -28,28 -11,14 -15,20 -11,17 -14,24 -13,25 -11,29 -9,29 -8,36 -3,25 -1,14 v 55 l 3,27 6,31 4,19 v 4 l -16,12 -35,23 -24,15 -42,28 -46,30 -5,2 -6,-5 1,-7 6,-10 9,-11 15,-16 22,-22 8,-7 24,-24 8,-7 12,-12 -1,-9 -4,-21 -3,-27 -5,-5 -21,-11 -27,-11 -28,-11 -21,-9 -5,-2 v -2 l 17,-8 64,-25 22,-9 3,-5 7,-39 10,-40 15,-43 9,-20 13,-25 14,-24 13,-21 12,-16 7,-8 -1,-7 -11,-24 -17,-39 -5,-10 -3,-4 1,-2 11,5 39,16 27,13 3,3 4,-2 14,-12 20,-15 29,-19 22,-13 23,-12 15,-6 27,-10 31,-9 26,-6 34,-6 4,-3 12,-36 13,-37 z"
id="path4"
style="display:inline" />
<path
fill="#408790"
d="m 1241,171 1,2 -5,9 -12,19 -12,21 -8,13 -11,19 -15,24 -12,20 -31,51 -9,15 -11,18 -8,14 -16,26 -9,15 -17,28 -11,19 -10,17 -13,21 -16,27 -8,13 -11,19 -9,15 -17,29 -10,16 -10,19 -15,24 -11,18 -13,23 -9,15 -4,8 -3,1 -1,-4 -2,-27 -4,-29 -6,-25 -9,-24 -9,-22 -7,-12 -9,-12 -4,-5 1,-4 8,-10 7,1 6,2 12,1 12,-3 8,-5 3,-1 v -2 h 2 l 7,-14 1,-5 v -9 l -4,-13 -8,-10 -12,-6 -6,-1 h -11 l -9,3 -9,7 -6,9 -3,7 -1,12 4,13 1,5 -10,10 -1,2 -4,-2 -24,-16 -21,-12 -21,-10 -19,-7 -22,-6 6,-7 16,-16 22,-18 18,-13 16,-11 18,-13 33,-22 16,-11 23,-16 11,-8 22,-14 17,-12 12,-8 35,-23 19,-12 17,-12 18,-12 20,-13 18,-11 23,-15 20,-12 17,-11 25,-16 22,-14 33,-22 12,-7 z"
id="path5"
style="display:inline" />
<path
fill="#79b085"
d="m 1228,180 4,1 -8,10 h -2 l -2,4 -14,15 -7,8 -31,33 -9,9 -7,8 -7,7 -7,8 -11,11 -7,8 -14,15 -67,67 -7,8 -14,15 -20,20 -7,8 -22,22 -7,8 -8,8 -7,8 -13,13 -7,8 -15,14 -4,4 -2,-1 -3,-5 h -8 l -7,-3 -6,-1 h -11 l -9,3 -9,7 -6,9 -3,7 -1,12 4,13 1,5 -10,10 -1,2 -4,-2 -24,-16 -21,-12 -21,-10 -19,-7 -22,-6 6,-7 16,-16 22,-18 18,-13 16,-11 18,-13 33,-22 16,-11 23,-16 11,-8 22,-14 17,-12 12,-8 35,-23 19,-12 17,-12 18,-12 20,-13 18,-11 23,-15 20,-12 17,-11 25,-16 22,-14 33,-22 z"
id="path6"
style="display:inline" />
<path
fill="#418790"
d="m 1032,575 h 2 l 1,52 h 181 l 1,2 v 79 l -154,1 h -29 v 89 l -1,65 -8,1 h -87 l -1,-1 V 744 l 1,-14 51,-84 8,-14 12,-19 10,-17 11,-19 z"
id="path7"
style="display:inline" />
<path
fill="#408790"
d="m 478,900 4,1 24,10 33,12 1,3 -24,16 -26,17 -24,15 -19,12 -32,19 -21,13 -19,11 -28,16 -24,14 -24,13 -19,10 -27,14 -22,12 -30,15 -19,9 -22,12 -30,15 -28,13 -20,9 -13,5 -8,1 -2,-1 1,-4 13,-11 14,-11 16,-13 16,-12 13,-10 48,-32 25,-15 19,-12 25,-16 40,-25 26,-17 19,-12 17,-11 19,-12 36,-24 41,-28 24,-16 z"
id="path10"
style="display:inline" />
<path
fill="#13597b"
d="m 577,931 7,1 16,4 24,3 50,3 1,2 -4,6 -7,7 -11,9 -14,13 -11,9 -16,13 -9,7 -15,12 -18,13 -17,12 -20,14 -29,19 -19,12 -24,15 -24,14 -14,6 h -6 l -5,-4 -2,-4 1,-7 6,-8 8,-8 11,-9 16,-13 13,-10 11,-9 15,-12 19,-14 2,-2 -1,-4 -15,-14 -3,-3 1,-4 10,-10 17,-13 16,-13 16,-12 15,-13 z"
id="path11"
style="display:inline" />
<path
fill="#78b085"
d="m 1115,443 h 125 l 3,2 v 80 l -1,1 -45,1 h -98 l -37,-1 v -3 l 4,-5 16,-27 15,-26 4,-6 3,-6 5,-4 4,-5 z"
id="path13"
style="display:inline" />
<path
fill="#408690"
d="m 433,813 3,1 9,15 3,6 -2,4 1,10 1,9 -10,7 -28,19 -17,12 -15,10 -22,14 -17,12 -24,16 -13,6 h -8 l -8,-5 -4,-5 -2,-5 v -7 l 4,-10 9,-11 10,-10 20,-14 26,-17 22,-14 15,-10 19,-13 19,-14 z"
id="path14"
style="display:inline" />
<path
fill="#408790"
d="m 233,945 h 7 l 6,5 2,5 -1,10 -9,10 -8,7 -18,12 -15,8 -10,3 h -11 l -8,-4 -3,-5 1,-7 4,-6 9,-9 10,-7 18,-10 23,-11 z"
id="path20"
style="display:inline" />
<path
fill="#0f4664"
d="m 370,1125 h 10 l 6,5 2,5 v 8 l -4,6 -8,8 -13,8 -3,1 h -8 l -7,-6 -1,-2 v -7 l 4,-9 7,-9 11,-7 z"
id="path32"
style="display:inline" />
<path
fill="#3f8690"
d="m 209,874 6,1 4,5 1,3 v 9 l -3,5 -7,7 -10,6 -3,1 h -7 l -6,-5 -1,-6 5,-10 6,-7 10,-7 z"
id="path38"
style="display:inline" />
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

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

Before

Width:  |  Height:  |  Size: 496 B

Some files were not shown because too many files have changed in this diff Show More