2026.06.04 frontend építés közben
This commit is contained in:
24
old_maps/frontend_old/.gitignore
vendored
Executable file
24
old_maps/frontend_old/.gitignore
vendored
Executable file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
old_maps/frontend_old/Archive/.vscode/extensions.json
vendored
Executable file
3
old_maps/frontend_old/Archive/.vscode/extensions.json
vendored
Executable file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
18
old_maps/frontend_old/Archive/admin/.nuxt/app.config.mjs
Normal file
18
old_maps/frontend_old/Archive/admin/.nuxt/app.config.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
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)
|
||||
434
old_maps/frontend_old/Archive/admin/.nuxt/components.d.ts
vendored
Normal file
434
old_maps/frontend_old/Archive/admin/.nuxt/components.d.ts
vendored
Normal file
@@ -0,0 +1,434 @@
|
||||
|
||||
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[]
|
||||
3536
old_maps/frontend_old/Archive/admin/.nuxt/dev/index.mjs
Normal file
3536
old_maps/frontend_old/Archive/admin/.nuxt/dev/index.mjs
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
119
old_maps/frontend_old/Archive/admin/.nuxt/i18n.options.mjs
Normal file
119
old_maps/frontend_old/Archive/admin/.nuxt/i18n.options.mjs
Normal file
@@ -0,0 +1,119 @@
|
||||
|
||||
// @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"
|
||||
43
old_maps/frontend_old/Archive/admin/.nuxt/imports.d.ts
vendored
Normal file
43
old_maps/frontend_old/Archive/admin/.nuxt/imports.d.ts
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
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';
|
||||
@@ -0,0 +1 @@
|
||||
{"id":"dev","timestamp":1776057763271}
|
||||
@@ -0,0 +1 @@
|
||||
{"id":"dev","timestamp":1776057763271,"prerendered":[]}
|
||||
17
old_maps/frontend_old/Archive/admin/.nuxt/nitro.json
Normal file
17
old_maps/frontend_old/Archive/admin/.nuxt/nitro.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"date": "2026-04-14T18:57:09.447Z",
|
||||
"preset": "nitro-dev",
|
||||
"framework": {
|
||||
"name": "nuxt",
|
||||
"version": "3.21.2"
|
||||
},
|
||||
"versions": {
|
||||
"nitro": "2.13.2"
|
||||
},
|
||||
"dev": {
|
||||
"pid": 19,
|
||||
"workerAddress": {
|
||||
"socketPath": "\u0000nitro-worker-19-2-2-2761.sock"
|
||||
}
|
||||
}
|
||||
}
|
||||
30
old_maps/frontend_old/Archive/admin/.nuxt/nuxt.d.ts
vendored
Normal file
30
old_maps/frontend_old/Archive/admin/.nuxt/nuxt.d.ts
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
/// <reference types="@pinia/nuxt" />
|
||||
/// <reference types="vuetify-nuxt-module" />
|
||||
/// <reference types="@nuxtjs/i18n" />
|
||||
/// <reference types="@nuxt/telemetry" />
|
||||
/// <reference types="@nuxtjs/tailwindcss" />
|
||||
/// <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 {}
|
||||
9
old_maps/frontend_old/Archive/admin/.nuxt/nuxt.json
Normal file
9
old_maps/frontend_old/Archive/admin/.nuxt/nuxt.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"_hash": "86WsHSzrghegd85QlSfb0tmyVB8WGKoWBHcdl2r1_DE",
|
||||
"project": {
|
||||
"rootDir": "/app"
|
||||
},
|
||||
"versions": {
|
||||
"nuxt": "3.21.2"
|
||||
}
|
||||
}
|
||||
17
old_maps/frontend_old/Archive/admin/.nuxt/schema/nuxt.schema.d.ts
vendored
Normal file
17
old_maps/frontend_old/Archive/admin/.nuxt/schema/nuxt.schema.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
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 {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"id": "#"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 4/13/2026, 5:22:43 AM
|
||||
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;
|
||||
199
old_maps/frontend_old/Archive/admin/.nuxt/tsconfig.json
Normal file
199
old_maps/frontend_old/Archive/admin/.nuxt/tsconfig.json
Normal file
@@ -0,0 +1,199 @@
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"#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"
|
||||
]
|
||||
}
|
||||
168
old_maps/frontend_old/Archive/admin/.nuxt/tsconfig.server.json
Normal file
168
old_maps/frontend_old/Archive/admin/.nuxt/tsconfig.server.json
Normal file
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
7
old_maps/frontend_old/Archive/admin/.nuxt/types/app-defaults.d.ts
vendored
Normal file
7
old_maps/frontend_old/Archive/admin/.nuxt/types/app-defaults.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
declare module 'nuxt/app/defaults' {
|
||||
type DefaultAsyncDataErrorValue = null
|
||||
type DefaultAsyncDataValue = null
|
||||
type DefaultErrorValue = null
|
||||
type DedupeOption = boolean | 'cancel' | 'defer'
|
||||
}
|
||||
31
old_maps/frontend_old/Archive/admin/.nuxt/types/app.config.d.ts
vendored
Normal file
31
old_maps/frontend_old/Archive/admin/.nuxt/types/app.config.d.ts
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
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> { }
|
||||
}
|
||||
24
old_maps/frontend_old/Archive/admin/.nuxt/types/build.d.ts
vendored
Normal file
24
old_maps/frontend_old/Archive/admin/.nuxt/types/build.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
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";
|
||||
1
old_maps/frontend_old/Archive/admin/.nuxt/types/builder-env.d.ts
vendored
Normal file
1
old_maps/frontend_old/Archive/admin/.nuxt/types/builder-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import "vite/client";
|
||||
439
old_maps/frontend_old/Archive/admin/.nuxt/types/components.d.ts
vendored
Normal file
439
old_maps/frontend_old/Archive/admin/.nuxt/types/components.d.ts
vendored
Normal file
@@ -0,0 +1,439 @@
|
||||
|
||||
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 {}
|
||||
20
old_maps/frontend_old/Archive/admin/.nuxt/types/i18n-plugin.d.ts
vendored
Normal file
20
old_maps/frontend_old/Archive/admin/.nuxt/types/i18n-plugin.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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 {}
|
||||
449
old_maps/frontend_old/Archive/admin/.nuxt/types/imports.d.ts
vendored
Normal file
449
old_maps/frontend_old/Archive/admin/.nuxt/types/imports.d.ts
vendored
Normal file
@@ -0,0 +1,449 @@
|
||||
// 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']>
|
||||
}
|
||||
}
|
||||
14
old_maps/frontend_old/Archive/admin/.nuxt/types/layouts.d.ts
vendored
Normal file
14
old_maps/frontend_old/Archive/admin/.nuxt/types/layouts.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
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>
|
||||
}
|
||||
}
|
||||
7
old_maps/frontend_old/Archive/admin/.nuxt/types/middleware.d.ts
vendored
Normal file
7
old_maps/frontend_old/Archive/admin/.nuxt/types/middleware.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { NavigationGuard } from 'vue-router'
|
||||
export type MiddlewareKey = never
|
||||
declare module 'nuxt/app' {
|
||||
interface PageMeta {
|
||||
middleware?: MiddlewareKey | NavigationGuard | Array<MiddlewareKey | NavigationGuard>
|
||||
}
|
||||
}
|
||||
14
old_maps/frontend_old/Archive/admin/.nuxt/types/nitro-config.d.ts
vendored
Normal file
14
old_maps/frontend_old/Archive/admin/.nuxt/types/nitro-config.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
// Generated by nitro
|
||||
|
||||
// App Config
|
||||
import type { Defu } from 'defu'
|
||||
|
||||
|
||||
|
||||
type UserAppConfig = Defu<{}, []>
|
||||
|
||||
declare module "nitropack/types" {
|
||||
interface AppConfig extends UserAppConfig {}
|
||||
|
||||
}
|
||||
export {}
|
||||
149
old_maps/frontend_old/Archive/admin/.nuxt/types/nitro-imports.d.ts
vendored
Normal file
149
old_maps/frontend_old/Archive/admin/.nuxt/types/nitro-imports.d.ts
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
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';
|
||||
17
old_maps/frontend_old/Archive/admin/.nuxt/types/nitro-layouts.d.ts
vendored
Normal file
17
old_maps/frontend_old/Archive/admin/.nuxt/types/nitro-layouts.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
17
old_maps/frontend_old/Archive/admin/.nuxt/types/nitro-middleware.d.ts
vendored
Normal file
17
old_maps/frontend_old/Archive/admin/.nuxt/types/nitro-middleware.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
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>
|
||||
}
|
||||
}
|
||||
39
old_maps/frontend_old/Archive/admin/.nuxt/types/nitro-nuxt.d.ts
vendored
Normal file
39
old_maps/frontend_old/Archive/admin/.nuxt/types/nitro-nuxt.d.ts
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
|
||||
/// <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>
|
||||
}
|
||||
}
|
||||
14
old_maps/frontend_old/Archive/admin/.nuxt/types/nitro-routes.d.ts
vendored
Normal file
14
old_maps/frontend_old/Archive/admin/.nuxt/types/nitro-routes.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
// 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 {}
|
||||
3
old_maps/frontend_old/Archive/admin/.nuxt/types/nitro.d.ts
vendored
Normal file
3
old_maps/frontend_old/Archive/admin/.nuxt/types/nitro.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/// <reference path="./nitro-routes.d.ts" />
|
||||
/// <reference path="./nitro-config.d.ts" />
|
||||
/// <reference path="./nitro-imports.d.ts" />
|
||||
42
old_maps/frontend_old/Archive/admin/.nuxt/types/plugins.d.ts
vendored
Normal file
42
old_maps/frontend_old/Archive/admin/.nuxt/types/plugins.d.ts
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
// 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 { }
|
||||
217
old_maps/frontend_old/Archive/admin/.nuxt/types/schema.d.ts
vendored
Normal file
217
old_maps/frontend_old/Archive/admin/.nuxt/types/schema.d.ts
vendored
Normal file
@@ -0,0 +1,217 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
0
old_maps/frontend_old/Archive/admin/.nuxt/types/vue-shim.d.ts
vendored
Normal file
0
old_maps/frontend_old/Archive/admin/.nuxt/types/vue-shim.d.ts
vendored
Normal file
44
old_maps/frontend_old/Archive/admin/Dockerfile
Normal file
44
old_maps/frontend_old/Archive/admin/Dockerfile
Normal file
@@ -0,0 +1,44 @@
|
||||
# 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"]
|
||||
24
old_maps/frontend_old/Archive/admin/Dockerfile.dev
Normal file
24
old_maps/frontend_old/Archive/admin/Dockerfile.dev
Normal file
@@ -0,0 +1,24 @@
|
||||
# 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"]
|
||||
18
old_maps/frontend_old/Archive/admin/app.vue
Normal file
18
old_maps/frontend_old/Archive/admin/app.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<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>
|
||||
635
old_maps/frontend_old/Archive/admin/components/AiLogsTile.vue
Normal file
635
old_maps/frontend_old/Archive/admin/components/AiLogsTile.vue
Normal file
@@ -0,0 +1,635 @@
|
||||
<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>
|
||||
474
old_maps/frontend_old/Archive/admin/components/FinancialTile.vue
Normal file
474
old_maps/frontend_old/Archive/admin/components/FinancialTile.vue
Normal file
@@ -0,0 +1,474 @@
|
||||
<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>
|
||||
@@ -0,0 +1,497 @@
|
||||
<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>
|
||||
@@ -0,0 +1,202 @@
|
||||
<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>
|
||||
@@ -0,0 +1,590 @@
|
||||
<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>
|
||||
168
old_maps/frontend_old/Archive/admin/components/TileCard.vue
Normal file
168
old_maps/frontend_old/Archive/admin/components/TileCard.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<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>
|
||||
174
old_maps/frontend_old/Archive/admin/components/TileWrapper.vue
Normal file
174
old_maps/frontend_old/Archive/admin/components/TileWrapper.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<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>
|
||||
@@ -0,0 +1,189 @@
|
||||
<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='© <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>
|
||||
@@ -0,0 +1,390 @@
|
||||
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' })
|
||||
}
|
||||
}
|
||||
}
|
||||
200
old_maps/frontend_old/Archive/admin/composables/usePolling.ts
Normal file
200
old_maps/frontend_old/Archive/admin/composables/usePolling.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
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
|
||||
237
old_maps/frontend_old/Archive/admin/composables/useRBAC.ts
Normal file
237
old_maps/frontend_old/Archive/admin/composables/useRBAC.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
185
old_maps/frontend_old/Archive/admin/composables/useServiceMap.ts
Normal file
185
old_maps/frontend_old/Archive/admin/composables/useServiceMap.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
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
|
||||
356
old_maps/frontend_old/Archive/admin/development_log.md
Normal file
356
old_maps/frontend_old/Archive/admin/development_log.md
Normal file
@@ -0,0 +1,356 @@
|
||||
# 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.
|
||||
69
old_maps/frontend_old/Archive/admin/locales/en.json
Normal file
69
old_maps/frontend_old/Archive/admin/locales/en.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
69
old_maps/frontend_old/Archive/admin/locales/hu.json
Normal file
69
old_maps/frontend_old/Archive/admin/locales/hu.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
61
old_maps/frontend_old/Archive/admin/nuxt.config.ts
Normal file
61
old_maps/frontend_old/Archive/admin/nuxt.config.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
40
old_maps/frontend_old/Archive/admin/package.json
Normal file
40
old_maps/frontend_old/Archive/admin/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
760
old_maps/frontend_old/Archive/admin/pages/dashboard.vue
Normal file
760
old_maps/frontend_old/Archive/admin/pages/dashboard.vue
Normal file
@@ -0,0 +1,760 @@
|
||||
<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>
|
||||
15
old_maps/frontend_old/Archive/admin/pages/index.vue
Normal file
15
old_maps/frontend_old/Archive/admin/pages/index.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<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>
|
||||
223
old_maps/frontend_old/Archive/admin/pages/login.vue
Normal file
223
old_maps/frontend_old/Archive/admin/pages/login.vue
Normal file
@@ -0,0 +1,223 @@
|
||||
<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>
|
||||
311
old_maps/frontend_old/Archive/admin/pages/moderation-map.vue
Normal file
311
old_maps/frontend_old/Archive/admin/pages/moderation-map.vue
Normal file
@@ -0,0 +1,311 @@
|
||||
<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>
|
||||
1247
old_maps/frontend_old/Archive/admin/pages/users.vue
Normal file
1247
old_maps/frontend_old/Archive/admin/pages/users.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 266 B |
@@ -0,0 +1,4 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 260 B |
239
old_maps/frontend_old/Archive/admin/stores/auth.ts
Normal file
239
old_maps/frontend_old/Archive/admin/stores/auth.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
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
|
||||
}
|
||||
})
|
||||
204
old_maps/frontend_old/Archive/admin/stores/tiles.ts
Normal file
204
old_maps/frontend_old/Archive/admin/stores/tiles.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
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
|
||||
}
|
||||
})
|
||||
4
old_maps/frontend_old/Archive/admin/tsconfig.json
Normal file
4
old_maps/frontend_old/Archive/admin/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
||||
61
old_maps/frontend_old/Archive/e2e/frontend-flow.spec.js
Normal file
61
old_maps/frontend_old/Archive/e2e/frontend-flow.spec.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
// Use internal Docker network hostname for frontend
|
||||
const FRONTEND_URL = 'http://sf_public_frontend:5173';
|
||||
// Test user credentials (should be a valid test user in the dev database)
|
||||
const TEST_EMAIL = 'superadmin@profibot.hu';
|
||||
const TEST_PASSWORD = 'anypassword';
|
||||
|
||||
test.describe('Frontend UI E2E Flow', () => {
|
||||
test('should login, select profile mode, and load dashboard', async ({ page }) => {
|
||||
// Step 1: Open login page
|
||||
await page.goto(`${FRONTEND_URL}/login`);
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
await expect(page.getByRole('heading', { name: /login/i })).toBeVisible();
|
||||
|
||||
// Step 2: Fill credentials and submit
|
||||
await page.getByLabel(/email/i).fill(TEST_EMAIL);
|
||||
await page.getByLabel(/password/i).fill(TEST_PASSWORD);
|
||||
await page.getByRole('button', { name: /sign in|login/i }).click();
|
||||
|
||||
// Step 3: Wait for redirect to profile selection (since no UI mode selected)
|
||||
await expect(page).toHaveURL(/\/profile-select/);
|
||||
await expect(page.getByRole('heading', { name: /welcome to service finder/i })).toBeVisible();
|
||||
|
||||
// Step 4: Select Private Garage mode
|
||||
await page.getByText(/private garage/i).click();
|
||||
await expect(page.locator('.selected').filter({ hasText: /private garage/i })).toBeVisible();
|
||||
|
||||
// Step 5: Click Continue to Dashboard
|
||||
await page.getByRole('button', { name: /continue to dashboard/i }).click();
|
||||
|
||||
// Step 6: Verify dashboard loads
|
||||
await expect(page).toHaveURL(/\//);
|
||||
await expect(page.getByRole('heading').filter({ hasText: /dashboard/i }).first()).toBeVisible();
|
||||
|
||||
// Step 7: Verify gamification trophies are present
|
||||
await expect(page.getByText(/trophies|achievements/i).first()).toBeVisible();
|
||||
|
||||
// Step 8: Verify "Add Expense" link is present and clickable
|
||||
const addExpenseLink = page.getByRole('link', { name: /add expense/i });
|
||||
await expect(addExpenseLink).toBeVisible();
|
||||
await addExpenseLink.click();
|
||||
// Should navigate to add expense page
|
||||
await expect(page.getByRole('heading', { name: /add expense/i })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should handle corporate fleet selection', async ({ page }) => {
|
||||
await page.goto(`${FRONTEND_URL}/login`);
|
||||
await page.getByLabel(/email/i).fill(TEST_EMAIL);
|
||||
await page.getByLabel(/password/i).fill(TEST_PASSWORD);
|
||||
await page.getByRole('button', { name: /sign in|login/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/profile-select/);
|
||||
await page.getByText(/corporate fleet/i).click();
|
||||
await page.getByRole('button', { name: /continue to dashboard/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\//);
|
||||
// Fleet dashboard may have different elements, but at least dashboard title
|
||||
await expect(page.getByRole('heading').filter({ hasText: /dashboard/i }).first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
1
old_maps/frontend_old/Archive/public/vite.svg
Executable file
1
old_maps/frontend_old/Archive/public/vite.svg
Executable file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
197
old_maps/frontend_old/Archive/src/App.vue
Executable file
197
old_maps/frontend_old/Archive/src/App.vue
Executable file
@@ -0,0 +1,197 @@
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.nav-link {
|
||||
position: relative;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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>
|
||||
1
old_maps/frontend_old/Archive/src/assets/vue.svg
Executable file
1
old_maps/frontend_old/Archive/src/assets/vue.svg
Executable file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
231
old_maps/frontend_old/Archive/src/components/DailyQuizModal.vue
Normal file
231
old_maps/frontend_old/Archive/src/components/DailyQuizModal.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useQuizStore } from '@/stores/quizStore'
|
||||
|
||||
const quizStore = useQuizStore()
|
||||
|
||||
const showModal = ref(false)
|
||||
const currentQuestionIndex = ref(0)
|
||||
const selectedOption = ref(null)
|
||||
const showResult = ref(false)
|
||||
const isCorrect = ref(false)
|
||||
const resultExplanation = ref('')
|
||||
const isLoading = ref(false)
|
||||
|
||||
const currentQuestion = computed(() => quizStore.questions[currentQuestionIndex.value])
|
||||
const totalQuestions = computed(() => quizStore.totalQuestions)
|
||||
const progress = computed(() => ((currentQuestionIndex.value + 1) / totalQuestions.value) * 100)
|
||||
|
||||
// Auto-show modal after 3-5 seconds if canPlayToday
|
||||
onMounted(() => {
|
||||
if (quizStore.canPlayToday) {
|
||||
setTimeout(() => {
|
||||
openModal()
|
||||
}, 3500) // 3.5 seconds
|
||||
}
|
||||
})
|
||||
|
||||
async function openModal() {
|
||||
if (!quizStore.canPlayToday) {
|
||||
alert('Már játszottál ma! Holnap próbáld újra.')
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
// Fetch daily quiz questions from API
|
||||
await quizStore.fetchDailyQuiz()
|
||||
resetQuiz()
|
||||
showModal.value = true
|
||||
} catch (error) {
|
||||
console.error('Failed to load daily quiz:', error)
|
||||
alert('Hiba történt a kvíz betöltése közben. Próbáld újra később.')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
function resetQuiz() {
|
||||
currentQuestionIndex.value = 0
|
||||
selectedOption.value = null
|
||||
showResult.value = false
|
||||
isCorrect.value = false
|
||||
resultExplanation.value = ''
|
||||
}
|
||||
|
||||
async function selectOption(optionIndex) {
|
||||
if (showResult.value) return
|
||||
selectedOption.value = optionIndex
|
||||
|
||||
try {
|
||||
const result = await quizStore.answerQuestion(currentQuestion.value.id, optionIndex)
|
||||
isCorrect.value = result.is_correct
|
||||
resultExplanation.value = result.explanation
|
||||
showResult.value = true
|
||||
} catch (error) {
|
||||
console.error('Failed to submit answer:', error)
|
||||
alert('Hiba történt a válasz beküldése közben.')
|
||||
}
|
||||
}
|
||||
|
||||
function nextQuestion() {
|
||||
if (currentQuestionIndex.value < totalQuestions.value - 1) {
|
||||
currentQuestionIndex.value++
|
||||
selectedOption.value = null
|
||||
showResult.value = false
|
||||
} else {
|
||||
finishQuiz()
|
||||
}
|
||||
}
|
||||
|
||||
async function finishQuiz() {
|
||||
try {
|
||||
await quizStore.completeDailyQuiz()
|
||||
showModal.value = false
|
||||
alert(`Kvíz befejezve! Szerezttél ${quizStore.userPoints} pontot. Streak: ${quizStore.currentStreak}`)
|
||||
} catch (error) {
|
||||
console.error('Failed to complete quiz:', error)
|
||||
alert('Hiba történt a kvíz befejezése közben.')
|
||||
}
|
||||
}
|
||||
|
||||
function skipToday() {
|
||||
quizStore.completeDailyQuiz() // mark as played today
|
||||
closeModal()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="showModal" class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 p-4">
|
||||
<div class="relative w-full max-w-2xl rounded-2xl bg-gradient-to-br from-blue-50 to-white shadow-2xl p-6 md:p-8 border border-blue-200">
|
||||
<!-- Close button -->
|
||||
<button @click="closeModal" class="absolute top-4 right-4 text-gray-500 hover:text-gray-800 text-2xl">
|
||||
×
|
||||
</button>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-6">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gradient-to-r from-blue-500 to-purple-500 mb-4">
|
||||
<span class="text-3xl">🧠</span>
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold text-gray-900">Napi Kvíz</h2>
|
||||
<p class="text-gray-600 mt-2">Teszteld tudásod és szerezz pontokat!</p>
|
||||
<div class="mt-4 flex items-center justify-between text-sm text-gray-700">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-bold">Pontok:</span>
|
||||
<span class="bg-blue-100 text-blue-800 px-3 py-1 rounded-full">{{ quizStore.userPoints }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-bold">Streak:</span>
|
||||
<span class="bg-amber-100 text-amber-800 px-3 py-1 rounded-full">{{ quizStore.currentStreak }} nap</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress bar -->
|
||||
<div class="mb-8">
|
||||
<div class="flex justify-between text-sm text-gray-700 mb-2">
|
||||
<span>Kérdés {{ currentQuestionIndex + 1 }} / {{ totalQuestions }}</span>
|
||||
<span>{{ Math.round(progress) }}%</span>
|
||||
</div>
|
||||
<div class="h-3 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-gradient-to-r from-blue-500 to-purple-500 transition-all duration-500" :style="{ width: `${progress}%` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Question -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-6">{{ currentQuestion.question }}</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<button
|
||||
v-for="(option, idx) in currentQuestion.options"
|
||||
:key="idx"
|
||||
@click="selectOption(idx)"
|
||||
class="p-4 text-left rounded-xl border-2 transition-all duration-300"
|
||||
:class="{
|
||||
'border-blue-500 bg-blue-50': selectedOption === idx,
|
||||
'border-gray-300 hover:border-blue-400 hover:bg-blue-50': selectedOption === null && !showResult,
|
||||
'border-green-500 bg-green-50': showResult && idx === currentQuestion.correctAnswer,
|
||||
'border-red-300 bg-red-50': showResult && selectedOption === idx && !isCorrect,
|
||||
}"
|
||||
:disabled="showResult"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center mr-3"
|
||||
:class="{
|
||||
'bg-blue-100 text-blue-800': selectedOption === idx && !showResult,
|
||||
'bg-green-100 text-green-800': showResult && idx === currentQuestion.correctAnswer,
|
||||
'bg-red-100 text-red-800': showResult && selectedOption === idx && !isCorrect,
|
||||
'bg-gray-100 text-gray-800': selectedOption !== idx && !showResult,
|
||||
}">
|
||||
{{ String.fromCharCode(65 + idx) }}
|
||||
</div>
|
||||
<span class="font-medium">{{ option }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result & Explanation -->
|
||||
<div v-if="showResult" class="mb-8 p-5 rounded-xl" :class="isCorrect ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="w-10 h-10 rounded-full flex items-center justify-center" :class="isCorrect ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'">
|
||||
{{ isCorrect ? '✅' : '❌' }}
|
||||
</div>
|
||||
<h4 class="text-xl font-bold" :class="isCorrect ? 'text-green-800' : 'text-red-800'">
|
||||
{{ isCorrect ? 'Helyes válasz!' : 'Sajnos nem talált!' }}
|
||||
</h4>
|
||||
</div>
|
||||
<p class="text-gray-800">{{ resultExplanation }}</p>
|
||||
<div class="mt-4 text-sm text-gray-700">
|
||||
<span class="font-bold">Pontok:</span> {{ isCorrect ? '+10' : '0' }} |
|
||||
<span class="font-bold">Streak:</span> {{ isCorrect ? 'növelve' : 'nullázva' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
<button
|
||||
v-if="!showResult"
|
||||
@click="skipToday"
|
||||
class="flex-1 py-3 px-6 rounded-xl border-2 border-gray-300 text-gray-700 font-semibold hover:bg-gray-100 transition"
|
||||
>
|
||||
Emlékeztess később
|
||||
</button>
|
||||
<button
|
||||
v-if="showResult && currentQuestionIndex < totalQuestions - 1"
|
||||
@click="nextQuestion"
|
||||
class="flex-1 py-3 px-6 rounded-xl bg-gradient-to-r from-blue-500 to-purple-500 text-white font-bold hover:opacity-90 transition"
|
||||
>
|
||||
Következő kérdés
|
||||
</button>
|
||||
<button
|
||||
v-if="showResult && currentQuestionIndex === totalQuestions - 1"
|
||||
@click="finishQuiz"
|
||||
class="flex-1 py-3 px-6 rounded-xl bg-gradient-to-r from-green-500 to-emerald-600 text-white font-bold hover:opacity-90 transition"
|
||||
>
|
||||
Kvíz befejezése
|
||||
</button>
|
||||
<button
|
||||
@click="closeModal"
|
||||
class="flex-1 py-3 px-6 rounded-xl bg-gray-200 text-gray-800 font-semibold hover:bg-gray-300 transition"
|
||||
>
|
||||
Bezárás
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Footer note -->
|
||||
<div class="mt-8 text-center text-sm text-gray-500">
|
||||
A napi kvíz csak egyszer játszható 24 óránként. Streaked növeléséhez válaszolj helyesen minden nap!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Additional custom styles if needed */
|
||||
</style>
|
||||
161
old_maps/frontend_old/Archive/src/components/ProfileSelector.vue
Normal file
161
old_maps/frontend_old/Archive/src/components/ProfileSelector.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<div class="max-w-6xl mx-auto p-6">
|
||||
<h1 class="text-4xl font-bold text-center mb-4 text-slate-900">Welcome to Service Finder</h1>
|
||||
<p class="text-lg text-slate-600 text-center mb-12 max-w-2xl mx-auto">
|
||||
Choose your experience based on how you use vehicles. Your selection will customize the dashboard and features.
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12">
|
||||
<!-- Private Garage Card - Vibrant gradient border & playful -->
|
||||
<div
|
||||
class="relative rounded-3xl p-8 cursor-pointer transition-all duration-300 hover:scale-[1.02] active:scale-95 bg-gradient-to-br from-white to-slate-50/80 backdrop-blur-sm border border-slate-200/60"
|
||||
:class="{
|
||||
'selected shadow-2xl ring-4 ring-opacity-30 ring-amber-400/50': isPrivateGarage,
|
||||
'private-garage': true
|
||||
}"
|
||||
@click="selectMode('private_garage')"
|
||||
>
|
||||
<!-- Vibrant gradient border effect -->
|
||||
<div v-if="isPrivateGarage" class="absolute -inset-0.5 bg-gradient-to-r from-amber-400 via-orange-400 to-pink-400 rounded-3xl blur-sm opacity-70 -z-10"></div>
|
||||
|
||||
<div class="mb-6 p-4 rounded-2xl inline-flex bg-gradient-to-br from-amber-100 to-orange-100 text-amber-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold mb-4 text-slate-900">Private Garage</h2>
|
||||
<p class="text-slate-700 mb-6 leading-relaxed">
|
||||
Perfect for individual vehicle owners. Track expenses, maintenance, and get personalized recommendations for your personal cars, motorcycles, or recreational vehicles.
|
||||
</p>
|
||||
<ul class="mb-8 space-y-3 card-features-private">
|
||||
<li class="flex items-center text-sm text-slate-700">Personal vehicle management</li>
|
||||
<li class="flex items-center text-sm text-slate-700">Expense tracking & budgeting</li>
|
||||
<li class="flex items-center text-sm text-slate-700">Maintenance reminders</li>
|
||||
<li class="flex items-center text-sm text-slate-700">Fuel efficiency analytics</li>
|
||||
</ul>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="px-3 py-1.5 rounded-full text-xs font-semibold bg-gradient-to-r from-amber-100 to-orange-100 text-amber-800 border border-amber-200/60">For Individuals</span>
|
||||
<button
|
||||
class="px-6 py-2.5 rounded-lg font-medium transition-all duration-200 bg-gradient-to-r from-amber-500 to-orange-500 text-white hover:from-amber-600 hover:to-orange-600 active:scale-95 shadow-md hover:shadow-lg"
|
||||
:class="{ 'ring-2 ring-amber-300 ring-offset-2': isPrivateGarage }"
|
||||
>
|
||||
{{ isPrivateGarage ? '✓ Selected' : 'Select' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Corporate Fleet Card - Minimalist sharp design -->
|
||||
<div
|
||||
class="relative rounded-3xl p-8 cursor-pointer transition-all duration-300 hover:scale-[1.02] active:scale-95 bg-gradient-to-br from-white to-slate-50/80 backdrop-blur-sm border border-slate-200/60"
|
||||
:class="{
|
||||
'selected shadow-2xl ring-4 ring-opacity-30 ring-blue-400/50': isCorporateFleet,
|
||||
'corporate-fleet': true
|
||||
}"
|
||||
@click="selectMode('corporate_fleet')"
|
||||
>
|
||||
<!-- Sharp business accent line -->
|
||||
<div v-if="isCorporateFleet" class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-blue-500 to-cyan-500 rounded-t-3xl"></div>
|
||||
|
||||
<div class="mb-6 p-4 rounded-2xl inline-flex bg-gradient-to-br from-blue-50 to-cyan-50 text-blue-700 border border-blue-200/40">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold mb-4 text-slate-900">Corporate Fleet</h2>
|
||||
<p class="text-slate-700 mb-6 leading-relaxed">
|
||||
Designed for fleet managers and businesses. Monitor multiple vehicles, optimize TCO (Total Cost of Ownership), and manage service schedules across your entire fleet.
|
||||
</p>
|
||||
<ul class="mb-8 space-y-3 card-features-corporate">
|
||||
<li class="flex items-center text-sm text-slate-700">Multi-vehicle fleet management</li>
|
||||
<li class="flex items-center text-sm text-slate-700">TCO & ROI analytics</li>
|
||||
<li class="flex items-center text-sm text-slate-700">Driver assignment & reporting</li>
|
||||
<li class="flex items-center text-sm text-slate-700">Bulk service scheduling</li>
|
||||
</ul>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="px-3 py-1.5 rounded-full text-xs font-semibold bg-gradient-to-r from-blue-50 to-cyan-50 text-blue-800 border border-blue-200/60">For Businesses</span>
|
||||
<button
|
||||
class="px-6 py-2.5 rounded-lg font-medium transition-all duration-200 bg-gradient-to-r from-blue-600 to-cyan-600 text-white hover:from-blue-700 hover:to-cyan-700 active:scale-95 shadow-md hover:shadow-lg"
|
||||
:class="{ 'ring-2 ring-blue-300 ring-offset-2': isCorporateFleet }"
|
||||
>
|
||||
{{ isCorporateFleet ? '✓ Selected' : 'Select' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row justify-between items-center p-6 border-t border-slate-200/60">
|
||||
<p class="text-sm text-slate-500">
|
||||
You can change this later from the header menu.
|
||||
</p>
|
||||
<button
|
||||
class="mt-4 md:mt-0 px-8 py-3.5 bg-gradient-to-r from-blue-600 to-cyan-600 text-white font-semibold rounded-xl hover:from-blue-700 hover:to-cyan-700 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center shadow-lg hover:shadow-xl active:scale-95"
|
||||
@click="continueToDashboard"
|
||||
:disabled="!mode"
|
||||
>
|
||||
Continue to Dashboard
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-2" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L12.586 11H5a1 1 0 110-2h7.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const appModeStore = useAppModeStore()
|
||||
const router = useRouter()
|
||||
|
||||
const { mode, isPrivateGarage, isCorporateFleet } = storeToRefs(appModeStore)
|
||||
|
||||
function selectMode(newMode) {
|
||||
appModeStore.setMode(newMode)
|
||||
}
|
||||
|
||||
function continueToDashboard() {
|
||||
console.log('ProfileSelector: Continuing to dashboard with mode', mode.value)
|
||||
try {
|
||||
router.push('/')
|
||||
console.log('ProfileSelector: Redirect to dashboard successful')
|
||||
} catch (error) {
|
||||
console.error('ProfileSelector: Failed to redirect to dashboard:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Keep only pseudo-element and media query styles */
|
||||
.card-features-private li::before,
|
||||
.card-features-corporate li::before {
|
||||
content: '✓';
|
||||
margin-right: 0.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.card-features-private li::before {
|
||||
color: #f59e0b; /* amber-500 */
|
||||
}
|
||||
|
||||
.card-features-corporate li::before {
|
||||
color: #06b6d4; /* cyan-500 */
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.grid {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.rounded-3xl.p-8 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.flex.flex-col.md\:flex-row {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,245 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useExpenseStore } from '@/stores/expenseStore'
|
||||
import { useGarageStore } from '@/stores/garageStore'
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const expenseStore = useExpenseStore()
|
||||
const garageStore = useGarageStore()
|
||||
|
||||
// Use selected vehicle from garage store, or default to first vehicle
|
||||
const selectedAssetId = ref(garageStore.selectedVehicle?.id || garageStore.vehicles[0]?.id || '')
|
||||
|
||||
const amount = ref('')
|
||||
const category = ref('fuel')
|
||||
const date = ref(new Date().toISOString().split('T')[0]) // today
|
||||
const description = ref('')
|
||||
const mileage = ref('')
|
||||
const isLoading = ref(false)
|
||||
const showDraftLimitModal = ref(false)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!selectedAssetId.value) {
|
||||
alert('Nincs kiválasztott jármű. Kérjük, először adj hozzá egy járművet.')
|
||||
return
|
||||
}
|
||||
if (!amount.value || !date.value) {
|
||||
alert('Kérjük, töltsd ki a kötelező mezőket.')
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const expenseData = {
|
||||
asset_id: selectedAssetId.value,
|
||||
cost_type: category.value, // fuel, service, tax, insurance
|
||||
amount_local: parseFloat(amount.value),
|
||||
currency_local: 'HUF', // default, could be dynamic
|
||||
date: new Date(date.value).toISOString(),
|
||||
description: description.value,
|
||||
mileage_at_cost: mileage.value ? parseInt(mileage.value) : null,
|
||||
data: {}
|
||||
}
|
||||
await expenseStore.createExpense(expenseData)
|
||||
|
||||
// Success
|
||||
alert('Költség sikeresen mentve!')
|
||||
|
||||
// Reset form
|
||||
amount.value = ''
|
||||
category.value = 'fuel'
|
||||
date.value = new Date().toISOString().split('T')[0]
|
||||
description.value = ''
|
||||
mileage.value = ''
|
||||
|
||||
// Close modal
|
||||
emit('close')
|
||||
} catch (error) {
|
||||
console.error('Error saving expense:', error)
|
||||
if (error.message === 'DRAFT_LIMIT_REACHED') {
|
||||
// Show custom modal for draft limit
|
||||
showDraftLimitModal.value = true
|
||||
} else {
|
||||
alert(`Hiba történt a mentés során: ${expenseStore.error || error.message}`)
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const closeDraftLimitModal = () => {
|
||||
showDraftLimitModal.value = false
|
||||
}
|
||||
|
||||
const openEditVehicle = () => {
|
||||
// TODO: Implement opening edit vehicle modal
|
||||
// For now, just close the draft limit modal and maybe show a message
|
||||
alert('Edit vehicle functionality to be implemented')
|
||||
closeDraftLimitModal()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Modal Backdrop -->
|
||||
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-black bg-opacity-50 p-4" @click.self="closeModal">
|
||||
<!-- Modal Container -->
|
||||
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden">
|
||||
<!-- Modal Header -->
|
||||
<div class="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<h2 class="text-xl font-bold text-gray-900">Költség / Üzemanyag hozzáadása</h2>
|
||||
<button @click="closeModal" class="text-gray-500 hover:text-gray-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="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="p-6">
|
||||
<form @submit.prevent="handleSubmit" class="space-y-5">
|
||||
<!-- Asset selection (if multiple vehicles) -->
|
||||
<div v-if="garageStore.vehicles.length > 0">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Jármű</label>
|
||||
<select
|
||||
v-model="selectedAssetId"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
|
||||
>
|
||||
<option v-for="vehicle in garageStore.vehicles" :key="vehicle.id" :value="vehicle.id">
|
||||
{{ vehicle.name || vehicle.license_plate || vehicle.vin }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-else class="text-sm text-amber-600 bg-amber-50 p-3 rounded-lg">
|
||||
Nincs még járműved. Először adj hozzá egy járművet a "Jármű Hozzáadása" gombbal.
|
||||
</div>
|
||||
|
||||
<!-- Amount -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Összeg (HUF)</label>
|
||||
<input
|
||||
v-model="amount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Category -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Kategória</label>
|
||||
<select
|
||||
v-model="category"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
|
||||
>
|
||||
<option value="fuel">Üzemanyag</option>
|
||||
<option value="service">Szerviz / Karbantartás</option>
|
||||
<option value="tax">Adó / Díj</option>
|
||||
<option value="insurance">Biztosítás</option>
|
||||
<option value="parking">Parkolás</option>
|
||||
<option value="toll">Útdíj</option>
|
||||
<option value="other">Egyéb</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Date -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Dátum</label>
|
||||
<input
|
||||
v-model="date"
|
||||
type="date"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Mileage (optional) -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Kilométeróra állása (opcionális)</label>
|
||||
<input
|
||||
v-model="mileage"
|
||||
type="number"
|
||||
min="0"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
|
||||
placeholder="pl. 123456"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Leírás (opcionális)</label>
|
||||
<textarea
|
||||
v-model="description"
|
||||
rows="3"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
|
||||
placeholder="Pl.: Tankolás, olajcsere..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
@click="closeModal"
|
||||
class="flex-1 px-4 py-3 border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-50 transition"
|
||||
>
|
||||
Mégse
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="isLoading || !selectedAssetId"
|
||||
class="flex-1 px-4 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span v-if="isLoading">Mentés...</span>
|
||||
<span v-else>Mentés</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Draft Limit Modal -->
|
||||
<div v-if="showDraftLimitModal" class="fixed inset-0 z-[200] flex items-center justify-center bg-black bg-opacity-70 p-4">
|
||||
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden">
|
||||
<div class="p-6">
|
||||
<div class="text-center mb-6">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-yellow-100 text-yellow-600 mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-900 mb-2">You have reached the limit for unregistered vehicles! 🔒</h3>
|
||||
<p class="text-gray-600 mb-6">
|
||||
Please edit this vehicle and provide the VIN (Chassis Number) or exact Catalog ID to unlock full expense tracking.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
@click="closeDraftLimitModal"
|
||||
class="flex-1 px-4 py-3 border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-50 transition"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="openEditVehicle"
|
||||
class="flex-1 px-4 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition"
|
||||
>
|
||||
Edit Vehicle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Additional custom styles if needed */
|
||||
</style>
|
||||
@@ -0,0 +1,404 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { useGarageStore } from '../../stores/garageStore'
|
||||
import { useAuthStore } from '../../stores/authStore'
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
const garageStore = useGarageStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Form fields
|
||||
const make = ref('')
|
||||
const model = ref('')
|
||||
const generation = ref('')
|
||||
const engine = ref('')
|
||||
const engineId = ref(null) // NEW: Store the engine/catalog ID
|
||||
const licensePlate = ref('')
|
||||
const year = ref('')
|
||||
const fuelType = ref('petrol')
|
||||
const isLoading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
// Catalog data
|
||||
const makes = ref([])
|
||||
const models = ref([])
|
||||
const generations = ref([])
|
||||
const engines = ref([])
|
||||
|
||||
// Loading states
|
||||
const loadingMakes = ref(false)
|
||||
const loadingModels = ref(false)
|
||||
const loadingGenerations = ref(false)
|
||||
const loadingEngines = ref(false)
|
||||
|
||||
// Search filters
|
||||
const makeSearch = ref('')
|
||||
const modelSearch = ref('')
|
||||
const generationSearch = ref('')
|
||||
const engineSearch = ref('')
|
||||
|
||||
// Filtered lists based on search
|
||||
const filteredMakes = ref([])
|
||||
const filteredModels = ref([])
|
||||
const filteredGenerations = ref([])
|
||||
const filteredEngines = ref([])
|
||||
|
||||
// Fetch makes on component mount
|
||||
onMounted(async () => {
|
||||
await fetchMakes()
|
||||
})
|
||||
|
||||
// Watch for search changes
|
||||
watch(makeSearch, (val) => {
|
||||
filteredMakes.value = makes.value.filter(m =>
|
||||
m.toLowerCase().includes(val.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
watch(modelSearch, (val) => {
|
||||
filteredModels.value = models.value.filter(m =>
|
||||
m.toLowerCase().includes(val.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
watch(generationSearch, (val) => {
|
||||
filteredGenerations.value = generations.value.filter(g =>
|
||||
g.toLowerCase().includes(val.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
watch(engineSearch, (val) => {
|
||||
filteredEngines.value = engines.value.filter(e =>
|
||||
e.variant.toLowerCase().includes(val.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
// Watch for make selection to fetch models
|
||||
watch(make, async (newMake) => {
|
||||
if (newMake) {
|
||||
await fetchModels(newMake)
|
||||
model.value = ''
|
||||
generation.value = ''
|
||||
engine.value = ''
|
||||
engineId.value = null
|
||||
generations.value = []
|
||||
engines.value = []
|
||||
} else {
|
||||
models.value = []
|
||||
generations.value = []
|
||||
engines.value = []
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for model selection to fetch generations
|
||||
watch(model, async (newModel) => {
|
||||
if (newModel && make.value) {
|
||||
await fetchGenerations(make.value, newModel)
|
||||
generation.value = ''
|
||||
engine.value = ''
|
||||
engineId.value = null
|
||||
engines.value = []
|
||||
} else {
|
||||
generations.value = []
|
||||
engines.value = []
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for generation selection to fetch engines
|
||||
watch(generation, async (newGen) => {
|
||||
if (newGen && make.value && model.value) {
|
||||
await fetchEngines(make.value, model.value, newGen)
|
||||
engine.value = ''
|
||||
engineId.value = null
|
||||
} else {
|
||||
engines.value = []
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for engine selection to auto-fill fuel type and capture ID
|
||||
watch(engine, (newEngine) => {
|
||||
if (newEngine) {
|
||||
const selectedEngine = engines.value.find(e => e.variant === newEngine)
|
||||
if (selectedEngine) {
|
||||
// Store the engine ID for API call
|
||||
engineId.value = selectedEngine.id
|
||||
// Auto-fill fuel type if available
|
||||
if (selectedEngine.fuel_type) {
|
||||
fuelType.value = selectedEngine.fuel_type.toLowerCase()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
engineId.value = null
|
||||
}
|
||||
})
|
||||
|
||||
// API calls
|
||||
async function fetchMakes() {
|
||||
loadingMakes.value = true
|
||||
try {
|
||||
const token = authStore.token
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'
|
||||
const response = await fetch(`${apiBaseUrl}/catalog/makes`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
if (!response.ok) throw new Error(`Failed to fetch makes: ${response.status}`)
|
||||
makes.value = await response.json()
|
||||
filteredMakes.value = makes.value
|
||||
} catch (err) {
|
||||
console.error('Error fetching makes:', err)
|
||||
error.value = 'Could not load makes. Please try again.'
|
||||
} finally {
|
||||
loadingMakes.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchModels(makeName) {
|
||||
loadingModels.value = true
|
||||
try {
|
||||
const token = authStore.token
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'
|
||||
const response = await fetch(`${apiBaseUrl}/catalog/models?make=${encodeURIComponent(makeName)}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
if (!response.ok) throw new Error(`Failed to fetch models: ${response.status}`)
|
||||
models.value = await response.json()
|
||||
filteredModels.value = models.value
|
||||
} catch (err) {
|
||||
console.error('Error fetching models:', err)
|
||||
error.value = 'Could not load models for this make.'
|
||||
} finally {
|
||||
loadingModels.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchGenerations(makeName, modelName) {
|
||||
loadingGenerations.value = true
|
||||
try {
|
||||
const token = authStore.token
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'
|
||||
const response = await fetch(`${apiBaseUrl}/catalog/generations?make=${encodeURIComponent(makeName)}&model=${encodeURIComponent(modelName)}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
if (!response.ok) throw new Error(`Failed to fetch generations: ${response.status}`)
|
||||
generations.value = await response.json()
|
||||
filteredGenerations.value = generations.value
|
||||
} catch (err) {
|
||||
console.error('Error fetching generations:', err)
|
||||
error.value = 'Could not load generations for this model.'
|
||||
} finally {
|
||||
loadingGenerations.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEngines(makeName, modelName, genName) {
|
||||
loadingEngines.value = true
|
||||
try {
|
||||
const token = authStore.token
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'
|
||||
const response = await fetch(`${apiBaseUrl}/catalog/engines?make=${encodeURIComponent(makeName)}&model=${encodeURIComponent(modelName)}&gen=${encodeURIComponent(genName)}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
if (!response.ok) throw new Error(`Failed to fetch engines: ${response.status}`)
|
||||
const engineData = await response.json()
|
||||
engines.value = engineData.map(e => ({
|
||||
variant: e.variant,
|
||||
fuel_type: e.fuel_type,
|
||||
id: e.id
|
||||
}))
|
||||
filteredEngines.value = engines.value
|
||||
} catch (err) {
|
||||
console.error('Error fetching engines:', err)
|
||||
error.value = 'Could not load engines for this generation.'
|
||||
} finally {
|
||||
loadingEngines.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
error.value = null
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
// Prepare vehicle data for the API
|
||||
const vehicleData = {
|
||||
make: make.value,
|
||||
model: model.value,
|
||||
licensePlate: licensePlate.value,
|
||||
year: parseInt(year.value) || new Date().getFullYear(),
|
||||
fuelType: fuelType.value,
|
||||
generation: generation.value,
|
||||
engine: engine.value,
|
||||
// Include the catalog ID (engine ID) for API
|
||||
catalogId: engineId.value,
|
||||
// Include organization ID (use active organization ID)
|
||||
organizationId: authStore.activeOrgId
|
||||
}
|
||||
|
||||
// Call the garage store to add vehicle
|
||||
await garageStore.addVehicle(vehicleData)
|
||||
|
||||
// Show success message
|
||||
alert('Sikeres mentés! Jármű hozzáadva.')
|
||||
|
||||
// Reset form
|
||||
make.value = ''
|
||||
model.value = ''
|
||||
generation.value = ''
|
||||
engine.value = ''
|
||||
engineId.value = null
|
||||
licensePlate.value = ''
|
||||
year.value = ''
|
||||
fuelType.value = 'petrol'
|
||||
makes.value = []
|
||||
models.value = []
|
||||
generations.value = []
|
||||
engines.value = []
|
||||
|
||||
// Close modal
|
||||
emit('close')
|
||||
} catch (err) {
|
||||
console.error('Error adding vehicle:', err)
|
||||
error.value = err.message || 'Ismeretlen hiba történt'
|
||||
alert(`Hiba: ${error.value}`)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Modal Backdrop -->
|
||||
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-black bg-opacity-50 p-4" @click.self="closeModal">
|
||||
<!-- Modal Container -->
|
||||
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden">
|
||||
<!-- Modal Header -->
|
||||
<div class="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<h2 class="text-xl font-bold text-gray-900">Jármű hozzáadása</h2>
|
||||
<button @click="closeModal" class="text-gray-500 hover:text-gray-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="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="p-6">
|
||||
<form @submit.prevent="handleSubmit" class="space-y-5">
|
||||
<!-- Make -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Gyártó</label>
|
||||
<input
|
||||
v-model="make"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition text-blue-900 placeholder-blue-700"
|
||||
placeholder="Pl.: Toyota"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Model -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Modell</label>
|
||||
<input
|
||||
v-model="model"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition text-blue-900 placeholder-blue-700"
|
||||
placeholder="Pl.: Corolla"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- License Plate -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Rendszám</label>
|
||||
<input
|
||||
v-model="licensePlate"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition text-blue-900 placeholder-blue-700"
|
||||
placeholder="Pl.: ABC-123"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Year and Fuel Type in a grid -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- Year -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Évjárat</label>
|
||||
<input
|
||||
v-model="year"
|
||||
type="number"
|
||||
min="1900"
|
||||
:max="new Date().getFullYear() + 1"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition text-blue-900 placeholder-blue-700"
|
||||
placeholder="2023"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Fuel Type -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Üzemanyag típus</label>
|
||||
<select
|
||||
v-model="fuelType"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition text-blue-900"
|
||||
>
|
||||
<option value="petrol">Benzin</option>
|
||||
<option value="diesel">Dízel</option>
|
||||
<option value="electric">Elektromos</option>
|
||||
<option value="hybrid">Hibrid</option>
|
||||
<option value="lpg">LPG</option>
|
||||
<option value="other">Egyéb</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div v-if="error" class="p-3 bg-red-50 border border-red-200 text-red-700 rounded-lg">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
@click="closeModal"
|
||||
:disabled="isLoading"
|
||||
class="flex-1 px-4 py-3 border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-50 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Mégse
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="isLoading"
|
||||
class="flex-1 px-4 py-3 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
|
||||
>
|
||||
<span v-if="isLoading" class="inline-block h-4 w-4 animate-spin rounded-full border-2 border-solid border-white border-r-transparent mr-2"></span>
|
||||
{{ isLoading ? 'Feldolgozás...' : 'Jármű hozzáadása' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Additional custom styles if needed */
|
||||
</style>
|
||||
@@ -0,0 +1,135 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const serviceType = ref('maintenance')
|
||||
const location = ref('')
|
||||
const urgency = ref('medium')
|
||||
|
||||
const handleSubmit = () => {
|
||||
// In a real app, you would call an API here
|
||||
console.log('Service search submitted:', {
|
||||
serviceType: serviceType.value,
|
||||
location: location.value,
|
||||
urgency: urgency.value
|
||||
})
|
||||
|
||||
// Show success message
|
||||
alert('Szerviz keresés elindítva! Hamarosan értesítünk.')
|
||||
|
||||
// Reset form
|
||||
serviceType.value = 'maintenance'
|
||||
location.value = ''
|
||||
urgency.value = 'medium'
|
||||
|
||||
// Close modal
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Modal Backdrop -->
|
||||
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-black bg-opacity-50 p-4" @click.self="closeModal">
|
||||
<!-- Modal Container -->
|
||||
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden">
|
||||
<!-- Modal Header -->
|
||||
<div class="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<h2 class="text-xl font-bold text-gray-900">Szerviz Keresése</h2>
|
||||
<button @click="closeModal" class="text-gray-500 hover:text-gray-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="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="p-6">
|
||||
<form @submit.prevent="handleSubmit" class="space-y-5">
|
||||
<!-- Service Type -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Szerviz típusa</label>
|
||||
<select
|
||||
v-model="serviceType"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
|
||||
>
|
||||
<option value="maintenance">Általános karbantartás</option>
|
||||
<option value="repair">Javítás</option>
|
||||
<option value="diagnostic">Diagnosztika</option>
|
||||
<option value="tire">Gumiszerviz</option>
|
||||
<option value="oil">Olajcsere</option>
|
||||
<option value="brake">Fékrendszer</option>
|
||||
<option value="other">Egyéb</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Location -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Helyszín (város/irányítószám)</label>
|
||||
<input
|
||||
v-model="location"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
|
||||
placeholder="Pl.: Budapest"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Urgency -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Sürgősség</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center">
|
||||
<input v-model="urgency" type="radio" value="low" class="mr-2">
|
||||
<span class="text-gray-700">Alacsony</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input v-model="urgency" type="radio" value="medium" class="mr-2">
|
||||
<span class="text-gray-700">Közepes</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input v-model="urgency" type="radio" value="high" class="mr-2">
|
||||
<span class="text-gray-700">Magas</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Részletes leírás (opcionális)</label>
|
||||
<textarea
|
||||
rows="3"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
|
||||
placeholder="Pl.: Motorhiba, féknyikorgás..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
@click="closeModal"
|
||||
class="flex-1 px-4 py-3 border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-50 transition"
|
||||
>
|
||||
Mégse
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="flex-1 px-4 py-3 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition"
|
||||
>
|
||||
Szerviz keresése
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Additional custom styles if needed */
|
||||
</style>
|
||||
@@ -0,0 +1,115 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import AddExpenseModal from './AddExpenseModal.vue'
|
||||
import SmartVehicleRegistration from './SmartVehicleRegistration.vue'
|
||||
import FindServiceModal from './FindServiceModal.vue'
|
||||
|
||||
const isOpen = ref(false)
|
||||
const showExpenseModal = ref(false)
|
||||
const showSmartVehicleRegistration = ref(false)
|
||||
const showServiceModal = ref(false)
|
||||
|
||||
const toggleMenu = () => {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
|
||||
const openExpenseModal = () => {
|
||||
showExpenseModal.value = true
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
const openSmartVehicleRegistration = () => {
|
||||
showSmartVehicleRegistration.value = true
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
const openServiceModal = () => {
|
||||
showServiceModal.value = true
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
const closeExpenseModal = () => {
|
||||
showExpenseModal.value = false
|
||||
}
|
||||
|
||||
const closeSmartVehicleRegistration = () => {
|
||||
showSmartVehicleRegistration.value = false
|
||||
}
|
||||
|
||||
const closeServiceModal = () => {
|
||||
showServiceModal.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Floating Action Button -->
|
||||
<div class="fixed bottom-6 right-6 z-50 flex flex-col items-end">
|
||||
<!-- Action Menu (shown when open) -->
|
||||
<div v-if="isOpen" class="mb-4 space-y-3">
|
||||
<!-- Add Expense Button -->
|
||||
<button
|
||||
@click="openExpenseModal"
|
||||
class="flex items-center justify-end gap-3 bg-blue-600 hover:bg-blue-700 text-white px-4 py-3 rounded-full shadow-lg transition-all duration-200 transform hover:scale-105"
|
||||
>
|
||||
<span class="text-sm font-semibold">Költség / Üzemanyag</span>
|
||||
<div class="w-10 h-10 flex items-center justify-center bg-blue-800 rounded-full">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Find Service Button -->
|
||||
<button
|
||||
@click="openServiceModal"
|
||||
class="flex items-center justify-end gap-3 bg-green-600 hover:bg-green-700 text-white px-4 py-3 rounded-full shadow-lg transition-all duration-200 transform hover:scale-105"
|
||||
>
|
||||
<span class="text-sm font-semibold">Szerviz Keresése</span>
|
||||
<div class="w-10 h-10 flex items-center justify-center bg-green-800 rounded-full">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Add Vehicle Button -->
|
||||
<button
|
||||
@click="openSmartVehicleRegistration"
|
||||
class="flex items-center justify-end gap-3 bg-purple-600 hover:bg-purple-700 text-white px-4 py-3 rounded-full shadow-lg transition-all duration-200 transform hover:scale-105"
|
||||
>
|
||||
<span class="text-sm font-semibold">Jármű Hozzáadása</span>
|
||||
<div class="w-10 h-10 flex items-center justify-center bg-purple-800 rounded-full">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Main FAB Button -->
|
||||
<button
|
||||
@click="toggleMenu"
|
||||
class="w-14 h-14 flex items-center justify-center bg-blue-700 hover:bg-blue-800 text-white rounded-full shadow-xl transition-all duration-200 transform hover:scale-110"
|
||||
:class="{ 'rotate-45': isOpen }"
|
||||
>
|
||||
<svg v-if="!isOpen" xmlns="http://www.w3.org/2000/svg" class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-7 w-7" 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>
|
||||
|
||||
<!-- Modals -->
|
||||
<AddExpenseModal v-if="showExpenseModal" @close="closeExpenseModal" />
|
||||
<SmartVehicleRegistration v-if="showSmartVehicleRegistration" @close="closeSmartVehicleRegistration" />
|
||||
<FindServiceModal v-if="showServiceModal" @close="closeServiceModal" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Smooth transitions */
|
||||
button {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div class="analytics-dashboard">
|
||||
<!-- Header with Mode Toggle -->
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 p-6 bg-gradient-to-r from-gray-50 to-white rounded-2xl shadow-sm border border-gray-200">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">Vehicle Analytics & TCO Dashboard</h1>
|
||||
<p class="text-gray-600 mt-2">
|
||||
{{ isPrivateGarage ? 'Personal driving insights and fun achievements' : 'Corporate fleet performance and cost optimization' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 md:mt-0">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-3 text-sm font-medium text-gray-700">View Mode:</span>
|
||||
<div class="relative inline-block w-64">
|
||||
<div class="bg-gray-100 rounded-xl p-1 flex">
|
||||
<button
|
||||
@click="setMode('private_garage')"
|
||||
:class="[
|
||||
'flex-1 py-3 px-4 rounded-lg text-sm font-medium transition-all duration-200',
|
||||
isPrivateGarage
|
||||
? 'bg-white shadow text-gray-900'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="mr-2">🎮</span>
|
||||
<span>Fun Stats</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
@click="setMode('corporate_fleet')"
|
||||
:class="[
|
||||
'flex-1 py-3 px-4 rounded-lg text-sm font-medium transition-all duration-200',
|
||||
isCorporateFleet
|
||||
? 'bg-white shadow text-gray-900'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="mr-2">📊</span>
|
||||
<span>Business BI</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="toggleMode"
|
||||
class="px-4 py-3 bg-gradient-to-r from-blue-500 to-indigo-600 text-white rounded-xl font-medium hover:from-blue-600 hover:to-indigo-700 transition-all duration-200 shadow-md hover:shadow-lg flex items-center"
|
||||
>
|
||||
<span class="mr-2">🔄</span>
|
||||
Switch to {{ isPrivateGarage ? 'Business' : 'Fun' }} View
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-sm text-gray-500 flex items-center">
|
||||
<div class="w-3 h-3 rounded-full bg-green-500 mr-2"></div>
|
||||
<span>Live data updated {{ lastUpdated }}</span>
|
||||
<button @click="refreshData" class="ml-4 text-blue-600 hover:text-blue-800 flex items-center">
|
||||
<span class="mr-1">↻</span>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode Indicator -->
|
||||
<div class="mb-6">
|
||||
<div v-if="isPrivateGarage" class="inline-flex items-center px-4 py-2 rounded-full bg-gradient-to-r from-blue-100 to-indigo-100 text-blue-800">
|
||||
<span class="mr-2">🎯</span>
|
||||
<span class="font-medium">Private Garage Mode</span>
|
||||
<span class="ml-2 text-sm">Personal insights and achievements</span>
|
||||
</div>
|
||||
<div v-else class="inline-flex items-center px-4 py-2 rounded-full bg-gradient-to-r from-green-100 to-emerald-100 text-green-800">
|
||||
<span class="mr-2">🏢</span>
|
||||
<span class="font-medium">Corporate Fleet Mode</span>
|
||||
<span class="ml-2 text-sm">Business intelligence and TCO analysis</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Component -->
|
||||
<div class="mt-6">
|
||||
<component :is="currentComponent" />
|
||||
</div>
|
||||
|
||||
<!-- Footer Notes -->
|
||||
<div class="mt-12 pt-6 border-t border-gray-200">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="bg-gray-50 p-4 rounded-xl">
|
||||
<h4 class="font-semibold text-gray-800 mb-2">📈 Data Sources</h4>
|
||||
<p class="text-sm text-gray-600">Vehicle telemetry, fuel receipts, maintenance records, and insurance data aggregated in real-time.</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-4 rounded-xl">
|
||||
<h4 class="font-semibold text-gray-800 mb-2">🎯 Key Metrics</h4>
|
||||
<p class="text-sm text-gray-600">TCO (Total Cost of Ownership), Cost per km, Fuel efficiency, Utilization rate, and Environmental impact.</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-4 rounded-xl">
|
||||
<h4 class="font-semibold text-gray-800 mb-2">🔄 Auto-Sync</h4>
|
||||
<p class="text-sm text-gray-600">Data updates every 24 hours. Manual refresh available. Historical data retained for 36 months.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, shallowRef } from 'vue'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import FunStats from './FunStats.vue'
|
||||
import BusinessBI from './BusinessBI.vue'
|
||||
|
||||
const appModeStore = useAppModeStore()
|
||||
const { mode, isPrivateGarage, isCorporateFleet, toggleMode, setMode } = appModeStore
|
||||
|
||||
const lastUpdated = ref('just now')
|
||||
const isLoading = ref(false)
|
||||
|
||||
const currentComponent = shallowRef(FunStats)
|
||||
|
||||
// Watch mode changes and update component
|
||||
import { watch } from 'vue'
|
||||
watch(() => mode.value, (newMode) => {
|
||||
if (newMode === 'private_garage') {
|
||||
currentComponent.value = FunStats
|
||||
} else {
|
||||
currentComponent.value = BusinessBI
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
const refreshData = () => {
|
||||
isLoading.value = true
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
const now = new Date()
|
||||
lastUpdated.value = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
isLoading.value = false
|
||||
|
||||
// Show success notification
|
||||
const event = new CustomEvent('show-toast', {
|
||||
detail: {
|
||||
message: 'Analytics data refreshed successfully',
|
||||
type: 'success'
|
||||
}
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
}, 800)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.analytics-dashboard {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Smooth transitions for mode switching */
|
||||
.component-enter-active,
|
||||
.component-leave-active {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.component-enter-from,
|
||||
.component-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,385 @@
|
||||
<template>
|
||||
<div class="business-bi">
|
||||
<h2 class="text-2xl font-bold text-gray-800 mb-6">📊 Business Intelligence Dashboard</h2>
|
||||
|
||||
<!-- Key Metrics Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-white rounded-xl p-6 shadow border border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Fleet Size</p>
|
||||
<p class="text-3xl font-bold text-gray-800">{{ businessMetrics.fleetSize }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<span class="text-2xl text-blue-600">🚗</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mt-2">Active vehicles</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-6 shadow border border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Total Monthly Cost</p>
|
||||
<p class="text-3xl font-bold text-gray-800">€{{ formatNumber(businessMetrics.totalMonthlyCost) }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center">
|
||||
<span class="text-2xl text-green-600">💰</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mt-2">All expenses combined</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-6 shadow border border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Avg Cost per Km</p>
|
||||
<p class="text-3xl font-bold text-gray-800">€{{ businessMetrics.averageCostPerKm.toFixed(2) }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||
<span class="text-2xl text-purple-600">📈</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mt-2">Operating efficiency</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-6 shadow border border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Utilization Rate</p>
|
||||
<p class="text-3xl font-bold text-gray-800">{{ businessMetrics.utilizationRate }}%</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-amber-100 rounded-lg flex items-center justify-center">
|
||||
<span class="text-2xl text-amber-600">⚡</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mt-2">Fleet activity</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts Section -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||
<!-- Monthly Costs Chart -->
|
||||
<div class="bg-white rounded-xl p-6 shadow border border-gray-200">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">Monthly Cost Breakdown (Last 6 Months)</h3>
|
||||
<div class="h-80">
|
||||
<canvas ref="monthlyCostsChart"></canvas>
|
||||
</div>
|
||||
<div class="mt-4 text-sm text-gray-500">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center">
|
||||
<div class="w-3 h-3 bg-blue-500 rounded-full mr-2"></div>
|
||||
<span>Maintenance</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-3 h-3 bg-green-500 rounded-full mr-2"></div>
|
||||
<span>Fuel</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-3 h-3 bg-amber-500 rounded-full mr-2"></div>
|
||||
<span>Insurance</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fuel Efficiency Trend Chart -->
|
||||
<div class="bg-white rounded-xl p-6 shadow border border-gray-200">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">Fuel Efficiency Trend (km per liter)</h3>
|
||||
<div class="h-80">
|
||||
<canvas ref="fuelEfficiencyChart"></canvas>
|
||||
</div>
|
||||
<div class="mt-4 text-sm text-gray-500">
|
||||
<p>Average: <span class="font-semibold">{{ averageFuelEfficiency.toFixed(1) }} km/L</span></p>
|
||||
<p class="text-green-600">↑ {{ ((averageFuelEfficiency - 12) / 12 * 100).toFixed(1) }}% improvement vs industry average</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cost per Km and TCO Analysis -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||
<!-- Cost per Km Chart -->
|
||||
<div class="bg-white rounded-xl p-6 shadow border border-gray-200">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">Cost per Kilometer Trend</h3>
|
||||
<div class="h-64">
|
||||
<canvas ref="costPerKmChart"></canvas>
|
||||
</div>
|
||||
<div class="mt-4 text-sm text-gray-500">
|
||||
<p>Average cost: <span class="font-semibold">€{{ averageCostPerKm.toFixed(2) }}/km</span></p>
|
||||
<p>Target: <span class="font-semibold">€0.38/km</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TCO Breakdown -->
|
||||
<div class="bg-white rounded-xl p-6 shadow border border-gray-200">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">Total Cost of Ownership (TCO) Breakdown</h3>
|
||||
<div class="h-64">
|
||||
<canvas ref="tcoChart"></canvas>
|
||||
</div>
|
||||
<div class="mt-4 text-sm text-gray-500">
|
||||
<p>Annual TCO: <span class="font-semibold">€{{ formatNumber(businessMetrics.totalMonthlyCost * 12) }}</span></p>
|
||||
<p>Per vehicle: <span class="font-semibold">€{{ formatNumber(Math.round(businessMetrics.totalMonthlyCost * 12 / businessMetrics.fleetSize)) }}/year</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
<div class="bg-white rounded-xl p-6 shadow border border-gray-200">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">Monthly Performance Details</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr class="bg-gray-50">
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Month</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Maintenance</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Fuel</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Insurance</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cost/km</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Efficiency</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tr v-for="(month, index) in monthlyCosts" :key="month.month">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ month.month }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">€{{ month.maintenance }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">€{{ month.fuel }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">€{{ month.insurance }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-semibold text-gray-900">€{{ month.total }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">€{{ costPerKmTrends[index]?.cost.toFixed(2) || '0.00' }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ fuelEfficiencyTrends[index]?.efficiency.toFixed(1) || '0.0' }} km/L</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Insights Panel -->
|
||||
<div class="mt-8 bg-gradient-to-r from-gray-800 to-gray-900 rounded-2xl p-6 text-white shadow-lg">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-full flex items-center justify-center mr-4">
|
||||
<span class="text-xl">💡</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold">Business Insights</h3>
|
||||
<p class="text-gray-300">AI-powered recommendations</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
|
||||
<div class="bg-white/10 p-4 rounded-xl">
|
||||
<h4 class="font-semibold mb-2">💰 Cost Optimization</h4>
|
||||
<p class="text-sm text-gray-200">Maintenance costs are {{ getCostComparison() }} than industry average. Consider preventive maintenance scheduling to reduce unexpected repairs.</p>
|
||||
</div>
|
||||
<div class="bg-white/10 p-4 rounded-xl">
|
||||
<h4 class="font-semibold mb-2">⛽ Fuel Efficiency</h4>
|
||||
<p class="text-sm text-gray-200">Your fleet is {{ getEfficiencyComparison() }} efficient than benchmark. Continue driver training programs for optimal performance.</p>
|
||||
</div>
|
||||
<div class="bg-white/10 p-4 rounded-xl">
|
||||
<h4 class="font-semibold mb-2">📅 Utilization Rate</h4>
|
||||
<p class="text-sm text-gray-200">{{ businessMetrics.utilizationRate }}% utilization is good. Consider dynamic routing to increase to 85% target.</p>
|
||||
</div>
|
||||
<div class="bg-white/10 p-4 rounded-xl">
|
||||
<h4 class="font-semibold mb-2">🔧 Downtime Management</h4>
|
||||
<p class="text-sm text-gray-200">{{ businessMetrics.downtimeHours }} hours/month downtime. Predictive maintenance could reduce this by 30%.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { useAnalyticsStore } from '@/stores/analyticsStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { Chart, registerables } from 'chart.js'
|
||||
|
||||
Chart.register(...registerables)
|
||||
|
||||
const analyticsStore = useAnalyticsStore()
|
||||
const { monthlyCosts, fuelEfficiencyTrends, costPerKmTrends, businessMetrics, averageFuelEfficiency, averageCostPerKm } = storeToRefs(analyticsStore)
|
||||
|
||||
const monthlyCostsChart = ref(null)
|
||||
const fuelEfficiencyChart = ref(null)
|
||||
const costPerKmChart = ref(null)
|
||||
const tcoChart = ref(null)
|
||||
|
||||
let chartInstances = []
|
||||
|
||||
const formatNumber = (num) => {
|
||||
return new Intl.NumberFormat('en-US').format(num)
|
||||
}
|
||||
|
||||
const getCostComparison = () => {
|
||||
const avgMaintenance = monthlyCosts.value.reduce((sum, month) => sum + month.maintenance, 0) / monthlyCosts.value.length
|
||||
return avgMaintenance > 500 ? 'higher' : avgMaintenance < 400 ? 'lower' : 'similar'
|
||||
}
|
||||
|
||||
const getEfficiencyComparison = () => {
|
||||
return averageFuelEfficiency.value > 13 ? 'more' : averageFuelEfficiency.value < 12 ? 'less' : 'equally'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Monthly Costs Chart (Stacked Bar)
|
||||
if (monthlyCostsChart.value) {
|
||||
const ctx = monthlyCostsChart.value.getContext('2d')
|
||||
const chart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: monthlyCosts.value.map(m => m.month),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Maintenance',
|
||||
data: monthlyCosts.value.map(m => m.maintenance),
|
||||
backgroundColor: '#3b82f6',
|
||||
stack: 'Stack 0',
|
||||
},
|
||||
{
|
||||
label: 'Fuel',
|
||||
data: monthlyCosts.value.map(m => m.fuel),
|
||||
backgroundColor: '#10b981',
|
||||
stack: 'Stack 0',
|
||||
},
|
||||
{
|
||||
label: 'Insurance',
|
||||
data: monthlyCosts.value.map(m => m.insurance),
|
||||
backgroundColor: '#f59e0b',
|
||||
stack: 'Stack 0',
|
||||
},
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Cost (€)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
chartInstances.push(chart)
|
||||
}
|
||||
|
||||
// Fuel Efficiency Chart (Line)
|
||||
if (fuelEfficiencyChart.value) {
|
||||
const ctx = fuelEfficiencyChart.value.getContext('2d')
|
||||
const chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: fuelEfficiencyTrends.value.map(m => m.month),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Fuel Efficiency (km/L)',
|
||||
data: fuelEfficiencyTrends.value.map(m => m.efficiency),
|
||||
borderColor: '#8b5cf6',
|
||||
backgroundColor: 'rgba(139, 92, 246, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: false,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'km per liter'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
chartInstances.push(chart)
|
||||
}
|
||||
|
||||
// Cost per Km Chart (Line)
|
||||
if (costPerKmChart.value) {
|
||||
const ctx = costPerKmChart.value.getContext('2d')
|
||||
const chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: costPerKmTrends.value.map(m => m.month),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Cost per Kilometer (€)',
|
||||
data: costPerKmTrends.value.map(m => m.cost),
|
||||
borderColor: '#ef4444',
|
||||
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: false,
|
||||
title: {
|
||||
display: true,
|
||||
text: '€ per km'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
chartInstances.push(chart)
|
||||
}
|
||||
|
||||
// TCO Chart (Doughnut)
|
||||
if (tcoChart.value) {
|
||||
const ctx = tcoChart.value.getContext('2d')
|
||||
const totalMaintenance = monthlyCosts.value.reduce((sum, month) => sum + month.maintenance, 0)
|
||||
const totalFuel = monthlyCosts.value.reduce((sum, month) => sum + month.fuel, 0)
|
||||
const totalInsurance = monthlyCosts.value.reduce((sum, month) => sum + month.insurance, 0)
|
||||
|
||||
const chart = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Maintenance', 'Fuel', 'Insurance'],
|
||||
datasets: [
|
||||
{
|
||||
data: [totalMaintenance, totalFuel, totalInsurance],
|
||||
backgroundColor: ['#3b82f6', '#10b981', '#f59e0b'],
|
||||
borderWidth: 2,
|
||||
borderColor: '#ffffff',
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
chartInstances.push(chart)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
chartInstances.forEach(chart => chart.destroy())
|
||||
chartInstances = []
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.business-bi {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div class="fun-stats">
|
||||
<h2 class="text-2xl font-bold text-gray-800 mb-6">🎮 Fun Stats & Achievements</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<!-- Moon Trip Card -->
|
||||
<div class="bg-gradient-to-br from-blue-50 to-indigo-100 rounded-2xl p-6 shadow-lg border border-blue-200">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center mr-4">
|
||||
<span class="text-2xl">🌙</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-800">Moon Trip</h3>
|
||||
<p class="text-sm text-gray-600">Distance traveled</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center py-4">
|
||||
<div class="text-4xl font-bold text-blue-700">{{ funFacts.moonTrips }}</div>
|
||||
<p class="text-gray-600 mt-2">trips to the Moon</p>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 text-center">
|
||||
You've driven <span class="font-semibold">{{ formatNumber(funFacts.totalKmDriven) }} km</span> - that's {{ funFacts.moonTrips }} trip{{ funFacts.moonTrips !== 1 ? 's' : '' }} to the Moon!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Earth Circuits Card -->
|
||||
<div class="bg-gradient-to-br from-green-50 to-emerald-100 rounded-2xl p-6 shadow-lg border border-green-200">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center mr-4">
|
||||
<span class="text-2xl">🌍</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-800">Earth Circuits</h3>
|
||||
<p class="text-sm text-gray-600">Around the world</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center py-4">
|
||||
<div class="text-4xl font-bold text-green-700">{{ funFacts.earthCircuits }}</div>
|
||||
<p class="text-gray-600 mt-2">times around Earth</p>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 text-center">
|
||||
Equivalent to {{ funFacts.earthCircuits }} circuit{{ funFacts.earthCircuits !== 1 ? 's' : '' }} around the equator!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Trees Saved Card -->
|
||||
<div class="bg-gradient-to-br from-amber-50 to-orange-100 rounded-2xl p-6 shadow-lg border border-amber-200">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center mr-4">
|
||||
<span class="text-2xl">🌳</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-800">Trees Saved</h3>
|
||||
<p class="text-sm text-gray-600">Environmental impact</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center py-4">
|
||||
<div class="text-4xl font-bold text-amber-700">{{ funFacts.totalTreesSaved }}</div>
|
||||
<p class="text-gray-600 mt-2">trees preserved</p>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 text-center">
|
||||
Your efficient driving saved {{ funFacts.totalTreesSaved }} tree{{ funFacts.totalTreesSaved !== 1 ? 's' : '' }} from CO₂ emissions!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- CO₂ Saved Card -->
|
||||
<div class="bg-gradient-to-br from-purple-50 to-pink-100 rounded-2xl p-6 shadow-lg border border-purple-200">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center mr-4">
|
||||
<span class="text-2xl">☁️</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-800">CO₂ Saved</h3>
|
||||
<p class="text-sm text-gray-600">Carbon footprint</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center py-4">
|
||||
<div class="text-4xl font-bold text-purple-700">{{ funFacts.totalCo2Saved }}</div>
|
||||
<p class="text-gray-600 mt-2">tons of CO₂</p>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 text-center">
|
||||
That's like taking {{ Math.round(funFacts.totalCo2Saved * 1.8) }} cars off the road for a year!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Money Saved Card -->
|
||||
<div class="bg-gradient-to-br from-cyan-50 to-teal-100 rounded-2xl p-6 shadow-lg border border-cyan-200">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-12 h-12 bg-cyan-100 rounded-xl flex items-center justify-center mr-4">
|
||||
<span class="text-2xl">💰</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-800">Money Saved</h3>
|
||||
<p class="text-sm text-gray-600">Smart driving pays off</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center py-4">
|
||||
<div class="text-4xl font-bold text-cyan-700">€{{ formatNumber(funFacts.totalMoneySaved) }}</div>
|
||||
<p class="text-gray-600 mt-2">total savings</p>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 text-center">
|
||||
Compared to average drivers, you saved €{{ formatNumber(funFacts.totalMoneySaved) }}!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Fuel Efficiency Card -->
|
||||
<div class="bg-gradient-to-br from-rose-50 to-red-100 rounded-2xl p-6 shadow-lg border border-rose-200">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-12 h-12 bg-rose-100 rounded-xl flex items-center justify-center mr-4">
|
||||
<span class="text-2xl">⛽</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-800">Fuel Efficiency</h3>
|
||||
<p class="text-sm text-gray-600">Your average</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center py-4">
|
||||
<div class="text-4xl font-bold text-rose-700">{{ averageFuelEfficiency.toFixed(1) }}</div>
|
||||
<p class="text-gray-600 mt-2">km per liter</p>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 text-center">
|
||||
{{ getEfficiencyMessage(averageFuelEfficiency) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fun Fact of the Day -->
|
||||
<div class="mt-8 bg-gradient-to-r from-indigo-500 to-purple-600 rounded-2xl p-6 text-white shadow-lg">
|
||||
<div class="flex items-center">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-full flex items-center justify-center mr-4">
|
||||
<span class="text-xl">💡</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold">Fun Fact of the Day</h3>
|
||||
<p class="text-indigo-100">Did you know?</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-4 text-lg">
|
||||
If every driver in your city achieved your fuel efficiency, we'd save enough CO₂ to fill {{ Math.round(funFacts.totalCo2Saved * 100) }} hot air balloons every year!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAnalyticsStore } from '@/stores/analyticsStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const analyticsStore = useAnalyticsStore()
|
||||
const { funFacts, averageFuelEfficiency } = storeToRefs(analyticsStore)
|
||||
|
||||
const formatNumber = (num) => {
|
||||
return new Intl.NumberFormat('en-US').format(num)
|
||||
}
|
||||
|
||||
const getEfficiencyMessage = (efficiency) => {
|
||||
if (efficiency > 15) return "Outstanding! You're among the top 5% most efficient drivers."
|
||||
if (efficiency > 12) return "Great job! You're more efficient than 80% of drivers."
|
||||
if (efficiency > 10) return "Good! You're above average in fuel efficiency."
|
||||
return 'Room for improvement. Check our tips to save more fuel.'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fun-stats {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div class="achievement-showcase">
|
||||
<!-- Mode indicator -->
|
||||
<div class="mode-indicator mb-8 p-4 rounded-xl bg-gradient-to-r from-slate-50 to-gray-100 border border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="w-10 h-10 rounded-full flex items-center justify-center mr-4"
|
||||
:class="isPrivateGarage ? 'bg-amber-100 text-amber-700' : 'bg-emerald-100 text-emerald-700'"
|
||||
>
|
||||
{{ isPrivateGarage ? '🏆' : '🏅' }}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-lg">
|
||||
{{ isPrivateGarage ? 'Private Garage Trophy Showcase' : 'Corporate Fleet Badge Board' }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ isPrivateGarage
|
||||
? 'Playful trophies for personal achievements'
|
||||
: 'Professional badges for fleet optimization'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="text-sm text-gray-500">
|
||||
Current mode:
|
||||
<span class="font-semibold" :class="isPrivateGarage ? 'text-amber-700' : 'text-emerald-700'">
|
||||
{{ isPrivateGarage ? 'Private Garage' : 'Corporate Fleet' }}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
@click="toggleMode"
|
||||
class="px-4 py-2 text-sm font-medium rounded-lg border transition-colors"
|
||||
:class="isPrivateGarage
|
||||
? 'border-amber-300 text-amber-700 bg-amber-50 hover:bg-amber-100'
|
||||
: 'border-emerald-300 text-emerald-700 bg-emerald-50 hover:bg-emerald-100'"
|
||||
>
|
||||
Switch to {{ isPrivateGarage ? 'Corporate' : 'Private' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic component rendering -->
|
||||
<div class="component-container">
|
||||
<TrophyCabinet v-if="isPrivateGarage" />
|
||||
<BadgeBoard v-else />
|
||||
</div>
|
||||
|
||||
<!-- Gamification stats -->
|
||||
<div class="mt-10 grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="p-5 rounded-xl bg-white border border-gray-200 shadow-sm">
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 text-xl mr-4">
|
||||
📈
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-gray-900">{{ earnedCount }}</div>
|
||||
<div class="text-sm text-gray-600">Achievements Earned</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 rounded-xl bg-white border border-gray-200 shadow-sm">
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 rounded-full bg-purple-100 flex items-center justify-center text-purple-600 text-xl mr-4">
|
||||
🎯
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-gray-900">{{ progressPercentage }}%</div>
|
||||
<div class="text-sm text-gray-600">Overall Progress</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 rounded-xl bg-white border border-gray-200 shadow-sm">
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 rounded-full bg-green-100 flex items-center justify-center text-green-600 text-xl mr-4">
|
||||
⭐
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-gray-900">{{ nextAchievement }}</div>
|
||||
<div class="text-sm text-gray-600">Next Achievement</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help text -->
|
||||
<div class="mt-8 p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div class="flex items-start">
|
||||
<div class="text-gray-500 mr-3">💡</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
<span class="font-semibold">How to earn more:</span>
|
||||
{{ isPrivateGarage
|
||||
? 'Add vehicles, log expenses, complete daily quizzes, and find services to unlock trophies.'
|
||||
: 'Optimize fleet efficiency, reduce costs, manage multiple vehicles, and maintain service records to earn badges.'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { useGamificationStore } from '@/stores/gamificationStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import TrophyCabinet from './TrophyCabinet.vue'
|
||||
import BadgeBoard from './BadgeBoard.vue'
|
||||
|
||||
const appModeStore = useAppModeStore()
|
||||
const gamificationStore = useGamificationStore()
|
||||
|
||||
const { isPrivateGarage, isCorporateFleet, toggleMode } = appModeStore
|
||||
const { earnedCount, progressPercentage, lockedAchievements } = storeToRefs(gamificationStore)
|
||||
|
||||
const nextAchievement = computed(() => {
|
||||
if (lockedAchievements.value.length > 0) {
|
||||
return lockedAchievements.value[0].title
|
||||
}
|
||||
return 'All earned!'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.achievement-showcase {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="badge-board">
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold text-slate-800 mb-2">🏅 Efficiency Badges</h2>
|
||||
<p class="text-gray-600">Professional recognition for fleet optimization and cost management.</p>
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center">
|
||||
<div class="w-3 h-3 rounded-full bg-emerald-500 mr-2"></div>
|
||||
<span class="text-sm text-gray-700">Earned: {{ earnedCount }}</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-3 h-3 rounded-full bg-gray-300 mr-2"></div>
|
||||
<span class="text-sm text-gray-700">Available: {{ totalAchievements - earnedCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-sm text-gray-500">Fleet Score</div>
|
||||
<div class="text-2xl font-bold text-slate-800">{{ fleetScore }}/100</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div
|
||||
v-for="achievement in achievements"
|
||||
:key="achievement.id"
|
||||
class="badge-card p-6 rounded-xl border transition-all duration-300"
|
||||
:class="[
|
||||
achievement.isEarned
|
||||
? 'border-emerald-200 bg-white shadow-md hover:shadow-lg'
|
||||
: 'border-gray-200 bg-gray-50'
|
||||
]"
|
||||
>
|
||||
<!-- Badge header -->
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="w-12 h-12 rounded-full flex items-center justify-center text-2xl"
|
||||
:class="achievement.isEarned ? 'bg-emerald-100 text-emerald-700' : 'bg-gray-200 text-gray-400'"
|
||||
>
|
||||
{{ achievement.icon }}
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="font-bold text-lg" :class="achievement.isEarned ? 'text-slate-900' : 'text-gray-500'">
|
||||
{{ achievement.title }}
|
||||
</h3>
|
||||
<div class="text-xs font-medium px-2 py-1 rounded-full inline-block mt-1"
|
||||
:class="achievement.isEarned ? 'bg-blue-100 text-blue-700' : 'bg-gray-200 text-gray-500'">
|
||||
{{ achievement.category.toUpperCase() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status indicator -->
|
||||
<div v-if="achievement.isEarned" class="text-emerald-600">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div v-else class="text-gray-400">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="text-gray-600 mb-5" :class="{ 'opacity-70': !achievement.isEarned }">
|
||||
{{ achievement.description }}
|
||||
</p>
|
||||
|
||||
<!-- Progress bar for unearned badges -->
|
||||
<div v-if="!achievement.isEarned" class="mt-4">
|
||||
<div class="flex justify-between text-xs text-gray-500 mb-1">
|
||||
<span>Progress</span>
|
||||
<span>0%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-gray-400 h-2 rounded-full" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Earned details -->
|
||||
<div v-if="achievement.isEarned" class="mt-4 pt-4 border-t border-gray-100">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-sm text-gray-500">
|
||||
<span class="font-medium">Awarded:</span> {{ achievement.earnedDate }}
|
||||
</div>
|
||||
<div class="text-sm font-semibold text-emerald-700">
|
||||
+{{ badgePoints(achievement.category) }} pts
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action button -->
|
||||
<div class="mt-6">
|
||||
<button
|
||||
v-if="!achievement.isEarned"
|
||||
class="w-full py-2 text-sm font-medium rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors"
|
||||
disabled
|
||||
>
|
||||
Not Yet Achieved
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="w-full py-2 text-sm font-medium rounded-lg bg-emerald-50 text-emerald-700 border border-emerald-200 hover:bg-emerald-100 transition-colors"
|
||||
>
|
||||
View Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary stats -->
|
||||
<div class="mt-10 p-6 bg-slate-50 rounded-xl border border-slate-200">
|
||||
<h3 class="font-bold text-lg text-slate-800 mb-4">Fleet Performance Summary</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-slate-800">{{ earnedCount }}</div>
|
||||
<div class="text-sm text-gray-600">Badges Earned</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-slate-800">{{ fleetScore }}</div>
|
||||
<div class="text-sm text-gray-600">Fleet Score</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-slate-800">{{ efficiencyBadgesCount }}</div>
|
||||
<div class="text-sm text-gray-600">Efficiency Badges</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-slate-800">{{ corporateBadgesCount }}</div>
|
||||
<div class="text-sm text-gray-600">Corporate Badges</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useGamificationStore } from '@/stores/gamificationStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const gamificationStore = useGamificationStore()
|
||||
const {
|
||||
achievements,
|
||||
earnedCount,
|
||||
totalAchievements
|
||||
} = storeToRefs(gamificationStore)
|
||||
|
||||
// Computed
|
||||
const fleetScore = computed(() => {
|
||||
const base = earnedCount.value * 12
|
||||
return Math.min(base, 100)
|
||||
})
|
||||
|
||||
const efficiencyBadgesCount = computed(() => {
|
||||
return achievements.value.filter(a =>
|
||||
a.category === 'efficiency' && a.isEarned
|
||||
).length
|
||||
})
|
||||
|
||||
const corporateBadgesCount = computed(() => {
|
||||
return achievements.value.filter(a =>
|
||||
a.category === 'corporate' && a.isEarned
|
||||
).length
|
||||
})
|
||||
|
||||
const badgePoints = (category) => {
|
||||
const points = {
|
||||
efficiency: 25,
|
||||
corporate: 30,
|
||||
finance: 20,
|
||||
service: 15,
|
||||
onboarding: 10,
|
||||
knowledge: 15,
|
||||
consistency: 10,
|
||||
social: 5
|
||||
}
|
||||
return points[category] || 10
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.badge-card {
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.badge-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="trophy-cabinet">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-2xl font-bold text-amber-800 mb-2">🏆 Trophy Cabinet</h2>
|
||||
<p class="text-gray-600">Your earned achievements shine here! Collect more to fill your shelf.</p>
|
||||
<div class="mt-4 flex items-center">
|
||||
<div class="w-full bg-gray-200 rounded-full h-3">
|
||||
<div
|
||||
class="bg-gradient-to-r from-amber-400 to-amber-600 h-3 rounded-full transition-all duration-500"
|
||||
:style="{ width: `${progressPercentage}%` }"
|
||||
></div>
|
||||
</div>
|
||||
<span class="ml-4 text-sm font-semibold text-amber-700">{{ earnedCount }}/{{ totalAchievements }} ({{ progressPercentage }}%)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<div
|
||||
v-for="achievement in achievements"
|
||||
:key="achievement.id"
|
||||
class="relative group"
|
||||
>
|
||||
<div
|
||||
class="trophy-card p-5 rounded-2xl border-2 transition-all duration-300 transform"
|
||||
:class="[
|
||||
achievement.isEarned
|
||||
? 'border-amber-300 bg-gradient-to-br from-amber-50 to-amber-100 shadow-lg hover:shadow-2xl hover:scale-105'
|
||||
: 'border-gray-300 bg-gray-100 opacity-60 grayscale'
|
||||
]"
|
||||
>
|
||||
<!-- Trophy Icon -->
|
||||
<div class="text-5xl mb-4 text-center">
|
||||
{{ achievement.icon }}
|
||||
</div>
|
||||
|
||||
<!-- Lock overlay for unearned -->
|
||||
<div
|
||||
v-if="!achievement.isEarned"
|
||||
class="absolute inset-0 bg-gray-800 bg-opacity-70 rounded-2xl flex items-center justify-center"
|
||||
>
|
||||
<div class="text-white text-center">
|
||||
<div class="text-3xl mb-2">🔒</div>
|
||||
<div class="text-sm font-semibold">Locked</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<h3 class="text-lg font-bold mb-2" :class="achievement.isEarned ? 'text-gray-900' : 'text-gray-500'">
|
||||
{{ achievement.title }}
|
||||
</h3>
|
||||
<p class="text-sm mb-3" :class="achievement.isEarned ? 'text-gray-700' : 'text-gray-400'">
|
||||
{{ achievement.description }}
|
||||
</p>
|
||||
|
||||
<!-- Category badge -->
|
||||
<div class="inline-block px-3 py-1 text-xs rounded-full"
|
||||
:class="achievement.isEarned ? 'bg-amber-200 text-amber-800' : 'bg-gray-300 text-gray-500'">
|
||||
{{ achievement.category }}
|
||||
</div>
|
||||
|
||||
<!-- Earned date -->
|
||||
<div v-if="achievement.isEarned" class="mt-4 pt-3 border-t border-amber-200">
|
||||
<div class="text-xs text-amber-600 font-medium">
|
||||
🎉 Earned on {{ achievement.earnedDate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Glow effect for earned trophies on hover -->
|
||||
<div
|
||||
v-if="achievement.isEarned"
|
||||
class="absolute -inset-1 bg-gradient-to-r from-blue-400 to-blue-600 rounded-2xl blur opacity-0 group-hover:opacity-30 transition-opacity duration-300 -z-10"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state message -->
|
||||
<div v-if="earnedCount === 0" class="text-center py-12">
|
||||
<div class="text-6xl mb-4">📭</div>
|
||||
<h3 class="text-xl font-semibold text-gray-700 mb-2">No trophies yet!</h3>
|
||||
<p class="text-gray-500">Start using the app to earn your first achievements.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useGamificationStore } from '@/stores/gamificationStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const gamificationStore = useGamificationStore()
|
||||
const {
|
||||
achievements,
|
||||
earnedCount,
|
||||
totalAchievements,
|
||||
progressPercentage
|
||||
} = storeToRefs(gamificationStore)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.trophy-card {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,309 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
vehicles: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
// Enhanced status colors for corporate look
|
||||
const statusColors = {
|
||||
'OK': 'bg-emerald-50 text-emerald-700 border border-emerald-200',
|
||||
'Service Due': 'bg-blue-50 text-blue-900 border border-blue-200 animate-pulse',
|
||||
'Warning': 'bg-rose-50 text-rose-700 border border-rose-200',
|
||||
'draft': 'bg-yellow-50 text-yellow-800 border border-yellow-300',
|
||||
'verified': 'bg-green-50 text-green-800 border border-green-300',
|
||||
'active': 'bg-blue-50 text-blue-800 border border-blue-300',
|
||||
'pending': 'bg-gray-50 text-gray-800 border border-gray-300',
|
||||
'incomplete': 'bg-orange-50 text-orange-800 border border-orange-300'
|
||||
}
|
||||
|
||||
const sortedVehicles = computed(() => {
|
||||
return [...props.vehicles].sort((a, b) => b.monthlyExpense - a.monthlyExpense)
|
||||
})
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
const formatMileage = (mileage) => {
|
||||
return new Intl.NumberFormat('en-US').format(mileage)
|
||||
}
|
||||
|
||||
// Country flag mapping
|
||||
const getCountryFlag = (make) => {
|
||||
const makeLower = make.toLowerCase()
|
||||
if (makeLower.includes('bmw') || makeLower.includes('mercedes') || makeLower.includes('audi') || makeLower.includes('volkswagen') || makeLower.includes('porsche')) {
|
||||
return 'https://flagcdn.com/w40/de.png'
|
||||
} else if (makeLower.includes('tesla') || makeLower.includes('ford') || makeLower.includes('chevrolet') || makeLower.includes('dodge')) {
|
||||
return 'https://flagcdn.com/w40/us.png'
|
||||
} else if (makeLower.includes('toyota') || makeLower.includes('honda') || makeLower.includes('nissan') || makeLower.includes('mazda')) {
|
||||
return 'https://flagcdn.com/w40/jp.png'
|
||||
} else if (makeLower.includes('ferrari') || makeLower.includes('lamborghini') || makeLower.includes('fiat') || makeLower.includes('alfa romeo')) {
|
||||
return 'https://flagcdn.com/w40/it.png'
|
||||
} else if (makeLower.includes('volvo') || makeLower.includes('saab')) {
|
||||
return 'https://flagcdn.com/w40/se.png'
|
||||
} else if (makeLower.includes('renault') || makeLower.includes('peugeot') || makeLower.includes('citroen')) {
|
||||
return 'https://flagcdn.com/w40/fr.png'
|
||||
} else if (makeLower.includes('skoda') || makeLower.includes('seat')) {
|
||||
return 'https://flagcdn.com/w40/cz.png'
|
||||
} else {
|
||||
return 'https://flagcdn.com/w40/eu.png'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white/80 backdrop-blur-sm rounded-2xl shadow-2xl border border-gray-300/50 overflow-hidden">
|
||||
<!-- Corporate Glass Header -->
|
||||
<div class="px-8 py-5 border-b border-gray-300/30 bg-gradient-to-r from-slate-900/90 to-slate-800/90 backdrop-blur-md">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-white tracking-tight">Corporate Fleet Management</h2>
|
||||
<p class="text-sm text-slate-300 mt-1">Enterprise-grade vehicle oversight with real-time analytics</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-3xl font-bold text-white">{{ formatCurrency(vehicles.reduce((sum, v) => sum + v.monthlyExpense, 0)) }}</div>
|
||||
<div class="text-sm text-slate-300">Total monthly fleet cost • {{ vehicles.length }} assets</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table Container -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-300/30">
|
||||
<thead class="bg-gradient-to-r from-slate-100 to-slate-200/80">
|
||||
<tr>
|
||||
<th scope="col" class="px-8 py-4 text-left text-xs font-semibold text-slate-700 uppercase tracking-wider">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2">🚗</span> Vehicle
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="px-8 py-4 text-left text-xs font-semibold text-slate-700 uppercase tracking-wider">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2">🏷️</span> License Plate
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="px-8 py-4 text-left text-xs font-semibold text-slate-700 uppercase tracking-wider">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2">📅</span> Year
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="px-8 py-4 text-left text-xs font-semibold text-slate-700 uppercase tracking-wider">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2">📊</span> Mileage
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="px-8 py-4 text-left text-xs font-semibold text-slate-700 uppercase tracking-wider">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2">⛽</span> Fuel Type
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="px-8 py-4 text-left text-xs font-semibold text-slate-700 uppercase tracking-wider">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2">🔧</span> Data Status
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="px-8 py-4 text-left text-xs font-semibold text-slate-700 uppercase tracking-wider">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2">💰</span> Monthly Cost
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="px-8 py-4 text-left text-xs font-semibold text-slate-700 uppercase tracking-wider">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2">⚡</span> Actions
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-300/20">
|
||||
<tr
|
||||
v-for="(vehicle, index) in sortedVehicles"
|
||||
:key="vehicle.id"
|
||||
:class="[
|
||||
'transition-all duration-200 hover:bg-slate-100/80',
|
||||
index % 2 === 0 ? 'bg-white' : 'bg-slate-50/70'
|
||||
]"
|
||||
>
|
||||
<td class="px-8 py-5 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="h-12 w-12 flex-shrink-0 bg-gradient-to-br from-slate-200 to-slate-300 rounded-xl overflow-hidden mr-4 shadow-sm border border-slate-300/50">
|
||||
<img
|
||||
:src="vehicle.imageUrl"
|
||||
:alt="`${vehicle.make} ${vehicle.model}`"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-slate-900 text-lg">{{ vehicle.make }} {{ vehicle.model }}</div>
|
||||
<div class="text-sm text-slate-600 mt-1">ID: {{ vehicle.id }} • Asset</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-8 py-5 whitespace-nowrap">
|
||||
<div class="flex items-center space-x-3">
|
||||
<img
|
||||
:src="getCountryFlag(vehicle.make)"
|
||||
:alt="`${vehicle.make} origin flag`"
|
||||
class="w-6 h-4 rounded-sm shadow-md border border-slate-300"
|
||||
/>
|
||||
<div>
|
||||
<div class="font-mono font-bold text-slate-900 text-lg tracking-wider">{{ vehicle.licensePlate }}</div>
|
||||
<div class="text-xs text-slate-500 mt-1">Registered</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-8 py-5 whitespace-nowrap">
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-slate-900">{{ vehicle.year }}</div>
|
||||
<div class="text-xs text-slate-500 mt-1">Model Year</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-8 py-5 whitespace-nowrap">
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-slate-900">{{ formatMileage(vehicle.mileage) }}</div>
|
||||
<div class="text-xs text-slate-500 mt-1">km</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-8 py-5 whitespace-nowrap">
|
||||
<span :class="[
|
||||
'px-4 py-2 rounded-full text-sm font-semibold shadow-sm',
|
||||
vehicle.fuelType === 'Electric' ? 'bg-emerald-100 text-emerald-800 border border-emerald-300' :
|
||||
vehicle.fuelType === 'Diesel' ? 'bg-blue-100 text-blue-800 border border-blue-300' :
|
||||
'bg-amber-100 text-amber-800 border border-amber-300'
|
||||
]">
|
||||
{{ vehicle.fuelType }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-8 py-5 whitespace-nowrap">
|
||||
<div class="space-y-2">
|
||||
<!-- Data Status Badge -->
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
:class="['px-3 py-1.5 rounded-full text-xs font-semibold flex items-center', statusColors[vehicle.data_status || vehicle.status] || 'bg-slate-100 text-slate-800 border border-slate-300']"
|
||||
>
|
||||
<span
|
||||
class="w-2 h-2 rounded-full mr-2"
|
||||
:class="{
|
||||
'bg-emerald-500': (vehicle.data_status || vehicle.status) === 'OK' || (vehicle.data_status || vehicle.status) === 'verified',
|
||||
'bg-blue-500': (vehicle.data_status || vehicle.status) === 'Service Due' || (vehicle.data_status || vehicle.status) === 'active',
|
||||
'bg-rose-500': (vehicle.data_status || vehicle.status) === 'Warning',
|
||||
'bg-yellow-500': (vehicle.data_status || vehicle.status) === 'draft',
|
||||
'bg-gray-500': (vehicle.data_status || vehicle.status) === 'pending',
|
||||
'bg-orange-500': (vehicle.data_status || vehicle.status) === 'incomplete'
|
||||
}"
|
||||
></span>
|
||||
{{ vehicle.data_status || vehicle.status }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Profile Completion Mini Progress Bar -->
|
||||
<div v-if="vehicle.profile_completion_percentage !== undefined" class="flex items-center space-x-2">
|
||||
<div class="w-16 bg-gray-200 rounded-full h-1.5">
|
||||
<div
|
||||
class="h-1.5 rounded-full transition-all duration-300"
|
||||
:class="{
|
||||
'bg-yellow-500': (vehicle.profile_completion_percentage || 0) < 50,
|
||||
'bg-blue-500': (vehicle.profile_completion_percentage || 0) >= 50 && (vehicle.profile_completion_percentage || 0) < 80,
|
||||
'bg-green-500': (vehicle.profile_completion_percentage || 0) >= 80
|
||||
}"
|
||||
:style="{ width: (vehicle.profile_completion_percentage || 0) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-600 font-medium">{{ vehicle.profile_completion_percentage || 0 }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-8 py-5 whitespace-nowrap">
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-slate-900">{{ formatCurrency(vehicle.monthlyExpense) }}</div>
|
||||
<div class="text-xs text-slate-500 mt-1">per month</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-8 py-5 whitespace-nowrap">
|
||||
<div class="flex space-x-2">
|
||||
<button class="px-4 py-2.5 bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-700 hover:to-cyan-700 text-white font-medium rounded-xl text-sm transition-all duration-200 active:scale-95 shadow-md hover:shadow-lg">
|
||||
View Details
|
||||
</button>
|
||||
<button class="px-3 py-2.5 border border-slate-300 hover:bg-slate-100 rounded-xl text-slate-700 transition-all duration-200 active:scale-95 shadow-sm hover:shadow">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Corporate Footer -->
|
||||
<div class="px-8 py-5 border-t border-gray-300/30 bg-gradient-to-r from-slate-100 to-slate-200/80">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-sm text-slate-700">
|
||||
<span class="font-semibold">Showing {{ vehicles.length }} of {{ vehicles.length }} corporate assets</span>
|
||||
<span class="mx-2">•</span>
|
||||
<span>Last updated: {{ new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) }}</span>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<button class="px-5 py-2.5 border border-slate-300 hover:bg-white text-slate-700 font-medium rounded-xl text-sm transition-all duration-200 active:scale-95 shadow-sm hover:shadow flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Export CSV
|
||||
</button>
|
||||
<button class="px-5 py-2.5 bg-gradient-to-r from-emerald-600 to-emerald-700 hover:from-emerald-700 hover:to-emerald-800 text-white font-medium rounded-xl text-sm transition-all duration-200 active:scale-95 shadow-md hover:shadow-lg flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
Add Vehicle
|
||||
</button>
|
||||
<button class="px-5 py-2.5 bg-gradient-to-r from-slate-700 to-slate-800 hover:from-slate-800 hover:to-slate-900 text-white font-medium rounded-xl text-sm transition-all duration-200 active:scale-95 shadow-md hover:shadow-lg flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
Analytics
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Custom table styles */
|
||||
table {
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
/* Smooth row transitions */
|
||||
tr {
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Custom scrollbar for table */
|
||||
.overflow-x-auto::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.overflow-x-auto::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.overflow-x-auto::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.overflow-x-auto::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,163 @@
|
||||
<script setup>
|
||||
import { useThemeStore } from '@/stores/themeStore'
|
||||
|
||||
defineProps({
|
||||
vehicle: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const themeStore = useThemeStore()
|
||||
const themeClasses = themeStore.themeClasses
|
||||
|
||||
const statusColors = {
|
||||
'OK': 'bg-green-100 text-green-800',
|
||||
'Service Due': 'bg-blue-100 text-blue-900',
|
||||
'Warning': 'bg-orange-100 text-orange-800',
|
||||
'draft': 'bg-yellow-100 text-yellow-800',
|
||||
'verified': 'bg-green-100 text-green-800',
|
||||
'active': 'bg-blue-100 text-blue-800',
|
||||
'pending': 'bg-gray-100 text-gray-800',
|
||||
'incomplete': 'bg-orange-100 text-orange-800'
|
||||
}
|
||||
|
||||
const brandLogoUrl = (make) => {
|
||||
const cleanMake = (make || '').toLowerCase().replace(/\s+/g, '')
|
||||
// Use simpleicons CDN
|
||||
return `https://cdn.simpleicons.org/${cleanMake}`
|
||||
}
|
||||
|
||||
// Country flag mapping
|
||||
const getCountryFlag = (make) => {
|
||||
const makeLower = (make || '').toLowerCase()
|
||||
if (makeLower.includes('bmw') || makeLower.includes('mercedes') || makeLower.includes('audi') || makeLower.includes('volkswagen') || makeLower.includes('porsche')) {
|
||||
return 'https://flagcdn.com/w40/de.png'
|
||||
} else if (makeLower.includes('tesla') || makeLower.includes('ford') || makeLower.includes('chevrolet') || makeLower.includes('dodge')) {
|
||||
return 'https://flagcdn.com/w40/us.png'
|
||||
} else if (makeLower.includes('toyota') || makeLower.includes('honda') || makeLower.includes('nissan') || makeLower.includes('mazda')) {
|
||||
return 'https://flagcdn.com/w40/jp.png'
|
||||
} else if (makeLower.includes('ferrari') || makeLower.includes('lamborghini') || makeLower.includes('fiat') || makeLower.includes('alfa romeo')) {
|
||||
return 'https://flagcdn.com/w40/it.png'
|
||||
} else if (makeLower.includes('volvo') || makeLower.includes('saab')) {
|
||||
return 'https://flagcdn.com/w40/se.png'
|
||||
} else if (makeLower.includes('renault') || makeLower.includes('peugeot') || makeLower.includes('citroen')) {
|
||||
return 'https://flagcdn.com/w40/fr.png'
|
||||
} else if (makeLower.includes('skoda') || makeLower.includes('seat')) {
|
||||
return 'https://flagcdn.com/w40/cz.png'
|
||||
} else {
|
||||
return 'https://flagcdn.com/w40/eu.png'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['rounded-2xl shadow-lg overflow-hidden hover:shadow-xl transition-all duration-500 border', themeClasses.card]">
|
||||
<!-- Vehicle Image -->
|
||||
<div class="h-48 bg-gray-200 relative overflow-hidden">
|
||||
<img
|
||||
:src="vehicle.imageUrl"
|
||||
:alt="`${vehicle.make} ${vehicle.model}`"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
<!-- Brand Logo -->
|
||||
<div class="absolute top-3 left-3 bg-white/80 backdrop-blur-sm rounded-lg p-2 shadow-md">
|
||||
<img
|
||||
:src="brandLogoUrl(vehicle.make)"
|
||||
:alt="vehicle.make"
|
||||
class="w-8 h-8"
|
||||
@error="(e) => e.target.style.display = 'none'"
|
||||
/>
|
||||
</div>
|
||||
<div class="absolute top-3 right-3">
|
||||
<span
|
||||
:class="['px-3 py-1 rounded-full text-xs font-semibold', statusColors[vehicle.data_status || vehicle.status] || 'bg-gray-100 text-gray-800']"
|
||||
>
|
||||
{{ vehicle.data_status || vehicle.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vehicle Details -->
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-900">{{ vehicle.make }} {{ vehicle.model }}</h3>
|
||||
<p class="text-gray-600">{{ vehicle.year }} • {{ vehicle.fuelType }}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-2xl font-bold text-blue-700">€{{ vehicle.monthlyExpense }}</div>
|
||||
<div class="text-sm text-gray-500">/month</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- License Plate with Country Flag -->
|
||||
<div class="mb-4">
|
||||
<div class="inline-flex items-center bg-gray-100 px-4 py-2 rounded-lg space-x-3">
|
||||
<img
|
||||
:src="getCountryFlag(vehicle.make)"
|
||||
:alt="`${vehicle.make} origin flag`"
|
||||
class="w-6 h-4 rounded-sm shadow-sm"
|
||||
/>
|
||||
<span class="text-gray-700 font-mono font-bold text-lg">{{ vehicle.licensePlate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Completion Progress Bar (shown for all vehicles with <100% completion) -->
|
||||
<div v-if="(vehicle.profile_completion_percentage || 0) < 100" class="mb-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span
|
||||
v-if="vehicle.data_status === 'draft'"
|
||||
class="px-2 py-1 text-xs font-semibold rounded-full bg-yellow-100 text-yellow-800"
|
||||
>
|
||||
DRAFT
|
||||
</span>
|
||||
<div class="flex-1">
|
||||
<div class="text-xs text-gray-600 mb-1">Profile: {{ vehicle.profile_completion_percentage || 0 }}% Complete</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
class="h-2 rounded-full transition-all duration-500"
|
||||
:class="{
|
||||
'bg-yellow-500': (vehicle.profile_completion_percentage || 0) < 50,
|
||||
'bg-blue-500': (vehicle.profile_completion_percentage || 0) >= 50 && (vehicle.profile_completion_percentage || 0) < 80,
|
||||
'bg-green-500': (vehicle.profile_completion_percentage || 0) >= 80
|
||||
}"
|
||||
:style="{ width: (vehicle.profile_completion_percentage || 0) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="vehicle.data_status === 'draft'" class="text-xs text-gray-500 mt-1">Edit vehicle to provide VIN or Catalog ID</p>
|
||||
<p v-else class="text-xs text-gray-500 mt-1">Complete your vehicle profile for better service recommendations</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-gray-900">{{ ((vehicle.mileage || 0) / 1000).toFixed(1) }}k</div>
|
||||
<div class="text-sm text-gray-600">km</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-gray-900">{{ vehicle.fuelType?.charAt(0) || 'U' }}</div>
|
||||
<div class="text-sm text-gray-600">Fuel</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex space-x-3">
|
||||
<button class="flex-1 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 rounded-lg transition-colors duration-200">
|
||||
View Details
|
||||
</button>
|
||||
<button class="px-4 py-3 border border-gray-300 hover:bg-gray-50 rounded-lg transition-colors duration-200">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-600" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Custom scrollbar for future use */
|
||||
</style>
|
||||
@@ -0,0 +1,271 @@
|
||||
<script setup>
|
||||
import { computed, ref, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { useGarageStore } from '@/stores/garageStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import VehicleCard from './VehicleCard.vue'
|
||||
import FleetTable from './FleetTable.vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const appModeStore = useAppModeStore()
|
||||
const garageStore = useGarageStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Animation state
|
||||
const isMounted = ref(false)
|
||||
const organizations = ref([])
|
||||
const activeOrganizationName = ref('')
|
||||
const loadingOrganizations = ref(false)
|
||||
|
||||
// Fetch vehicles on component mount (simulated)
|
||||
onMounted(() => {
|
||||
garageStore.fetchVehicles()
|
||||
// Trigger animation after mount
|
||||
setTimeout(() => {
|
||||
isMounted.value = true
|
||||
}, 100)
|
||||
|
||||
// Fetch organizations if in fleet mode
|
||||
if (appModeStore.isCorporateFleet) {
|
||||
fetchOrganizations()
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for mode changes
|
||||
watch(() => appModeStore.mode, (newMode) => {
|
||||
if (newMode === 'fleet') {
|
||||
fetchOrganizations()
|
||||
} else {
|
||||
activeOrganizationName.value = ''
|
||||
}
|
||||
})
|
||||
|
||||
// Fetch user's organizations
|
||||
const fetchOrganizations = async () => {
|
||||
if (!authStore.token) return
|
||||
|
||||
try {
|
||||
loadingOrganizations.value = true
|
||||
const response = await api.get('/organizations/my')
|
||||
organizations.value = response.data || []
|
||||
|
||||
// Find active organization name
|
||||
const activeOrgId = authStore.activeOrgId || authStore.userProfile?.active_organization_id
|
||||
if (activeOrgId && organizations.value.length > 0) {
|
||||
// Try to find organization by ID
|
||||
// Note: The current endpoint returns limited data, we may need to enhance it
|
||||
const org = organizations.value.find(o => o.organization_id === parseInt(activeOrgId))
|
||||
if (org) {
|
||||
activeOrganizationName.value = org.display_name || org.name || org.full_name || `Organization #${activeOrgId}`
|
||||
} else {
|
||||
activeOrganizationName.value = 'Corporate Fleet'
|
||||
}
|
||||
} else {
|
||||
activeOrganizationName.value = 'Corporate Fleet'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch organizations:', error)
|
||||
activeOrganizationName.value = 'Corporate Fleet'
|
||||
} finally {
|
||||
loadingOrganizations.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const stats = computed(() => ({
|
||||
totalVehicles: garageStore.totalVehicles,
|
||||
totalMonthlyExpense: garageStore.totalMonthlyExpense,
|
||||
vehiclesNeedingService: garageStore.vehiclesNeedingService
|
||||
}))
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
// Computed for header title
|
||||
const headerTitle = computed(() => {
|
||||
if (appModeStore.isPrivateGarage) {
|
||||
return 'Saját Garázs'
|
||||
} else {
|
||||
if (activeOrganizationName.value && activeOrganizationName.value !== 'Corporate Fleet') {
|
||||
return `Céges Flotta: ${activeOrganizationName.value}`
|
||||
}
|
||||
return 'Céges Flotta'
|
||||
}
|
||||
})
|
||||
|
||||
// Computed for header description
|
||||
const headerDescription = computed(() => {
|
||||
if (appModeStore.isPrivateGarage) {
|
||||
return 'Your personal vehicle collection and maintenance tracker'
|
||||
} else {
|
||||
if (activeOrganizationName.value && activeOrganizationName.value !== 'Corporate Fleet') {
|
||||
return `Company-wide vehicle management and cost analytics for ${activeOrganizationName.value}`
|
||||
}
|
||||
return 'Company-wide vehicle management and cost analytics'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-8">
|
||||
<!-- Header with Stats -->
|
||||
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-2xl p-6 border border-blue-100">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<div class="flex items-center gap-3">
|
||||
<h1 class="text-3xl font-bold text-gray-900">
|
||||
{{ headerTitle }}
|
||||
</h1>
|
||||
<span
|
||||
v-if="appModeStore.isPrivateGarage"
|
||||
class="px-3 py-1 bg-gradient-to-r from-green-500 to-emerald-600 text-white text-xs font-semibold rounded-full shadow-sm"
|
||||
>
|
||||
B2C
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="px-3 py-1 bg-gradient-to-r from-blue-500 to-indigo-600 text-white text-xs font-semibold rounded-full shadow-sm"
|
||||
>
|
||||
B2B
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-gray-600 mt-2">
|
||||
{{ headerDescription }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button
|
||||
@click="appModeStore.toggleMode"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-lg font-medium text-gray-700 hover:bg-gray-50 transition-all duration-300 flex items-center active:scale-95"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Switch to {{ appModeStore.isPrivateGarage ? 'Corporate' : 'Private' }} View
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div
|
||||
v-for="(stat, index) in [
|
||||
{ label: 'Total Vehicles', value: stats.totalVehicles, icon: 'check', color: 'blue' },
|
||||
{ label: 'Monthly Cost', value: formatCurrency(stats.totalMonthlyExpense), icon: 'currency', color: 'green' },
|
||||
{ label: 'Need Service', value: stats.vehiclesNeedingService, icon: 'warning', color: 'orange' }
|
||||
]"
|
||||
:key="stat.label"
|
||||
class="bg-white rounded-xl p-5 shadow-sm border border-gray-200 transition-all duration-500 hover:shadow-md hover:-translate-y-1"
|
||||
:style="{
|
||||
opacity: isMounted ? 1 : 0,
|
||||
transform: isMounted ? 'translateY(0)' : 'translateY(20px)',
|
||||
transitionDelay: `${index * 100}ms`
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div :class="[`p-3 bg-${stat.color}-100 rounded-lg mr-4`]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" :class="[`h-6 w-6 text-${stat.color}-600`]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path v-if="stat.icon === 'check'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<path v-if="stat.icon === 'currency'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<path v-if="stat.icon === 'warning'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.998-.833-2.732 0L4.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-gray-900">{{ stat.value }}</div>
|
||||
<div class="text-gray-600">{{ stat.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dual-UI Content -->
|
||||
<div v-if="appModeStore.isPrivateGarage">
|
||||
<!-- Private Garage: Card Grid with TransitionGroup -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-2xl font-bold text-gray-900">My Vehicles</h2>
|
||||
<div class="text-gray-600">
|
||||
{{ garageStore.vehicles.length }} vehicles in your garage
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TransitionGroup
|
||||
name="stagger-card"
|
||||
tag="div"
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-2 gap-6"
|
||||
>
|
||||
<VehicleCard
|
||||
v-for="(vehicle, index) in garageStore.vehicles"
|
||||
:key="vehicle.id"
|
||||
:vehicle="vehicle"
|
||||
:style="{
|
||||
opacity: isMounted ? 1 : 0,
|
||||
transform: isMounted ? 'translateY(0) scale(1)' : 'translateY(30px) scale(0.95)',
|
||||
transitionDelay: `${index * 150}ms`
|
||||
}"
|
||||
class="transition-all duration-700 ease-out"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<!-- Corporate Fleet: Table View -->
|
||||
<FleetTable :vehicles="garageStore.vehicles" />
|
||||
</div>
|
||||
|
||||
<!-- Empty State (if no vehicles) -->
|
||||
<div v-if="garageStore.vehicles.length === 0" class="text-center py-12">
|
||||
<div class="text-6xl text-gray-300 mb-4">🚗</div>
|
||||
<h3 class="text-xl font-bold text-gray-500 mb-2">No vehicles yet</h3>
|
||||
<p class="text-gray-600 mb-6">Add your first vehicle to get started</p>
|
||||
<button
|
||||
@click="router.push('/vehicles/add')"
|
||||
class="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition-all duration-300 hover:scale-105 active:scale-95"
|
||||
>
|
||||
Add First Vehicle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Staggered card animations */
|
||||
.stagger-card-move {
|
||||
transition: transform 0.7s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.stagger-card-enter-active,
|
||||
.stagger-card-leave-active {
|
||||
transition: all 0.7s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.stagger-card-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
|
||||
.stagger-card-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px) scale(0.95);
|
||||
}
|
||||
|
||||
.stagger-card-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* Smooth hover effects */
|
||||
.transition-all {
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
</style>
|
||||
33
old_maps/frontend_old/Archive/src/main.js
Executable file
33
old_maps/frontend_old/Archive/src/main.js
Executable file
@@ -0,0 +1,33 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { markRaw } from 'vue'
|
||||
import router from './router'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// Global error handler
|
||||
app.config.errorHandler = (err, instance, info) => {
|
||||
console.error('Global Vue error caught:', err)
|
||||
console.error('Error info:', info)
|
||||
// Optionally show a user-friendly error message
|
||||
// You could integrate with a notification store here
|
||||
}
|
||||
|
||||
// Global promise rejection handler (for unhandled async errors)
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
console.error('Unhandled promise rejection:', event.reason)
|
||||
event.preventDefault()
|
||||
})
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
// Inject router into all Pinia stores
|
||||
pinia.use(({ store }) => {
|
||||
store.router = markRaw(router)
|
||||
})
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
151
old_maps/frontend_old/Archive/src/router/index.js
Executable file
151
old_maps/frontend_old/Archive/src/router/index.js
Executable file
@@ -0,0 +1,151 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
// Nézetek importálása
|
||||
import Dashboard from '../views/Dashboard.vue';
|
||||
import Expenses from '../views/Expenses.vue';
|
||||
import AddExpense from '../views/AddExpense.vue';
|
||||
import Login from '../views/Login.vue';
|
||||
import Register from '../views/Register.vue';
|
||||
import ForgotPassword from '../views/ForgotPassword.vue';
|
||||
import ResetPassword from '../views/ResetPassword.vue';
|
||||
import AddVehicle from '../views/AddVehicle.vue';
|
||||
import AdminStats from '../views/admin/AdminStats.vue';
|
||||
import ProfileSelect from '../views/ProfileSelect.vue';
|
||||
import Debug from '../views/Debug.vue';
|
||||
|
||||
const routes = [
|
||||
// Védett útvonalak
|
||||
{ path: '/', name: 'Dashboard', component: Dashboard, meta: { requiresAuth: true } },
|
||||
{ path: '/expenses', name: 'Expenses', component: Expenses, meta: { requiresAuth: true } },
|
||||
{ path: '/expenses/add', name: 'AddExpense', component: AddExpense, meta: { requiresAuth: true } },
|
||||
{ path: '/vehicles/add', name: 'AddVehicle', component: AddVehicle, meta: { requiresAuth: true } },
|
||||
|
||||
// Profile selection (public but requires auth)
|
||||
{ path: '/profile-select', name: 'ProfileSelect', component: ProfileSelect, meta: { requiresAuth: true } },
|
||||
|
||||
// ADMIN útvonal
|
||||
{
|
||||
path: '/admin',
|
||||
name: 'Admin',
|
||||
component: AdminStats,
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
|
||||
// Nyilvános útvonalak
|
||||
{ path: '/login', name: 'Login', component: Login },
|
||||
{ path: '/register', name: 'Register', component: Register },
|
||||
{ path: '/forgot-password', name: 'ForgotPassword', component: ForgotPassword },
|
||||
{ path: '/reset-password', name: 'ResetPassword', component: ResetPassword },
|
||||
|
||||
// DEBUG útvonal (nyilvános, nincs auth check)
|
||||
{ path: '/debug', name: 'Debug', component: Debug },
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
});
|
||||
|
||||
// Helper function to check if UI mode is selected
|
||||
function hasUIModeSelected() {
|
||||
if (typeof window === 'undefined') return false;
|
||||
const saved = localStorage.getItem('ui_mode');
|
||||
// Accept both UI values (private_garage, corporate_fleet) and backend values (personal, fleet)
|
||||
return saved === 'private_garage' || saved === 'corporate_fleet' || saved === 'personal' || saved === 'fleet';
|
||||
}
|
||||
|
||||
// A "SOROMPÓ" (Auth Guard) LOGIKA
|
||||
router.beforeEach((to, from, next) => {
|
||||
console.group('🚀 Router Navigation Guard - Flight Recorder');
|
||||
console.log(`📊 Navigation: ${from.path} → ${to.path}`);
|
||||
console.log(`📍 To Route:`, to);
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
const isAdmin = localStorage.getItem('is_admin') === 'true';
|
||||
const uiMode = localStorage.getItem('ui_mode');
|
||||
|
||||
// Import stores to get real-time state
|
||||
let isAuthenticated = false;
|
||||
let appMode = null;
|
||||
|
||||
try {
|
||||
// Try to get auth store state if available
|
||||
const authStore = window.__pinia?.state.value?.auth;
|
||||
if (authStore) {
|
||||
isAuthenticated = !!authStore.token;
|
||||
console.log('🔐 Auth Store State:', authStore);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Could not access auth store:', e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to get app mode store state if available
|
||||
const appModeStore = window.__pinia?.state.value?.appMode;
|
||||
if (appModeStore) {
|
||||
appMode = appModeStore.mode;
|
||||
console.log('🎛️ App Mode Store State:', appModeStore);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Could not access app mode store:', e.message);
|
||||
}
|
||||
|
||||
console.log('📋 Authentication Status:');
|
||||
console.log(' • Token in localStorage:', token ? `YES (${token.substring(0, 20)}...)` : 'NO');
|
||||
console.log(' • isAdmin flag:', isAdmin);
|
||||
console.log(' • UI Mode in localStorage:', uiMode || 'NOT SET');
|
||||
console.log(' • Auth Store isAuthenticated:', isAuthenticated);
|
||||
console.log(' • App Mode Store mode:', appMode);
|
||||
console.log(' • Route requiresAuth:', to.meta.requiresAuth || false);
|
||||
console.log(' • Route requiresAdmin:', to.meta.requiresAdmin || false);
|
||||
|
||||
// Auth check
|
||||
if (to.meta.requiresAuth && !token) {
|
||||
console.warn('❌ AUTH FAILED: Route requires auth but no token found');
|
||||
console.warn(` ↳ Redirecting to /login (from ${to.path})`);
|
||||
console.groupEnd();
|
||||
next('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
// Admin check
|
||||
if (to.meta.requiresAdmin && !isAdmin) {
|
||||
console.warn('❌ ADMIN CHECK FAILED: Route requires admin but user is not admin');
|
||||
console.warn(` ↳ Redirecting to / (from ${to.path})`);
|
||||
console.groupEnd();
|
||||
next('/');
|
||||
return;
|
||||
}
|
||||
|
||||
// UI mode selection logic
|
||||
if (to.meta.requiresAuth && token) {
|
||||
const hasMode = hasUIModeSelected();
|
||||
console.log('🎯 UI Mode Check:');
|
||||
console.log(' • hasUIModeSelected():', hasMode);
|
||||
console.log(' • Target route name:', to.name);
|
||||
|
||||
// If user tries to access dashboard without mode selection, redirect to profile-select
|
||||
if (to.name === 'Dashboard' && !hasMode) {
|
||||
console.warn('⚠️ UI MODE MISSING: Dashboard access without mode selection');
|
||||
console.warn(` ↳ Redirecting to /profile-select (from ${to.path})`);
|
||||
console.groupEnd();
|
||||
next('/profile-select');
|
||||
return;
|
||||
}
|
||||
|
||||
// If user tries to access profile-select but already has mode, redirect to dashboard
|
||||
if (to.name === 'ProfileSelect' && hasMode) {
|
||||
console.warn('⚠️ UI MODE ALREADY SET: Profile-select access with existing mode');
|
||||
console.warn(` ↳ Redirecting to / (from ${to.path})`);
|
||||
console.groupEnd();
|
||||
next('/');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ ALL CHECKS PASSED: Allowing navigation to', to.path);
|
||||
console.groupEnd();
|
||||
next();
|
||||
});
|
||||
|
||||
export default router;
|
||||
167
old_maps/frontend_old/Archive/src/services/api.js
Normal file
167
old_maps/frontend_old/Archive/src/services/api.js
Normal file
@@ -0,0 +1,167 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// Create axios instance with base URL from environment variable
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
// Request interceptor to add auth token
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
console.group('📤 Axios Request Interceptor - Flight Recorder');
|
||||
console.log('📊 Request Details:');
|
||||
console.log(' • URL:', config.url);
|
||||
console.log(' • Method:', config.method);
|
||||
console.log(' • Base URL:', config.baseURL);
|
||||
|
||||
// Get token from localStorage (check both 'token' and 'access_token' for compatibility)
|
||||
if (typeof window !== 'undefined') {
|
||||
let token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
console.log('⚠️ Using access_token (legacy) instead of token');
|
||||
}
|
||||
}
|
||||
|
||||
if (token) {
|
||||
console.log('🔐 Adding Authorization header with token');
|
||||
console.log(' • Token present:', token.substring(0, 20) + '...');
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
} else {
|
||||
console.log('⚠️ No auth token found in localStorage');
|
||||
console.log(' • token key:', localStorage.getItem('token') ? 'PRESENT' : 'MISSING');
|
||||
console.log(' • access_token key:', localStorage.getItem('access_token') ? 'PRESENT' : 'MISSING');
|
||||
}
|
||||
} else {
|
||||
console.log('🌐 Window not available (SSR)');
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
console.error('❌ Request interceptor error:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
)
|
||||
|
||||
// Response interceptor for error handling
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
console.group('🚨 Axios Response Interceptor - Flight Recorder');
|
||||
console.log('📊 Interceptor triggered for error:', error);
|
||||
|
||||
if (error.response) {
|
||||
console.log('📡 Response Details:');
|
||||
console.log(' • Status:', error.response.status);
|
||||
console.log(' • URL:', error.config?.url);
|
||||
console.log(' • Method:', error.config?.method);
|
||||
console.log(' • Headers:', error.config?.headers);
|
||||
|
||||
if (error.response.status === 401) {
|
||||
console.warn('🔐 401 UNAUTHORIZED DETECTED!');
|
||||
console.warn(' ↳ This will trigger logout and redirect');
|
||||
|
||||
// Log current auth state before clearing
|
||||
const token = localStorage.getItem('token');
|
||||
const accessToken = localStorage.getItem('access_token');
|
||||
console.log(' ↳ Current localStorage state:');
|
||||
console.log(' - token:', token ? `YES (${token.substring(0, 20)}...)` : 'NO');
|
||||
console.log(' - access_token:', accessToken ? `YES (${accessToken.substring(0, 20)}...)` : 'NO');
|
||||
|
||||
// Handle unauthorized - clear ALL auth tokens and redirect to login
|
||||
if (typeof window !== 'undefined') {
|
||||
console.log(' ↳ Clearing auth tokens from localStorage...');
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
localStorage.removeItem('is_admin');
|
||||
localStorage.removeItem('user_email');
|
||||
localStorage.removeItem('user_role');
|
||||
|
||||
// Also try to call auth store logout if available
|
||||
try {
|
||||
const authStore = window.__pinia?.state.value?.auth;
|
||||
if (authStore && authStore.logout) {
|
||||
console.log(' ↳ Calling auth store logout()...');
|
||||
// We can't call the function directly from here, but we can dispatch an event
|
||||
window.dispatchEvent(new CustomEvent('force-logout'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(' ↳ Could not access auth store:', e.message);
|
||||
}
|
||||
|
||||
console.warn(' ↳ Redirecting to /login');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
} else if (error.response.status === 403) {
|
||||
console.warn('🔒 403 FORBIDDEN DETECTED');
|
||||
console.warn(' ↳ User lacks permissions for this resource');
|
||||
} else if (error.response.status === 404) {
|
||||
console.warn('🔍 404 NOT FOUND DETECTED');
|
||||
console.warn(' ↳ API endpoint or resource not found');
|
||||
} else if (error.response.status >= 500) {
|
||||
console.error('💥 SERVER ERROR DETECTED (5xx)');
|
||||
console.error(' ↳ Backend server issue');
|
||||
}
|
||||
} else if (error.request) {
|
||||
console.error('🌐 NETWORK ERROR: Request was made but no response received');
|
||||
console.error(' ↳ Possible network issue or CORS problem');
|
||||
} else {
|
||||
console.error('⚙️ SETUP ERROR: Error in request configuration');
|
||||
console.error(' ↳', error.message);
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
)
|
||||
|
||||
export default api
|
||||
|
||||
// Catalog API functions
|
||||
export const catalogApi = {
|
||||
async getMakes(vehicleClass = null) {
|
||||
const params = {}
|
||||
if (vehicleClass) {
|
||||
params.vehicle_class = vehicleClass
|
||||
}
|
||||
const response = await api.get('/api/v1/catalog/makes', { params })
|
||||
return response.data
|
||||
},
|
||||
async getModels(make, vehicleClass = null) {
|
||||
const params = { make }
|
||||
if (vehicleClass) {
|
||||
params.vehicle_class = vehicleClass
|
||||
}
|
||||
const response = await api.get('/api/v1/catalog/models', { params })
|
||||
return response.data
|
||||
},
|
||||
async getGenerations(make, model) {
|
||||
const response = await api.get('/api/v1/catalog/generations', { params: { make, model } })
|
||||
return response.data
|
||||
},
|
||||
async getEngines(make, model, gen) {
|
||||
const response = await api.get('/api/v1/catalog/engines', { params: { make, model, gen } })
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
// Organization API functions
|
||||
export const organizationApi = {
|
||||
async getMyOrganizations() {
|
||||
const response = await api.get('/organizations/my')
|
||||
return response.data
|
||||
},
|
||||
async updateActiveOrganization(organizationId) {
|
||||
const response = await api.patch('/users/me/active-organization', {
|
||||
organization_id: organizationId
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
202
old_maps/frontend_old/Archive/src/stores/analyticsStore.js
Normal file
202
old_maps/frontend_old/Archive/src/stores/analyticsStore.js
Normal file
@@ -0,0 +1,202 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useAuthStore } from './authStore'
|
||||
|
||||
export const useAnalyticsStore = defineStore('analytics', () => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Real data - initially empty, will be fetched from API
|
||||
const monthlyCosts = ref([])
|
||||
const fuelEfficiencyTrends = ref([])
|
||||
const costPerKmTrends = ref([])
|
||||
const funFacts = ref({
|
||||
totalKmDriven: 0,
|
||||
totalTreesSaved: 0,
|
||||
totalCo2Saved: 0,
|
||||
totalMoneySaved: 0,
|
||||
moonTrips: computed(() => Math.round(funFacts.value.totalKmDriven / 384400)),
|
||||
earthCircuits: computed(() => Math.round(funFacts.value.totalKmDriven / 40075)),
|
||||
})
|
||||
const businessMetrics = ref({
|
||||
fleetSize: 0,
|
||||
averageVehicleAge: 0,
|
||||
totalMonthlyCost: 0,
|
||||
averageCostPerKm: 0,
|
||||
utilizationRate: 0,
|
||||
downtimeHours: 0,
|
||||
})
|
||||
|
||||
const isLoading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
// Getters
|
||||
const totalCosts = computed(() => {
|
||||
return monthlyCosts.value.reduce((sum, month) => sum + month.total, 0)
|
||||
})
|
||||
|
||||
const averageMonthlyCost = computed(() => {
|
||||
return monthlyCosts.value.length > 0 ? totalCosts.value / monthlyCosts.value.length : 0
|
||||
})
|
||||
|
||||
const averageFuelEfficiency = computed(() => {
|
||||
const sum = fuelEfficiencyTrends.value.reduce((acc, item) => acc + item.efficiency, 0)
|
||||
return fuelEfficiencyTrends.value.length > 0 ? sum / fuelEfficiencyTrends.value.length : 0
|
||||
})
|
||||
|
||||
const averageCostPerKm = computed(() => {
|
||||
const sum = costPerKmTrends.value.reduce((acc, item) => acc + item.cost, 0)
|
||||
return costPerKmTrends.value.length > 0 ? sum / costPerKmTrends.value.length : 0
|
||||
})
|
||||
|
||||
// Actions
|
||||
function addMonthlyCost(data) {
|
||||
monthlyCosts.value.push(data)
|
||||
}
|
||||
|
||||
function updateFuelEfficiency(month, efficiency) {
|
||||
const index = fuelEfficiencyTrends.value.findIndex(item => item.month === month)
|
||||
if (index !== -1) {
|
||||
fuelEfficiencyTrends.value[index].efficiency = efficiency
|
||||
}
|
||||
}
|
||||
|
||||
function updateFunFacts(newFacts) {
|
||||
Object.assign(funFacts.value, newFacts)
|
||||
}
|
||||
|
||||
// Real API fetch - NO MORE MOCK DATA
|
||||
async function fetchDashboardAnalytics() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
// Get auth token
|
||||
const token = authStore.token
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated')
|
||||
}
|
||||
|
||||
// Call real backend API
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/analytics/dashboard`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch analytics: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log('AnalyticsStore: Fetched dashboard analytics', data)
|
||||
|
||||
// Transform API response to frontend format
|
||||
monthlyCosts.value = data.monthly_costs || []
|
||||
fuelEfficiencyTrends.value = data.fuel_efficiency_trends || []
|
||||
costPerKmTrends.value = data.cost_per_km_trends || []
|
||||
|
||||
if (data.fun_facts) {
|
||||
funFacts.value = {
|
||||
totalKmDriven: data.fun_facts.total_km_driven || 0,
|
||||
totalTreesSaved: data.fun_facts.total_trees_saved || 0,
|
||||
totalCo2Saved: data.fun_facts.total_co2_saved || 0,
|
||||
totalMoneySaved: data.fun_facts.total_money_saved || 0,
|
||||
moonTrips: computed(() => Math.round((data.fun_facts.total_km_driven || 0) / 384400)),
|
||||
earthCircuits: computed(() => Math.round((data.fun_facts.total_km_driven || 0) / 40075)),
|
||||
}
|
||||
}
|
||||
|
||||
if (data.business_metrics) {
|
||||
businessMetrics.value = {
|
||||
fleetSize: data.business_metrics.fleet_size || 0,
|
||||
averageVehicleAge: data.business_metrics.average_vehicle_age || 0,
|
||||
totalMonthlyCost: data.business_metrics.total_monthly_cost || 0,
|
||||
averageCostPerKm: data.business_metrics.average_cost_per_km || 0,
|
||||
utilizationRate: data.business_metrics.utilization_rate || 0,
|
||||
downtimeHours: data.business_metrics.downtime_hours || 0,
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('AnalyticsStore: Error fetching analytics', err)
|
||||
error.value = err.message
|
||||
// Keep empty data (no mock fallback)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch vehicle-specific analytics
|
||||
async function fetchVehicleAnalytics(vehicleId) {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const token = authStore.token
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated')
|
||||
}
|
||||
|
||||
// Call vehicle summary endpoint
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/analytics/${vehicleId}/summary`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch vehicle analytics: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log('AnalyticsStore: Fetched vehicle analytics', data)
|
||||
|
||||
// For now, just return the data - frontend components can use it directly
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('AnalyticsStore: Error fetching vehicle analytics', err)
|
||||
error.value = err.message
|
||||
throw err
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch fleet analytics (aggregated)
|
||||
async function fetchFleetAnalytics() {
|
||||
// For now, use the dashboard endpoint which includes fleet metrics
|
||||
return fetchDashboardAnalytics()
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
monthlyCosts,
|
||||
fuelEfficiencyTrends,
|
||||
costPerKmTrends,
|
||||
funFacts,
|
||||
businessMetrics,
|
||||
isLoading,
|
||||
error,
|
||||
|
||||
// Getters
|
||||
totalCosts,
|
||||
averageMonthlyCost,
|
||||
averageFuelEfficiency,
|
||||
averageCostPerKm,
|
||||
|
||||
// Actions
|
||||
addMonthlyCost,
|
||||
updateFuelEfficiency,
|
||||
updateFunFacts,
|
||||
fetchDashboardAnalytics,
|
||||
fetchVehicleAnalytics,
|
||||
fetchFleetAnalytics,
|
||||
}
|
||||
})
|
||||
135
old_maps/frontend_old/Archive/src/stores/appModeStore.js
Normal file
135
old_maps/frontend_old/Archive/src/stores/appModeStore.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import api from '@/services/api'
|
||||
import { useGarageStore } from './garageStore'
|
||||
|
||||
export const useAppModeStore = defineStore('appMode', () => {
|
||||
// State
|
||||
const mode = ref('personal') // backend compatible values: 'personal' or 'fleet'
|
||||
const isLoading = ref(false)
|
||||
|
||||
// Getters
|
||||
const isPrivateGarage = computed(() => mode.value === 'personal')
|
||||
const isCorporateFleet = computed(() => mode.value === 'fleet')
|
||||
|
||||
// Actions
|
||||
async function setMode(newMode) {
|
||||
// Map UI values to backend compatible values
|
||||
let backendMode = newMode
|
||||
if (newMode === 'private_garage') {
|
||||
backendMode = 'personal'
|
||||
} else if (newMode === 'corporate_fleet') {
|
||||
backendMode = 'fleet'
|
||||
}
|
||||
|
||||
if (!['personal', 'fleet'].includes(backendMode)) {
|
||||
console.error('Invalid mode:', newMode)
|
||||
return
|
||||
}
|
||||
mode.value = backendMode
|
||||
persistMode(backendMode)
|
||||
await saveModeToBackend(backendMode)
|
||||
}
|
||||
|
||||
function toggleMode() {
|
||||
const newMode = mode.value === 'personal' ? 'fleet' : 'personal'
|
||||
setMode(newMode)
|
||||
}
|
||||
|
||||
// SSR-safe localStorage persistence
|
||||
function persistMode(mode) {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('ui_mode', mode)
|
||||
}
|
||||
}
|
||||
|
||||
function getInitialMode() {
|
||||
if (typeof window !== 'undefined') {
|
||||
const saved = localStorage.getItem('ui_mode')
|
||||
// Map UI values to backend compatible values
|
||||
if (saved === 'private_garage' || saved === 'personal') {
|
||||
return 'personal'
|
||||
} else if (saved === 'corporate_fleet' || saved === 'fleet') {
|
||||
return 'fleet'
|
||||
}
|
||||
}
|
||||
// Default mode
|
||||
return 'personal'
|
||||
}
|
||||
|
||||
// Load user preferences from backend on app startup
|
||||
async function loadModeFromBackend() {
|
||||
if (typeof window === 'undefined') return
|
||||
try {
|
||||
isLoading.value = true
|
||||
const response = await api.get('/users/me')
|
||||
const user = response.data
|
||||
if (user.ui_mode && ['personal', 'fleet'].includes(user.ui_mode)) {
|
||||
mode.value = user.ui_mode
|
||||
persistMode(user.ui_mode)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load UI mode from backend, using local storage', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Save mode to backend via PATCH /users/me/preferences
|
||||
async function saveModeToBackend(newMode) {
|
||||
try {
|
||||
await api.patch('/users/me/preferences', {
|
||||
ui_mode: newMode
|
||||
})
|
||||
|
||||
// Also update active organization based on mode
|
||||
// When switching to fleet mode, we need to set an active organization
|
||||
// When switching to personal mode, we should clear the active organization
|
||||
if (newMode === 'fleet') {
|
||||
// Try to get the user's active organization from auth store
|
||||
// For now, we'll just make sure the backend knows we're in fleet mode
|
||||
// The actual organization selection should happen elsewhere
|
||||
console.log('Switched to fleet mode - organization selection may be needed')
|
||||
} else if (newMode === 'personal') {
|
||||
// Clear active organization when switching to personal mode
|
||||
try {
|
||||
await api.patch('/users/me/active-organization', {
|
||||
organization_id: null
|
||||
})
|
||||
console.log('Cleared active organization for personal mode')
|
||||
} catch (orgError) {
|
||||
console.warn('Failed to clear active organization', orgError)
|
||||
// Non-critical error, continue
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh the garage store to reflect the new scope
|
||||
try {
|
||||
const garageStore = useGarageStore()
|
||||
await garageStore.fetchVehicles()
|
||||
console.log('Garage store refreshed after mode change')
|
||||
} catch (refreshError) {
|
||||
console.warn('Failed to refresh garage store', refreshError)
|
||||
// Non-critical error, continue
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save UI mode to backend', error)
|
||||
// Optionally revert local state? For now just log.
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
mode.value = getInitialMode()
|
||||
// Load from backend after store creation (call in component's onMounted)
|
||||
// We expose loadModeFromBackend for components to call
|
||||
|
||||
return {
|
||||
mode,
|
||||
isLoading,
|
||||
isPrivateGarage,
|
||||
isCorporateFleet,
|
||||
setMode,
|
||||
toggleMode,
|
||||
loadModeFromBackend,
|
||||
}
|
||||
})
|
||||
322
old_maps/frontend_old/Archive/src/stores/authStore.js
Normal file
322
old_maps/frontend_old/Archive/src/stores/authStore.js
Normal file
@@ -0,0 +1,322 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import router from '../router'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
// State
|
||||
const token = ref(localStorage.getItem('token') || '')
|
||||
const isAdmin = ref(localStorage.getItem('is_admin') === 'true')
|
||||
const userEmail = ref(localStorage.getItem('user_email') || '')
|
||||
const userRole = ref(localStorage.getItem('user_role') || '')
|
||||
const userProfile = ref(null) // Full user profile from /users/me
|
||||
const activeOrgId = ref(localStorage.getItem('active_org_id') || null) // Active organization ID
|
||||
|
||||
// Getters
|
||||
const isLoggedIn = computed(() => !!token.value)
|
||||
const isTester = computed(() => userEmail.value === 'tester_pro@profibot.hu' || userRole.value === 'tester')
|
||||
const displayName = computed(() => {
|
||||
if (isTester.value) return 'TESTER PRO'
|
||||
if (isAdmin.value) return 'ADMIN'
|
||||
return 'USER'
|
||||
})
|
||||
|
||||
// Actions
|
||||
const login = async (email, password) => {
|
||||
console.log('AuthStore: Real API login attempt for', email)
|
||||
|
||||
try {
|
||||
// Prepare URL-encoded form data for OAuth2 password grant
|
||||
// FastAPI's OAuth2PasswordRequestForm expects application/x-www-form-urlencoded
|
||||
const params = new URLSearchParams()
|
||||
params.append('username', email)
|
||||
params.append('password', password)
|
||||
|
||||
// Call real backend API
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu';
|
||||
const response = await fetch(`${apiBaseUrl}/auth/login`, {
|
||||
method: 'POST',
|
||||
body: params,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error('AuthStore: Login API error', response.status, errorText)
|
||||
throw new Error(`Login failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log('AuthStore: Login API response', data)
|
||||
|
||||
// Extract token and user info
|
||||
const accessToken = data.access_token
|
||||
const refreshToken = data.refresh_token
|
||||
const tokenType = data.token_type
|
||||
const isActive = data.is_active
|
||||
|
||||
// We need to decode the JWT token to get user role and info
|
||||
// For now, we'll make a separate API call to get user info
|
||||
// Or we can parse the JWT token (simple base64 decode)
|
||||
let roleValue = 'user'
|
||||
let adminFlag = false
|
||||
|
||||
try {
|
||||
// Decode JWT token to get payload
|
||||
const tokenParts = accessToken.split('.')
|
||||
if (tokenParts.length === 3) {
|
||||
const payload = JSON.parse(atob(tokenParts[1]))
|
||||
roleValue = payload.role || 'user'
|
||||
adminFlag = roleValue === 'admin' || roleValue === 'superadmin'
|
||||
console.log('AuthStore: Decoded JWT payload', payload)
|
||||
}
|
||||
} catch (decodeError) {
|
||||
console.warn('AuthStore: Could not decode JWT token', decodeError)
|
||||
// Fallback: Make API call to get user info
|
||||
// For now, we'll use a default role
|
||||
}
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('token', accessToken)
|
||||
localStorage.setItem('refresh_token', refreshToken)
|
||||
localStorage.setItem('is_admin', adminFlag.toString())
|
||||
localStorage.setItem('user_email', email)
|
||||
localStorage.setItem('user_role', roleValue)
|
||||
|
||||
// Update store state
|
||||
token.value = accessToken
|
||||
isAdmin.value = adminFlag
|
||||
userEmail.value = email
|
||||
userRole.value = roleValue
|
||||
|
||||
console.log('AuthStore: State updated, redirecting to /profile-select')
|
||||
|
||||
// Redirect to profile-select (as per router logic)
|
||||
try {
|
||||
await router.push('/profile-select')
|
||||
console.log('AuthStore: Redirect successful')
|
||||
} catch (error) {
|
||||
console.error('AuthStore: Router redirect failed:', error)
|
||||
throw error
|
||||
}
|
||||
|
||||
return { success: true, token: accessToken, isAdmin: adminFlag, role: roleValue }
|
||||
|
||||
} catch (error) {
|
||||
console.error('AuthStore: Login failed', error)
|
||||
throw error // Re-throw the error instead of falling back to mock
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
// Clear localStorage
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('is_admin')
|
||||
localStorage.removeItem('user_email')
|
||||
localStorage.removeItem('user_role')
|
||||
localStorage.removeItem('ui_mode') // Also clear UI mode on logout
|
||||
localStorage.removeItem('active_org_id') // Clear active organization ID
|
||||
|
||||
// Reset store state
|
||||
token.value = ''
|
||||
isAdmin.value = false
|
||||
userEmail.value = ''
|
||||
userRole.value = ''
|
||||
userProfile.value = null
|
||||
activeOrgId.value = null
|
||||
|
||||
// Redirect to login
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
const fetchUserProfile = async () => {
|
||||
if (!token.value) {
|
||||
throw new Error('Not authenticated')
|
||||
}
|
||||
|
||||
try {
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu';
|
||||
const response = await fetch(`${apiBaseUrl}/users/me`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token.value}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch user profile: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log('AuthStore: Fetched user profile', data)
|
||||
|
||||
// Update user profile state
|
||||
userProfile.value = data
|
||||
|
||||
// Also update email and role from profile if available
|
||||
if (data.email && !userEmail.value) {
|
||||
userEmail.value = data.email
|
||||
localStorage.setItem('user_email', data.email)
|
||||
}
|
||||
|
||||
if (data.role && !userRole.value) {
|
||||
userRole.value = data.role
|
||||
localStorage.setItem('user_role', data.role)
|
||||
isAdmin.value = data.role === 'admin' || data.role === 'superadmin'
|
||||
localStorage.setItem('is_admin', isAdmin.value.toString())
|
||||
}
|
||||
|
||||
// Store active organization ID if available
|
||||
if (data.active_organization_id !== undefined) {
|
||||
activeOrgId.value = data.active_organization_id
|
||||
localStorage.setItem('active_org_id', data.active_organization_id)
|
||||
console.log('AuthStore: Set active organization ID:', data.active_organization_id)
|
||||
} else if (data.scope_id) {
|
||||
// Fallback to scope_id if active_organization_id is not provided
|
||||
activeOrgId.value = data.scope_id
|
||||
localStorage.setItem('active_org_id', data.scope_id)
|
||||
console.log('AuthStore: Set active organization ID from scope_id:', data.scope_id)
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('AuthStore: Error fetching user profile', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Update active organization
|
||||
const updateActiveOrganization = async (organizationId) => {
|
||||
if (!token.value) {
|
||||
throw new Error('Not authenticated')
|
||||
}
|
||||
|
||||
try {
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu';
|
||||
const response = await fetch(`${apiBaseUrl}/users/me/active-organization`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token.value}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
organization_id: organizationId
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to update active organization: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log('AuthStore: Updated active organization', data)
|
||||
|
||||
// Check if response contains new JWT token (backend now returns {user: {...}, access_token: "...", token_type: "bearer"})
|
||||
if (data.access_token) {
|
||||
console.log('AuthStore: Received new access token from organization switch')
|
||||
|
||||
// Update token in localStorage and store state
|
||||
localStorage.setItem('token', data.access_token)
|
||||
token.value = data.access_token
|
||||
|
||||
// Decode new token to update role if needed
|
||||
try {
|
||||
const tokenParts = data.access_token.split('.')
|
||||
if (tokenParts.length === 3) {
|
||||
const payload = JSON.parse(atob(tokenParts[1]))
|
||||
const roleValue = payload.role || 'user'
|
||||
const adminFlag = roleValue === 'admin' || roleValue === 'superadmin'
|
||||
|
||||
localStorage.setItem('user_role', roleValue)
|
||||
localStorage.setItem('is_admin', adminFlag.toString())
|
||||
|
||||
userRole.value = roleValue
|
||||
isAdmin.value = adminFlag
|
||||
console.log('AuthStore: Updated role from new token:', roleValue)
|
||||
}
|
||||
} catch (decodeError) {
|
||||
console.warn('AuthStore: Could not decode new JWT token', decodeError)
|
||||
}
|
||||
}
|
||||
|
||||
// Update local state
|
||||
activeOrgId.value = organizationId
|
||||
localStorage.setItem('active_org_id', organizationId || '')
|
||||
|
||||
// Also update user profile to reflect changes
|
||||
await fetchUserProfile()
|
||||
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('AuthStore: Error updating active organization', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
const checkAuth = () => {
|
||||
// Sync with localStorage on page load
|
||||
const storedToken = localStorage.getItem('token') || ''
|
||||
const storedEmail = localStorage.getItem('user_email') || ''
|
||||
const storedRole = localStorage.getItem('user_role') || ''
|
||||
|
||||
// Try to decode JWT token to get role if not stored
|
||||
let decodedRole = storedRole
|
||||
let decodedIsAdmin = storedRole === 'admin' || storedRole === 'superadmin'
|
||||
|
||||
if (storedToken && !storedRole) {
|
||||
try {
|
||||
// Decode JWT token to get payload
|
||||
const tokenParts = storedToken.split('.')
|
||||
if (tokenParts.length === 3) {
|
||||
const payload = JSON.parse(atob(tokenParts[1]))
|
||||
decodedRole = payload.role || 'user'
|
||||
decodedIsAdmin = decodedRole === 'admin' || decodedRole === 'superadmin'
|
||||
console.log('AuthStore: Decoded JWT on checkAuth', payload)
|
||||
|
||||
// Update localStorage with decoded role
|
||||
localStorage.setItem('user_role', decodedRole)
|
||||
localStorage.setItem('is_admin', decodedIsAdmin.toString())
|
||||
}
|
||||
} catch (decodeError) {
|
||||
console.warn('AuthStore: Could not decode JWT token on checkAuth', decodeError)
|
||||
}
|
||||
} else if (storedToken && storedRole) {
|
||||
// Use stored role
|
||||
decodedIsAdmin = storedRole === 'admin' || storedRole === 'superadmin'
|
||||
}
|
||||
|
||||
token.value = storedToken
|
||||
isAdmin.value = decodedIsAdmin
|
||||
userEmail.value = storedEmail
|
||||
userRole.value = decodedRole || 'user'
|
||||
}
|
||||
|
||||
// Initialize on store creation
|
||||
checkAuth()
|
||||
|
||||
return {
|
||||
// State
|
||||
token,
|
||||
isAdmin,
|
||||
userEmail,
|
||||
userRole,
|
||||
userProfile,
|
||||
activeOrgId,
|
||||
|
||||
// Getters
|
||||
isLoggedIn,
|
||||
isTester,
|
||||
displayName,
|
||||
|
||||
// Actions
|
||||
login,
|
||||
logout,
|
||||
checkAuth,
|
||||
fetchUserProfile,
|
||||
updateActiveOrganization
|
||||
}
|
||||
})
|
||||
34
old_maps/frontend_old/Archive/src/stores/expenseStore.js
Normal file
34
old_maps/frontend_old/Archive/src/stores/expenseStore.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
export const useExpenseStore = defineStore('expense', () => {
|
||||
const isLoading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
async function createExpense(expenseData) {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await api.post('/expenses/', expenseData)
|
||||
return response.data
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.detail || err.message
|
||||
// Check for DRAFT_LIMIT_REACHED error
|
||||
if (err.response?.status === 403 && err.response?.data?.detail === "DRAFT_LIMIT_REACHED") {
|
||||
const draftError = new Error('DRAFT_LIMIT_REACHED')
|
||||
draftError.response = err.response
|
||||
throw draftError
|
||||
}
|
||||
throw err
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
createExpense,
|
||||
}
|
||||
})
|
||||
158
old_maps/frontend_old/Archive/src/stores/gamificationStore.js
Normal file
158
old_maps/frontend_old/Archive/src/stores/gamificationStore.js
Normal file
@@ -0,0 +1,158 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
export const useGamificationStore = defineStore('gamification', () => {
|
||||
// State
|
||||
const achievements = ref([])
|
||||
const badges = ref([])
|
||||
const userStats = ref(null)
|
||||
const isLoading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
// Getters
|
||||
const earnedAchievements = computed(() =>
|
||||
achievements.value.filter(a => a.is_earned)
|
||||
)
|
||||
|
||||
const lockedAchievements = computed(() =>
|
||||
achievements.value.filter(a => !a.is_earned)
|
||||
)
|
||||
|
||||
const totalAchievements = computed(() => achievements.value.length)
|
||||
|
||||
const earnedCount = computed(() => earnedAchievements.value.length)
|
||||
|
||||
const progressPercentage = computed(() => {
|
||||
if (totalAchievements.value === 0) return 0
|
||||
return Math.round((earnedCount.value / totalAchievements.value) * 100)
|
||||
})
|
||||
|
||||
const earnedBadges = computed(() =>
|
||||
badges.value.filter(b => b.is_earned)
|
||||
)
|
||||
|
||||
// Helper function for API calls (using centralized api instance)
|
||||
async function apiFetch(url, options = {}) {
|
||||
try {
|
||||
const response = await api.get(url, options)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
throw new Error(`API error ${error.response.status}: ${JSON.stringify(error.response.data)}`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
async function fetchAchievements() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await apiFetch('/gamification/achievements')
|
||||
achievements.value = data.achievements || []
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch achievements:', err)
|
||||
error.value = err.message
|
||||
// No mock fallback - let the error propagate
|
||||
achievements.value = []
|
||||
throw err
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchBadges() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await apiFetch('/gamification/my-badges')
|
||||
badges.value = data.map(badge => ({
|
||||
id: badge.badge_id,
|
||||
title: badge.badge_name,
|
||||
description: badge.badge_description,
|
||||
icon_url: badge.badge_icon_url,
|
||||
is_earned: true,
|
||||
earned_date: badge.earned_at,
|
||||
category: 'badge'
|
||||
}))
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch badges:', err)
|
||||
error.value = err.message
|
||||
// No mock fallback - propagate error
|
||||
badges.value = []
|
||||
throw err
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUserStats() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await apiFetch('/gamification/me')
|
||||
userStats.value = data
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch user stats:', err)
|
||||
error.value = err.message
|
||||
// No mock fallback - propagate error
|
||||
userStats.value = null
|
||||
throw err
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAllGamificationData() {
|
||||
await Promise.all([
|
||||
fetchAchievements(),
|
||||
fetchBadges(),
|
||||
fetchUserStats()
|
||||
])
|
||||
}
|
||||
|
||||
async function earnAchievement(id) {
|
||||
// In a real implementation, this would call an API endpoint
|
||||
// For now, we'll just update local state
|
||||
const achievement = achievements.value.find(a => a.id === id)
|
||||
if (achievement && !achievement.is_earned) {
|
||||
achievement.is_earned = true
|
||||
achievement.earned_date = new Date().toISOString().split('T')[0]
|
||||
}
|
||||
}
|
||||
|
||||
function resetAchievements() {
|
||||
achievements.value.forEach(a => {
|
||||
a.is_earned = false
|
||||
a.earned_date = null
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize store with data - RE-ENABLED after token fix
|
||||
fetchAllGamificationData()
|
||||
|
||||
return {
|
||||
achievements,
|
||||
badges,
|
||||
userStats,
|
||||
earnedAchievements,
|
||||
lockedAchievements,
|
||||
totalAchievements,
|
||||
earnedCount,
|
||||
progressPercentage,
|
||||
earnedBadges,
|
||||
isLoading,
|
||||
error,
|
||||
fetchAchievements,
|
||||
fetchBadges,
|
||||
fetchUserStats,
|
||||
fetchAllGamificationData,
|
||||
earnAchievement,
|
||||
resetAchievements
|
||||
}
|
||||
})
|
||||
298
old_maps/frontend_old/Archive/src/stores/garageStore.js
Normal file
298
old_maps/frontend_old/Archive/src/stores/garageStore.js
Normal file
@@ -0,0 +1,298 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useAuthStore } from './authStore'
|
||||
|
||||
export const useGarageStore = defineStore('garage', () => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Real vehicle data - initially empty, will be fetched from API
|
||||
const vehicles = ref([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
// Getters
|
||||
const totalVehicles = computed(() => vehicles.value.length)
|
||||
const totalMonthlyExpense = computed(() =>
|
||||
vehicles.value.reduce((sum, vehicle) => sum + (vehicle.monthlyExpense || 0), 0)
|
||||
)
|
||||
const vehiclesNeedingService = computed(() =>
|
||||
vehicles.value.filter(v => v.status === 'Service Due' || v.status === 'Warning').length
|
||||
)
|
||||
|
||||
// Actions
|
||||
async function addVehicle(vehicle) {
|
||||
// Real API call to POST /api/v1/assets (Thick Asset endpoint)
|
||||
const token = authStore.token
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated')
|
||||
}
|
||||
|
||||
try {
|
||||
// Transform frontend vehicle data to Thick Asset API schema
|
||||
// Include all required fields for thick asset creation
|
||||
const payload = {
|
||||
// Core identification
|
||||
vin: vehicle.vin || null, // Send null for draft vehicles
|
||||
license_plate: vehicle.licensePlate || 'N/A',
|
||||
catalog_id: vehicle.catalogId || null,
|
||||
organization_id: vehicle.organizationId || authStore.activeOrgId,
|
||||
|
||||
// Ownership fields (required by AssetCreate schema)
|
||||
owner_org_id: vehicle.owner_org_id || null,
|
||||
operator_org_id: vehicle.operator_org_id || null,
|
||||
|
||||
// Thick Asset fields - send even if catalog_id is provided for completeness
|
||||
brand: vehicle.brand || vehicle.make || null,
|
||||
model: vehicle.model || null,
|
||||
vehicle_class: vehicle.vehicleClass || vehicle.class || null,
|
||||
fuel_type: vehicle.fuelType || vehicle.fuel || null,
|
||||
year_of_manufacture: vehicle.year || vehicle.yearOfManufacture || null,
|
||||
engine_capacity: vehicle.engineCapacity || null,
|
||||
power_kw: vehicle.powerKw || null,
|
||||
transmission: vehicle.transmission || null,
|
||||
body_type: vehicle.bodyType || null,
|
||||
color: vehicle.color || null,
|
||||
current_mileage: vehicle.currentMileage || vehicle.mileage || 0,
|
||||
|
||||
// Metadata
|
||||
status: vehicle.status || 'draft', // Default to draft for 2-step creation
|
||||
data_status: 'incomplete', // Will be updated by backend snapshot sync
|
||||
profile_completion_percentage: 0 // Will be calculated by backend
|
||||
}
|
||||
|
||||
// Remove null values to keep payload clean
|
||||
Object.keys(payload).forEach(key => {
|
||||
if (payload[key] === null || payload[key] === undefined) {
|
||||
delete payload[key]
|
||||
}
|
||||
})
|
||||
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/api/v1/assets/vehicles`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Failed to add vehicle: ${response.status} ${response.statusText}`
|
||||
|
||||
// Handle specific error cases
|
||||
if (response.status === 409) {
|
||||
errorMessage = 'Duplicate VIN or license plate detected. Please check your input.'
|
||||
} else if (response.status === 429) {
|
||||
errorMessage = 'Rate limit exceeded. Please try again later.'
|
||||
} else if (response.status === 400) {
|
||||
errorMessage = `Invalid input: ${errorText}`
|
||||
} else if (response.status === 403) {
|
||||
errorMessage = 'Permission denied. You may have reached your vehicle limit.'
|
||||
}
|
||||
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log('GarageStore: Thick Asset created successfully', data)
|
||||
|
||||
// After successful save, fetch fresh data from server to ensure consistency
|
||||
await fetchVehicles()
|
||||
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('GarageStore: Error adding vehicle', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
function removeVehicle(id) {
|
||||
// In a real app, this would be an API call to DELETE /assets/{id}
|
||||
vehicles.value = vehicles.value.filter(v => v.id !== id)
|
||||
}
|
||||
|
||||
function updateVehicle(id, updates) {
|
||||
// In a real app, this would be an API call to PATCH /assets/{id}
|
||||
const index = vehicles.value.findIndex(v => v.id === id)
|
||||
if (index !== -1) {
|
||||
vehicles.value[index] = { ...vehicles.value[index], ...updates }
|
||||
}
|
||||
}
|
||||
|
||||
function getVehicleById(id) {
|
||||
return vehicles.value.find(v => v.id === id)
|
||||
}
|
||||
|
||||
// Real API fetch - NO MORE MOCK DATA
|
||||
async function fetchVehicles() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
// Get auth token
|
||||
const token = authStore.token
|
||||
|
||||
if (!token) {
|
||||
console.error('GarageStore: No authentication token available')
|
||||
throw new Error('Not authenticated')
|
||||
}
|
||||
|
||||
console.log('GarageStore: Starting vehicle fetch with token', token.substring(0, 20) + '...')
|
||||
|
||||
// Call real backend API
|
||||
// First try the assets endpoint for user's vehicles
|
||||
const apiUrl = `${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/assets/vehicles`
|
||||
console.log('GarageStore: Fetching from primary endpoint:', apiUrl)
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
console.log('GarageStore: Primary endpoint response status:', response.status, response.statusText)
|
||||
|
||||
if (!response.ok) {
|
||||
// If 404, try alternative endpoint
|
||||
if (response.status === 404) {
|
||||
console.log('GarageStore: Primary endpoint returned 404, trying user assets endpoint')
|
||||
// Try user assets endpoint
|
||||
const userApiUrl = `${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}/users/me/assets`
|
||||
const userResponse = await fetch(userApiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
console.log('GarageStore: User assets endpoint response status:', userResponse.status, userResponse.statusText)
|
||||
|
||||
if (!userResponse.ok) {
|
||||
let errorBody = ''
|
||||
try {
|
||||
errorBody = await userResponse.text()
|
||||
} catch (e) {
|
||||
errorBody = 'Could not read error response'
|
||||
}
|
||||
console.error('GarageStore: User assets endpoint failed:', {
|
||||
status: userResponse.status,
|
||||
statusText: userResponse.statusText,
|
||||
body: errorBody
|
||||
})
|
||||
throw new Error(`Failed to fetch vehicles: ${userResponse.status} ${userResponse.statusText}`)
|
||||
}
|
||||
|
||||
const data = await userResponse.json()
|
||||
console.log('GarageStore: Fetched vehicles from user assets endpoint', data)
|
||||
vehicles.value = transformApiResponse(data)
|
||||
} else {
|
||||
let errorBody = ''
|
||||
try {
|
||||
errorBody = await response.text()
|
||||
} catch (e) {
|
||||
errorBody = 'Could not read error response'
|
||||
}
|
||||
console.error('GarageStore: Primary endpoint failed:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: errorBody
|
||||
})
|
||||
throw new Error(`Failed to fetch vehicles: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
} else {
|
||||
const data = await response.json()
|
||||
console.log('GarageStore: Fetched vehicles from assets endpoint', data)
|
||||
vehicles.value = transformApiResponse(data)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('GarageStore: Error fetching vehicles', err)
|
||||
console.error('GarageStore: Full error stack:', err.stack)
|
||||
error.value = err.message
|
||||
// NO MORE MOCK DATA FALLBACK - fail properly
|
||||
vehicles.value = []
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
console.log('GarageStore: Fetch completed, vehicles count:', vehicles.value.length)
|
||||
return vehicles.value
|
||||
}
|
||||
|
||||
// Helper function to transform API response to frontend format
|
||||
function transformApiResponse(data) {
|
||||
if (!Array.isArray(data)) {
|
||||
// If single object, wrap in array
|
||||
data = [data]
|
||||
}
|
||||
|
||||
return data.map(item => {
|
||||
// Safely extract catalog properties with optional chaining
|
||||
const catalog = item.catalog || {}
|
||||
const catalogMake = catalog?.make || catalog?.brand || null
|
||||
const catalogModel = catalog?.model || null
|
||||
const catalogFuelType = catalog?.fuel_type || catalog?.fuel || null
|
||||
const catalogYear = catalog?.year || catalog?.year_of_manufacture || null
|
||||
|
||||
// Safely extract profile completion percentage
|
||||
const profileCompletion = item.profile_completion_percentage ||
|
||||
item.profile_completion ||
|
||||
(item.profile_status === 'complete' ? 100 : 0) || 0
|
||||
|
||||
// Extract data_status from API (new field for vehicle data completeness)
|
||||
const dataStatus = item.data_status || item.status || (item.is_active ? 'active' : 'draft')
|
||||
|
||||
return {
|
||||
id: item.id || item.asset_id || 0,
|
||||
make: item.make || item.brand || item.vehicle_make || catalogMake || 'Unknown',
|
||||
model: item.model || item.vehicle_model || catalogModel || 'Unknown',
|
||||
year: item.year || item.year_of_manufacture || item.manufacture_year || catalogYear || 2023,
|
||||
licensePlate: item.license_plate || item.registration_number || 'N/A',
|
||||
status: item.status || (item.is_active ? 'active' : 'draft'),
|
||||
data_status: dataStatus,
|
||||
profile_completion_percentage: profileCompletion,
|
||||
monthlyExpense: item.monthly_expense || item.average_monthly_cost || 0,
|
||||
fuelType: item.fuel_type || item.fuel || catalogFuelType || 'Unknown',
|
||||
mileage: item.mileage || item.current_mileage || item.odometer_reading || 0,
|
||||
imageUrl: item.image_url || item.photo_url || getDefaultImage(item.make || item.brand || catalogMake),
|
||||
// Include catalog object for template access with null safety
|
||||
catalog: catalog || null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to get default image based on make
|
||||
function getDefaultImage(make) {
|
||||
// Handle null/undefined make with optional chaining and fallback
|
||||
const makeLower = (make || '').toLowerCase()
|
||||
if (makeLower.includes('bmw')) {
|
||||
return 'https://images.unsplash.com/photo-1555215695-3004980ad54e?w=400&h=300&fit=crop'
|
||||
} else if (makeLower.includes('audi')) {
|
||||
return 'https://images.unsplash.com/photo-1553440569-bcc63803a83d?w=400&h=300&fit=crop'
|
||||
} else if (makeLower.includes('mercedes')) {
|
||||
return 'https://images.unsplash.com/photo-1563720223485-8d6d5c5c8c3b?w=400&h=300&fit=crop'
|
||||
} else if (makeLower.includes('tesla')) {
|
||||
return 'https://images.unsplash.com/photo-1560958089-b8a1929cea89?w=400&h=300&fit=crop'
|
||||
}
|
||||
return 'https://images.unsplash.com/photo-1549399542-7e3f8b79c341?w=400&h=300&fit=crop'
|
||||
}
|
||||
|
||||
return {
|
||||
vehicles,
|
||||
isLoading,
|
||||
error,
|
||||
totalVehicles,
|
||||
totalMonthlyExpense,
|
||||
vehiclesNeedingService,
|
||||
addVehicle,
|
||||
removeVehicle,
|
||||
updateVehicle,
|
||||
getVehicleById,
|
||||
fetchVehicles
|
||||
}
|
||||
})
|
||||
261
old_maps/frontend_old/Archive/src/stores/quizStore.js
Normal file
261
old_maps/frontend_old/Archive/src/stores/quizStore.js
Normal file
@@ -0,0 +1,261 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useQuizStore = defineStore('quiz', () => {
|
||||
// State
|
||||
const userPoints = ref(0)
|
||||
const currentStreak = ref(0)
|
||||
const lastPlayedDate = ref(null)
|
||||
const questions = ref([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
// Getters
|
||||
const canPlayToday = computed(() => {
|
||||
if (!lastPlayedDate.value) return true
|
||||
const last = new Date(lastPlayedDate.value)
|
||||
const now = new Date()
|
||||
// Reset at midnight (different calendar day)
|
||||
const lastDay = last.toDateString()
|
||||
const today = now.toDateString()
|
||||
return lastDay !== today
|
||||
})
|
||||
|
||||
const totalQuestions = computed(() => questions.value.length)
|
||||
|
||||
// Helper function to get auth token
|
||||
function getAuthToken() {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Try both token keys for compatibility
|
||||
return localStorage.getItem('token') || localStorage.getItem('auth_token')
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Helper function for API calls
|
||||
async function apiFetch(url, options = {}) {
|
||||
const token = getAuthToken()
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
}
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'https://dev.servicefinder.hu'}${url}`, {
|
||||
...options,
|
||||
headers
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`API error ${response.status}: ${errorText}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
// Actions
|
||||
async function fetchQuizStats() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await apiFetch('/gamification/quiz/stats')
|
||||
userPoints.value = data.total_quiz_points || 0
|
||||
currentStreak.value = data.current_streak || 0
|
||||
lastPlayedDate.value = data.last_played || null
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch quiz stats:', err)
|
||||
error.value = err.message
|
||||
// Fallback to localStorage if API fails
|
||||
userPoints.value = getStoredPoints()
|
||||
currentStreak.value = getStoredStreak()
|
||||
lastPlayedDate.value = getStoredLastPlayedDate()
|
||||
return {
|
||||
total_quiz_points: userPoints.value,
|
||||
current_streak: currentStreak.value,
|
||||
last_played: lastPlayedDate.value,
|
||||
can_play_today: canPlayToday.value
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDailyQuiz() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await apiFetch('/gamification/quiz/daily')
|
||||
questions.value = data.questions || []
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch daily quiz:', err)
|
||||
error.value = err.message
|
||||
// Fallback to mock questions if API fails
|
||||
questions.value = getMockQuestions()
|
||||
return {
|
||||
questions: questions.value,
|
||||
total_questions: questions.value.length,
|
||||
date: new Date().toISOString().split('T')[0]
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function answerQuestion(questionId, selectedOptionIndex) {
|
||||
try {
|
||||
const response = await apiFetch('/gamification/quiz/answer', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
question_id: questionId,
|
||||
selected_option: selectedOptionIndex
|
||||
})
|
||||
})
|
||||
|
||||
if (response.is_correct) {
|
||||
userPoints.value += response.points_awarded
|
||||
currentStreak.value += 1
|
||||
persistState() // Update localStorage as fallback
|
||||
} else {
|
||||
currentStreak.value = 0
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (err) {
|
||||
console.error('Failed to submit quiz answer:', err)
|
||||
error.value = err.message
|
||||
// Fallback to local logic
|
||||
return answerQuestionLocal(questionId, selectedOptionIndex)
|
||||
}
|
||||
}
|
||||
|
||||
async function completeDailyQuiz() {
|
||||
try {
|
||||
await apiFetch('/gamification/quiz/complete', {
|
||||
method: 'POST'
|
||||
})
|
||||
lastPlayedDate.value = new Date().toISOString()
|
||||
persistState()
|
||||
} catch (err) {
|
||||
console.error('Failed to complete daily quiz:', err)
|
||||
error.value = err.message
|
||||
// Fallback to local storage
|
||||
lastPlayedDate.value = new Date().toISOString()
|
||||
persistState()
|
||||
}
|
||||
}
|
||||
|
||||
// Local fallback functions
|
||||
function answerQuestionLocal(questionId, selectedOptionIndex) {
|
||||
const question = questions.value.find(q => q.id === questionId)
|
||||
if (!question) return { is_correct: false, correct_answer: -1, explanation: 'Question not found' }
|
||||
|
||||
const isCorrect = selectedOptionIndex === question.correctAnswer
|
||||
if (isCorrect) {
|
||||
userPoints.value += 10
|
||||
currentStreak.value += 1
|
||||
} else {
|
||||
currentStreak.value = 0
|
||||
}
|
||||
|
||||
persistState()
|
||||
return {
|
||||
is_correct: isCorrect,
|
||||
correct_answer: question.correctAnswer,
|
||||
points_awarded: isCorrect ? 10 : 0,
|
||||
explanation: question.explanation
|
||||
}
|
||||
}
|
||||
|
||||
function resetStreak() {
|
||||
currentStreak.value = 0
|
||||
persistState()
|
||||
}
|
||||
|
||||
function addPoints(points) {
|
||||
userPoints.value += points
|
||||
persistState()
|
||||
}
|
||||
|
||||
// SSR-safe localStorage persistence (fallback only)
|
||||
function persistState() {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('quiz_points', userPoints.value.toString())
|
||||
localStorage.setItem('quiz_streak', currentStreak.value.toString())
|
||||
localStorage.setItem('quiz_last_played', lastPlayedDate.value)
|
||||
}
|
||||
}
|
||||
|
||||
function getStoredPoints() {
|
||||
if (typeof window !== 'undefined') {
|
||||
return parseInt(localStorage.getItem('quiz_points') || '0')
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function getStoredStreak() {
|
||||
if (typeof window !== 'undefined') {
|
||||
return parseInt(localStorage.getItem('quiz_streak') || '0')
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function getStoredLastPlayedDate() {
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('quiz_last_played') || null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getMockQuestions() {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
question: 'Melyik alkatrész felelős a motor levegő‑üzemanyag keverékének szabályozásáért?',
|
||||
options: ['Generátor', 'Lambda‑szonda', 'Féktárcsa', 'Olajszűrő'],
|
||||
correctAnswer: 1,
|
||||
explanation: 'A lambda‑szonda méri a kipufogógáz oxigéntartalmát, és ezen alapul a befecskendezés.'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
question: 'Mennyi ideig érvényes egy gépjármű műszaki vizsgája Magyarországon?',
|
||||
options: ['1 év', '2 év', '4 év', '6 év'],
|
||||
correctAnswer: 1,
|
||||
explanation: 'A személygépkocsik műszaki vizsgája 2 évre érvényes, kivéve az újonnan forgalomba helyezett autókat.'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
question: 'Melyik anyag NEM része a hibrid autók akkumulátorának?',
|
||||
options: ['Lítium', 'Nikkel', 'Ólom', 'Kobalt'],
|
||||
correctAnswer: 2,
|
||||
explanation: 'A hibrid és elektromos autók akkumulátoraiban általában lítium, nikkel és kobalt található, ólom az ólom‑savas akkukban van.'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Initialize store with stats - DISABLED for debugging
|
||||
// fetchQuizStats()
|
||||
console.log('🚨 Quiz store: Auto-fetch DISABLED for debugging')
|
||||
|
||||
return {
|
||||
userPoints,
|
||||
currentStreak,
|
||||
lastPlayedDate,
|
||||
questions,
|
||||
canPlayToday,
|
||||
totalQuestions,
|
||||
isLoading,
|
||||
error,
|
||||
fetchQuizStats,
|
||||
fetchDailyQuiz,
|
||||
answerQuestion,
|
||||
completeDailyQuiz,
|
||||
resetStreak,
|
||||
addPoints
|
||||
}
|
||||
})
|
||||
43
old_maps/frontend_old/Archive/src/stores/themeStore.js
Normal file
43
old_maps/frontend_old/Archive/src/stores/themeStore.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useThemeStore = defineStore('theme', {
|
||||
state: () => ({
|
||||
currentTheme: 'luxury_showroom', // 'luxury_showroom' or 'rusty_workshop'
|
||||
}),
|
||||
getters: {
|
||||
isLuxury: (state) => state.currentTheme === 'luxury_showroom',
|
||||
isWorkshop: (state) => state.currentTheme === 'rusty_workshop',
|
||||
themeClasses: (state) => {
|
||||
if (state.currentTheme === 'luxury_showroom') {
|
||||
return {
|
||||
background: 'bg-gradient-to-br from-slate-900 via-slate-800 to-gray-900',
|
||||
text: 'text-amber-100',
|
||||
accent: 'text-amber-400',
|
||||
border: 'border-amber-700',
|
||||
card: 'bg-slate-800/70 backdrop-blur-lg border border-amber-700/30',
|
||||
button: 'bg-amber-700 hover:bg-amber-600 text-white',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
background: 'bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900',
|
||||
text: 'text-orange-100',
|
||||
accent: 'text-orange-400',
|
||||
border: 'border-orange-800',
|
||||
card: 'bg-gray-800/90 border border-dashed border-orange-700/50',
|
||||
button: 'bg-orange-800 hover:bg-orange-700 text-white',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
toggleTheme() {
|
||||
this.currentTheme = this.currentTheme === 'luxury_showroom' ? 'rusty_workshop' : 'luxury_showroom'
|
||||
},
|
||||
setTheme(theme) {
|
||||
if (['luxury_showroom', 'rusty_workshop'].includes(theme)) {
|
||||
this.currentTheme = theme
|
||||
}
|
||||
},
|
||||
},
|
||||
persist: true, // optional: if using pinia-plugin-persistedstate
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user