Hello from MCP server
<!-- BaseLayout.vue -->
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button fill="clear" @click="openMenu">
<ion-icon :icon="menuOutline" class="menu-icon"></ion-icon>
</ion-button>
</ion-buttons>
<ion-title class="toolbar-title">
<img
:src="customLogo || '/images/branding-dark.png'"
alt="Logo"
class="toolbar-logo clickable-logo"
@click="goHome"
/>
</ion-title>
<ion-buttons slot="end">
<slot name="header-buttons" />
<ion-button
v-if="tnfrStore.canGoToPayment && !tnfrStore.invoice.paymentConfirmed"
fill="solid"
color="success"
size="small"
@click="goToPayment"
class="payment-button"
>
Go To Payment
</ion-button>
<ion-button v-if="route.path !== '/review'" @click="goToCart" class="cart-button">
<ion-icon :icon="tnfrStore.invoice.paymentConfirmed ? checkmarkCircle : cart" color="primary" />
<span v-if="tnfrStore.jobs.length > 0" class="cart-badge">{{ tnfrStore.jobs.length }}</span>
</ion-button>
<!-- <ion-button @click="openOptions">
<ion-icon :icon="ellipsisVertical" />
</ion-button> -->
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<slot />
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import { onMounted, computed, ref } from "vue";
import {
IonButton,
IonButtons,
IonContent,
IonHeader,
IonIcon,
IonMenuButton,
IonPage,
IonTitle,
IonToolbar,
popoverController,
menuController,
} from "@ionic/vue";
import { sunnyOutline, moonOutline, skullOutline, menuOutline, arrowBack, cart, checkmarkCircle } from "ionicons/icons";
import { ellipsisVertical } from "ionicons/icons";
import PopoverOptions from "@/components/PopoverOptions.vue";
import { useCurrencyStore } from "@/stores/currency";
import { usePreferencesStore } from "@/stores/preferences";
import { useSessionStore } from "@/stores/session";
import { useTnfrStore } from "@/stores/tnfr";
import { useRouter, useRoute } from "vue-router";
import { useTheme } from "@/composables/useTheme";
import { useLogo } from "@/composables/useLogo";
const props = defineProps<{
title: string;
}>();
const emit = defineEmits(["debug"]);
const currencyStore = useCurrencyStore();
const preferences = usePreferencesStore();
const sessionStore = useSessionStore();
const tnfrStore = useTnfrStore();
const router = useRouter();
const route = useRoute();
const { reapplyTheme } = useTheme();
const { logo, loadLogos } = useLogo();
// Use computed to reactively get the logo
const customLogo = computed(() => logo.value);
const isTechnicianTitleClickable = computed(() => {
return props.title === "Technician" && route.path !== "/service-call";
});
const roleLabels: Record<string, string> = {
'tech-in-training': 'Tech in Training',
'tech': 'Tech',
'author': 'Author',
'admin': 'Admin',
};
const roleLabel = computed(() => roleLabels[preferences.role] || preferences.role);
const handleTitleClick = () => {
if (isTechnicianTitleClickable.value) {
router.push("/service-call");
}
};
// Watch for dark mode preference changes
preferences.$subscribe((mutation, state) => {
document.documentElement.classList.toggle("ion-palette-dark", state.darkMode);
// Re-apply theme colors for the new mode
reapplyTheme();
});
const setCurrency = async (currency: string) => {
await preferences.setPreference("currency", currency);
};
const setRole = async (role: string) => {
await preferences.setPreference("role", role);
};
// Track consecutive taps on dark mode toggle for secret navigation
const darkModeTapCount = ref(0);
let darkModeTapTimeout: ReturnType<typeof setTimeout> | null = null;
const toggleDarkMode = async () => {
await preferences.setPreference("darkMode", String(!preferences.darkMode));
// Track consecutive taps
darkModeTapCount.value++;
// Clear existing timeout
if (darkModeTapTimeout) {
clearTimeout(darkModeTapTimeout);
}
// Check for 3+ consecutive taps
if (darkModeTapCount.value >= 3) {
darkModeTapCount.value = 0;
if (route.path === "/framework") {
router.push("/");
} else {
router.push("/framework");
}
return;
}
// Reset tap count after 500ms of no taps
darkModeTapTimeout = setTimeout(() => {
darkModeTapCount.value = 0;
}, 500);
};
const openMenu = async () => {
await menuController.open('main-menu');
};
const goHome = () => {
router.push('/');
};
const goToCart = () => {
if (tnfrStore.invoice.paymentConfirmed) {
router.push('/review');
} else {
router.push('/cart');
}
};
const goToPayment = () => {
router.push('/payment');
};
async function openOptions(e: Event) {
const popover = await popoverController.create({
component: PopoverOptions,
componentProps: {
currencies: currencyStore.currencies,
setCurrency,
onDebug: () => emit("debug"),
onRoleChange: setRole,
},
event: e,
});
await popover.present();
await popover.onDidDismiss();
}
onMounted(async () => {
await currencyStore.getCurrencies();
await currencyStore.getRates();
await preferences.getPreferences();
// Sync currency prefs to userPrefs table (one-way, for framework module use)
if (preferences.currency) {
await currencyStore.syncCurrencyPrefs(preferences.currency);
}
// Apply dark mode based on user preference
document.documentElement.classList.toggle("ion-palette-dark", preferences.darkMode);
// Load custom logos
await loadLogos();
});
</script>
<style scoped>
/* Make all toolbar icons larger */
ion-toolbar ion-icon {
font-size: 36px;
}
.menu-icon {
font-size: 36px;
color: var(--ion-text-color);
}
.back-button {
--padding-start: 12px;
--padding-end: 12px;
font-weight: 600;
}
.payment-button {
--padding-start: 12px;
--padding-end: 12px;
font-weight: 600;
}
.toolbar-title {
text-align: center;
}
.toolbar-logo {
height: 32px;
width: auto;
max-width: 200px;
object-fit: contain;
}
.clickable-logo {
cursor: pointer;
transition: opacity 0.2s ease;
}
.clickable-logo:hover {
opacity: 0.7;
}
.clickable-logo:active {
opacity: 0.5;
}
.cart-button {
position: relative;
}
.cart-badge {
position: absolute;
top: 4px;
right: 4px;
background-color: var(--ion-color-danger);
color: white;
font-size: 11px;
font-weight: 700;
min-width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
}
</style>