Hello from MCP server
<template>
<BaseLayout :title="menu.name">
<div class="menu-container">
<div class="header-section">
<div class="title-container">
<div class="title-icons">
<ion-icon :icon="arrowBack" @click="goBack" class="clickable-icon"></ion-icon>
</div>
<h1 class="page-title">{{ getMenuTitle() || 'Menu Options' }}</h1>
<div class="title-icons-right">
<ion-button
fill="solid"
:color="isClicked ? 'primary' : 'medium'"
size="small"
class="sa-button"
@click="toggleButton"
>
SA
</ion-button>
</div>
</div>
</div>
<div class="content-section">
<div class="tiers-grid">
<div
v-for="tier in menu?.tiers"
:key="tier.id || tier.name"
v-show="!isTierHidden(tier.id)"
class="tier-card"
:class="getTierClass(tier.name)"
@click="selectTier(tier)"
>
<!-- Tier Badge -->
<div class="tier-badge" :class="getTierBadgeClass(tier.name)">
<span class="tier-name">{{ tier.name }}</span>
</div>
<!-- Tier Content -->
<div class="tier-content">
<h3 class="tier-title">{{ getTierOfferTitle(tier) || tier.title?.toUpperCase() || tier.name }}</h3>
<!-- Features List -->
<div class="features-section">
<template v-if="tier.menuCopy">
<div
v-for="copy in tier.menuCopy"
:key="copy.id"
class="feature-item"
>
<div
v-for="item in copy.contentItems"
:key="item.id"
class="feature-text"
>
<ion-icon :icon="checkmarkCircle" class="feature-icon"></ion-icon>
<span>{{ item.content }}</span>
</div>
</div>
</template>
<template v-else-if="tier.contentItems">
<div
v-for="item in tier.contentItems"
:key="item.id"
v-show="!item.refId?.includes('_title')"
class="feature-text"
>
<ion-icon :icon="checkmarkCircle" class="feature-icon"></ion-icon>
<span>{{ item.content }}</span>
</div>
</template>
</div>
<!-- Pricing Section -->
<div class="pricing-section">
<div v-if="isClicked && tier.discountPrice" class="discount-price">
<show-currency :currencyIn="tier.discountPrice" />
</div>
<div class="price-amount" :class="{ 'price-crossed': isClicked && tier.discountPrice }">
<show-currency :currencyIn="tier.price" />
</div>
<div class="warranty-text" v-if="tier.warranty">
{{ tier.warranty }}
</div>
</div>
<!-- Select Button -->
<div class="select-button-wrapper">
<ion-button expand="block" color="primary" class="select-button">
Select {{ tier.name }}
</ion-button>
</div>
</div>
</div>
</div>
</div>
</div>
</BaseLayout>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { getDb } from "@/dataAccess/getDb";
import BaseLayout from "@/components/BaseLayout.vue";
import ShowCurrency from "@/components/ShowCurrency.vue";
import calculatePrice from "@/framework/calculatePrice";
import legacyFormula from "@/lib/legacyFormula";
import legacyFormulaDiscount from "@/lib/legacyFormulaDiscount";
import { useSessionStore } from "@/stores/session";
import { useOrganizationStore } from "@/stores/organization";
import {
IonIcon,
IonButton,
} from "@ionic/vue";
import { arrowBack, checkmarkCircle } from "ionicons/icons";
const sessionStore = useSessionStore();
const orgStore = useOrganizationStore();
const route = useRoute();
const router = useRouter();
const menu = ref<any>({ name: "" });
const menuId = ref<string>("");
const isClicked = ref(false);
const goBack = () => {
router.back();
};
// Get current job and its menu modifications
const getCurrentJob = () => {
const jobId = route.params.id as string;
return sessionStore.jobs.find((j) => j.id === jobId);
};
// Get modified menu title or default
const getMenuTitle = () => {
const job = getCurrentJob();
return job?.menuModifications?.menuTitle || menu.value.name;
};
// Check if tier is hidden
const isTierHidden = (tierId: string) => {
const job = getCurrentJob();
return job?.menuModifications?.tierModifications?.[tierId]?.hidden || false;
};
// Get modified offer title for a tier
const getTierOfferTitle = (tier: any) => {
const job = getCurrentJob();
return job?.menuModifications?.tierModifications?.[tier.id]?.offerTitle || null;
};
function getTierClass(tierName: string): string {
const name = tierName?.toLowerCase() || "";
if (name.includes("platinum")) return "tier-platinum";
if (name.includes("gold")) return "tier-gold";
if (name.includes("silver")) return "tier-silver";
if (name.includes("bronze")) return "tier-bronze";
if (name.includes("band")) return "tier-bandaid";
return "";
}
function getTierBadgeClass(tierName: string): string {
const name = tierName?.toLowerCase() || "";
if (name.includes("platinum")) return "badge-platinum";
if (name.includes("gold")) return "badge-gold";
if (name.includes("silver")) return "badge-silver";
if (name.includes("bronze")) return "badge-bronze";
if (name.includes("band")) return "badge-bandaid";
return "";
}
function selectTier(tier: any) {
const currentMenuId = menuId.value;
const tierId = tier.id;
if ((route.name as string)?.includes("job.show")) {
const jobId = route.params.id;
router.push(`/job/${jobId}/confirm/${tierId}`);
} else {
router.push(`/menus/${currentMenuId}/confirm/${tierId}`);
}
}
function populateFormulaVars(f: any, offer: any, extraCostMultiplier: number, useDiscount = false) {
f.vars.hourlyFee = orgStore.hourlyFee;
f.vars.serviceCallFee = orgStore.serviceCallFee;
f.vars.saDiscount = useDiscount ? 0.97 : 1; // 3% discount when active
f.vars.salesTax = orgStore.salesTax;
f.vars.multiplier = parseFloat(offer.multiplier);
f.vars.extraCostMultiplier = extraCostMultiplier;
return f;
}
onMounted(async () => {
try {
const db = await getDb();
await orgStore.loadVariables();
await sessionStore.load();
const job = sessionStore.jobs.find((e) => e.id == route.params.id);
let currentMenuId = "";
let menuResponse = null;
if ((route.name as string).includes("job.show")) {
// Job-based menu display - use cached menu data from job
if (job) {
if (job.menuData) {
// Use cached menu data - no database fetch needed!
console.log('[MenuTemplateDev] Using cached menu data from job');
menuResponse = job.menuData;
menuId.value = menuResponse.id;
} else if (db && job.problem?.menus) {
// Fallback: fetch from database if not cached (shouldn't happen with new flow)
console.log('[MenuTemplateDev] Menu data not cached, fetching from database');
const menus = job.problem.menus;
if (typeof menus === 'string') {
try {
const parsed = JSON.parse(menus);
currentMenuId = Array.isArray(parsed) ? parsed[0] : menus;
} catch (e) {
currentMenuId = menus;
}
} else if (Array.isArray(menus)) {
currentMenuId = menus[0];
}
menuId.value = currentMenuId;
menuResponse = await db.menus.byMenuId(currentMenuId);
}
}
} else if (db) {
// Direct menu browsing (not from job) - fetch from database
currentMenuId = route.params.id as string;
menuId.value = currentMenuId;
menuResponse = await db.menus.byMenuId(currentMenuId);
}
if (menuResponse) {
menu.value = menuResponse;
let extraCostMultiplier = 1;
if (job) {
extraCostMultiplier = job.extraTime * job.extraMaterial;
}
for (const tier of menu.value.tiers) {
const f = populateFormulaVars(
legacyFormula(),
tier.offer,
extraCostMultiplier,
);
const result = await calculatePrice(
tier.offer.costsMaterial,
tier.offer.costsTime,
f,
);
tier.price = result.finalPrice;
tier.priceConverted = result.finalPriceConverted;
const fDiscount = populateFormulaVars(
legacyFormulaDiscount(),
tier.offer,
extraCostMultiplier,
true, // Apply 3% discount
);
const discountResult = await calculatePrice(
tier.offer.costsMaterial,
tier.offer.costsTime,
fDiscount,
);
tier.discountPrice = discountResult.finalPrice;
tier.discountPriceConverted = discountResult.finalPriceConverted;
const titleItem = tier.contentItems?.find((e: any) =>
e.refId?.includes("_title"),
);
if (titleItem) {
tier.title = titleItem.content;
}
const tierName = tier.name?.toLowerCase() || "";
if (tierName.includes("platinum")) {
tier.warranty = "2 year limited warranty";
} else if (tierName.includes("gold")) {
tier.warranty = "18 month limited warranty";
} else if (tierName.includes("silver")) {
tier.warranty = "1 year limited warranty";
} else if (tierName.includes("bronze")) {
tier.warranty = "6 month limited warranty";
} else if (tierName.includes("band")) {
tier.warranty = "30 day limited warranty";
}
}
}
} catch (error) {
console.error("Error loading menu:", error);
}
});
async function toggleButton() {
isClicked.value = !isClicked.value;
if (menu.value?.tiers) {
const job = sessionStore.jobs.find((e) => e.id == route.params.id);
let extraCostMultiplier = 1;
if (job) {
extraCostMultiplier = job.extraTime * job.extraMaterial;
}
for (const tier of menu.value.tiers) {
if (isClicked.value) {
const fDiscount = populateFormulaVars(
legacyFormulaDiscount(),
tier.offer,
extraCostMultiplier,
true, // Apply 3% discount
);
const discountResult = await calculatePrice(
tier.offer.costsMaterial,
tier.offer.costsTime,
fDiscount,
);
tier.discountPrice = discountResult.finalPrice;
tier.discountPriceConverted = discountResult.finalPriceConverted;
}
}
}
}
</script>
<style scoped>
.menu-container {
padding: 20px;
max-width: 1400px;
margin: 0 auto;
}
.header-section {
margin-bottom: 32px;
}
.title-container {
position: relative;
margin-bottom: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.title-icons {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
display: flex;
gap: 16px;
}
.title-icons ion-icon {
font-size: 48px;
}
.title-icons-right {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
}
.clickable-icon {
cursor: pointer;
transition: transform 0.2s ease, color 0.2s ease;
color: var(--ion-color-light);
}
.clickable-icon:hover {
transform: scale(1.1);
color: var(--ion-color-primary-shade);
}
.clickable-icon:active {
transform: scale(0.95);
}
.page-title {
margin: 0;
color: #ffffff;
font-size: 48px;
font-weight: 700;
text-align: center;
font-variant: small-caps;
}
.sa-button {
--padding-start: 12px;
--padding-end: 12px;
font-weight: 700;
font-size: 14px;
height: 40px;
}
.content-section {
padding: 0 20px 20px 20px;
}
.tiers-grid {
display: flex;
flex-direction: column;
gap: 24px;
max-width: 800px;
margin: 0 auto;
}
.tier-card {
background-color: var(--ion-color-dark);
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
border: 2px solid transparent;
display: flex;
flex-direction: column;
}
.tier-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
/* Tier-specific border colors */
.tier-platinum {
border-color: var(--menu-color-platinum, #3db4d6);
}
.tier-gold {
border-color: var(--menu-color-gold, #ffd83b);
}
.tier-silver {
border-color: var(--menu-color-silver, #bfbfbf);
}
.tier-bronze {
border-color: var(--menu-color-bronze, #ffad2b);
}
.tier-bandaid {
border-color: var(--menu-color-bandaid, #ff8073);
}
.tier-badge {
padding: 16px 20px;
text-align: center;
font-weight: 700;
font-size: 24px;
text-transform: uppercase;
color: #000;
}
.badge-platinum {
background: linear-gradient(135deg, var(--menu-color-platinum, #3db4d6) 0%, var(--menu-color-platinum-tint, #5ec3dd) 100%);
}
.badge-gold {
background: linear-gradient(135deg, var(--menu-color-gold, #ffd83b) 0%, var(--menu-color-gold-tint, #ffe066) 100%);
}
.badge-silver {
background: linear-gradient(135deg, var(--menu-color-silver, #bfbfbf) 0%, var(--menu-color-silver-tint, #d4d4d4) 100%);
}
.badge-bronze {
background: linear-gradient(135deg, var(--menu-color-bronze, #ffad2b) 0%, var(--menu-color-bronze-tint, #ffc04d) 100%);
}
.badge-bandaid {
background: linear-gradient(135deg, var(--menu-color-bandaid, #ff8073) 0%, var(--menu-color-bandaid-tint, #ff9a8f) 100%);
}
.tier-name {
display: block;
line-height: 1.2;
}
.tier-content {
padding: 24px;
flex: 1;
display: flex;
flex-direction: column;
}
.tier-title {
margin: 0 0 20px 0;
color: #ffffff;
font-size: 20px;
font-weight: 600;
text-align: center;
line-height: 1.3;
}
.features-section {
flex: 1;
margin-bottom: 24px;
}
.feature-item {
margin-bottom: 8px;
}
.feature-text {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
color: var(--ion-color-light);
font-size: 15px;
line-height: 1.5;
}
.feature-icon {
color: var(--ion-color-primary);
font-size: 20px;
flex-shrink: 0;
margin-top: 2px;
}
.pricing-section {
text-align: center;
margin-bottom: 20px;
padding-top: 20px;
border-top: 1px solid var(--ion-color-medium);
}
.discount-price {
color: var(--ion-color-primary);
font-size: 24px;
font-weight: 700;
margin-bottom: 8px;
}
.price-amount {
color: #ffffff;
font-size: 32px;
font-weight: 700;
margin-bottom: 12px;
}
.price-crossed {
text-decoration: line-through;
opacity: 0.5;
font-size: 24px;
}
.warranty-text {
color: var(--ion-color-medium);
font-size: 13px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.select-button-wrapper {
margin-top: auto;
}
.select-button {
--padding-top: 16px;
--padding-bottom: 16px;
font-weight: 600;
font-size: 16px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Mobile adjustments */
@media (max-width: 767px) {
.menu-container {
padding: 16px;
}
.page-title {
font-size: 36px;
}
.tier-badge {
font-size: 20px;
padding: 12px 16px;
}
.tier-title {
font-size: 18px;
}
.feature-text {
font-size: 14px;
}
.price-amount {
font-size: 28px;
}
}
</style>