Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
<!-- 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>