Hello from MCP server
<template>
<BaseLayout title="Menu">
<template #header-buttons>
<ion-button fill="clear" @click="toggleTechHandbook">
<ion-icon :icon="showTechHandbook ? closeOutline : helpCircleOutline" color="primary" style="font-size: 36px;" />
</ion-button>
</template>
<div class="menu-container">
<!-- Menu Page Heading -->
<div class="menu-page-heading">
<h1>What Should We Do?</h1>
<h2 v-if="currentJobName" class="job-name-subtitle">{{ currentJobName }}</h2>
<ion-chip v-if="currentOfferRefId" :class="refIdChipClass">{{ currentOfferRefId }}</ion-chip>
</div>
<div v-if="!currentJob" class="empty-state">
<p>Job not found</p>
<ion-button @click="router.push('/cart')">Back to Cart</ion-button>
</div>
<template v-else>
<!-- Agreement Options with Navigation Arrows -->
<div v-if="!showTechHandbook" class="agreement-nav-row">
<ion-button
fill="solid"
class="nav-arrow nav-arrow-left"
:class="{ 'nav-arrow-disabled': isSingleMenu }"
:disabled="isSingleMenu"
@click="handlePrevClick"
>
<ion-icon slot="icon-only" :icon="chevronBackOutline" />
</ion-button>
<div class="agreement-card">
<div class="agreement-row">
<div class="agreement-option" @click="toggleServiceAgreement">
<ion-icon
:icon="tnfrStore.applyDiscount ? checkmarkCircle : ellipseOutline"
:class="{ 'checked': tnfrStore.applyDiscount }"
class="agreement-icon"
/>
<span class="agreement-label">Service Agreement</span>
</div>
<div class="agreement-option" @click="togglePaymentPlan">
<ion-icon
:icon="tnfrStore.showPlan ? checkmarkCircle : ellipseOutline"
:class="{ 'checked': tnfrStore.showPlan }"
class="agreement-icon"
/>
<span class="agreement-label">Payment Plan</span>
</div>
</div>
</div>
<ion-button
fill="solid"
class="nav-arrow nav-arrow-right"
:class="{ 'nav-arrow-disabled': isSingleMenu }"
:disabled="isSingleMenu"
@click="handleNextClick"
>
<ion-icon slot="icon-only" :icon="chevronForwardOutline" />
</ion-button>
</div>
<!-- Tech Handbook View -->
<div v-if="showTechHandbook" class="tech-handbook-view">
<div class="tech-handbook-description" v-if="problemDescription">
<h3>Description</h3>
<p>{{ problemDescription }}</p>
</div>
<div v-for="tier in menuTiers" :key="tier.id || tier.name" class="tech-handbook-tier">
<h3 class="tech-handbook-tier-name">{{ tier.name }}</h3>
<ul v-if="tier.offer?.techHandbookExpanded?.length || tier.offer?.techHandbook?.length" class="tech-handbook-list">
<li v-for="(item, idx) in (tier.offer?.techHandbookExpanded || tier.offer?.techHandbook || [])" :key="idx">
{{ item.content || item }}
</li>
</ul>
<p v-else class="tech-handbook-empty">No tech handbook content available</p>
</div>
</div>
<!-- Menu Grid View -->
<div v-else class="menu-grid">
<LegacyMenuSection
v-for="tier in menuTiers"
:key="tier.id || tier.name"
:tier-name="tier.name"
:tier-title="tier.contentItems?.map((item: any) => item.content).join(' ') || tier.title"
:tier-class="getTierClass(tier.name)"
:menu-copy="tier.menuCopy"
:content-items="tier.contentItems"
:price="tier.price"
:price-converted="tier.priceConverted"
:discount-price="tier.discountPrice"
:discount-price-converted="tier.discountPriceConverted"
:warranty="tier.warranty"
:show-discount="tnfrStore.applyDiscount"
:show-plan="tnfrStore.showPlan"
:selected="isTierSelected(tier)"
@click="selectTier(tier)"
/>
</div>
</template>
</div>
</BaseLayout>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { IonButton, IonIcon, IonChip } from "@ionic/vue";
import { checkmarkCircle, ellipseOutline, chevronBackOutline, chevronForwardOutline, helpCircleOutline, closeOutline } from "ionicons/icons";
import BaseLayout from "@/components/BaseLayout.vue";
import LegacyMenuSection from "@/components/LegacyMenuSection.vue";
import { useTnfrStore } from "@/stores/tnfr";
const route = useRoute();
const router = useRouter();
const tnfrStore = useTnfrStore();
// Tech handbook toggle state
const showTechHandbook = ref(false);
function toggleTechHandbook() {
showTechHandbook.value = !showTechHandbook.value;
}
// Load session state from database on mount
onMounted(async () => {
await tnfrStore.load();
});
// Get job ID from route
const jobId = computed(() => route.params.jobId as string);
// Jobs sorted by highest price offer to lowest
const sortedJobs = computed(() => {
return [...tnfrStore.jobs].sort((a, b) => {
const priceA = a.menus?.[0]?.tiers?.[0]?.price || 0;
const priceB = b.menus?.[0]?.tiers?.[0]?.price || 0;
return priceB - priceA; // highest first
});
});
// Check if there's only one menu
const isSingleMenu = computed(() => {
return sortedJobs.value.length <= 1;
});
// Current job from jobs array only
const currentJob = computed(() => {
if (!jobId.value) return null;
return tnfrStore.jobs.find(j => j.id === jobId.value) || null;
});
// Job name for display (from menu name)
const currentJobName = computed(() => {
return currentJob.value?.menus?.[0]?.name || '';
});
// Offer refId for display
const currentOfferRefId = computed(() => {
const job = currentJob.value;
if (!job) return '';
// If there's a selected offer, show its refId
if (job.selectedOffer?.refId) {
return job.selectedOffer.refId;
}
// Otherwise show first tier's refId with last character removed
const baseRefId = job.menus?.[0]?.tiers?.[0]?.offer?.refId || '';
return baseRefId.slice(0, -1);
});
// Chip class based on selected tier
const refIdChipClass = computed(() => {
const job = currentJob.value;
if (!job?.selectedTierName) {
return 'refid-chip refid-chip-none';
}
const tierName = job.selectedTierName.toLowerCase();
if (tierName.includes('platinum')) return 'refid-chip refid-chip-platinum';
if (tierName.includes('gold')) return 'refid-chip refid-chip-gold';
if (tierName.includes('silver')) return 'refid-chip refid-chip-silver';
if (tierName.includes('bronze')) return 'refid-chip refid-chip-bronze';
if (tierName.includes('band')) return 'refid-chip refid-chip-bandaid';
return 'refid-chip refid-chip-none';
});
// Problem description for info modal
const problemDescription = computed(() => {
return currentJob.value?.problem?.description || '';
});
// Service agreement discount rate (3% discount)
const SERVICE_AGREEMENT_RATE = 0.97;
// Warranty text based on tier name
function getWarrantyText(tierName: string): string {
const name = tierName?.toLowerCase() || '';
if (name.includes('diamond') || name.includes('platinum')) return '2 year limited warranty';
if (name.includes('gold')) return '18 month limited warranty';
if (name.includes('silver')) return '1 year limited warranty';
if (name.includes('bronze')) return '6 month limited warranty';
if (name.includes('economy') || name.includes('band')) return '30 day limited warranty';
return '';
}
// Menu tiers from the job
const menuTiers = computed(() => {
const menu = currentJob.value?.menus?.[0];
if (!menu?.tiers) return [];
return menu.tiers.map((t: any) => {
const price = t.price || 0;
const priceConverted = t.priceConverted?.formatted || '';
// Calculate discount price using service agreement rate
const discountPrice = t.discountPrice || (price * SERVICE_AGREEMENT_RATE);
// Calculate converted discount price
let discountPriceConverted = '';
if (t.priceConverted) {
const symbol = t.priceConverted.symbol || '';
const discountValue = (t.priceConverted.value || 0) * SERVICE_AGREEMENT_RATE;
discountPriceConverted = `${symbol}${Math.ceil(discountValue).toLocaleString()}`;
}
const tierName = t.tier?.name || t.name;
return {
id: t.id,
refId: t.refId,
name: tierName,
title: tierName,
rank: t.tier?.rank,
warranty: t.tier?.warrantyCopy || getWarrantyText(tierName),
offer: t.offer,
menuCopy: t.menuCopy,
contentItems: t.contentItems,
price,
priceConverted,
discountPrice,
discountPriceConverted,
};
});
});
// Get CSS class for tier styling
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 '';
}
// Toggle service agreement
async function toggleServiceAgreement() {
tnfrStore.applyDiscount = !tnfrStore.applyDiscount;
await tnfrStore.save();
}
// Toggle payment plan
async function togglePaymentPlan() {
tnfrStore.showPlan = !tnfrStore.showPlan;
await tnfrStore.save();
}
// Check if tier is selected
function isTierSelected(tier: any): boolean {
const job = currentJob.value;
if (tier.offer && job?.selectedOffer) {
return job.selectedOffer.id === tier.offer.id;
}
return false;
}
// Handle tier selection
async function selectTier(tier: any) {
const job = currentJob.value;
if (!job || !tier.offer) return;
const price = tier.price;
const tierBaseHours = tier.offer?.costsTime?.reduce(
(sum: number, cost: any) => sum + (cost.hours || 0),
0
) || 0;
// Extract content from menuCopy or contentItems
const tierContent: string[] = [];
if (tier.menuCopy?.length > 0) {
tier.menuCopy.forEach((copy: any) => {
copy.contentItems?.forEach((item: any) => {
if (item.content) tierContent.push(item.content);
});
});
} else if (tier.contentItems?.length > 0) {
tier.contentItems.forEach((item: any) => {
if (item.content) tierContent.push(item.content);
});
}
await tnfrStore.setSelectedOffer(
job.id,
tier.offer,
price.toString(),
tier.name,
tier.title,
tierBaseHours,
tierContent
);
}
// Navigate to next menu (cycles through, never goes to payment)
function navigateToNext() {
const job = currentJob.value;
if (!job) return;
const jobs = sortedJobs.value;
if (jobs.length === 0) return;
const idx = jobs.findIndex(j => j.id === job.id);
if (idx === -1) {
router.push(`/menu/${jobs[0].id}`);
return;
}
// Cycle to next menu (wrap around at end)
const nextIdx = (idx + 1) % jobs.length;
router.push(`/menu/${jobs[nextIdx].id}`);
}
// Navigate to previous menu (cycles through)
function navigatePrev() {
const job = currentJob.value;
if (!job) return;
const jobs = sortedJobs.value;
if (jobs.length === 0) return;
const idx = jobs.findIndex(j => j.id === job.id);
if (idx === -1) {
router.push(`/menu/${jobs[0].id}`);
return;
}
// Cycle to previous menu (wrap around at beginning)
const prevIdx = (idx - 1 + jobs.length) % jobs.length;
router.push(`/menu/${jobs[prevIdx].id}`);
}
// Handle prev arrow click
function handlePrevClick() {
if (!isSingleMenu.value) {
navigatePrev();
}
}
// Handle next arrow click
function handleNextClick() {
if (!isSingleMenu.value) {
navigateToNext();
}
}
</script>
<style scoped>
.menu-container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.menu-page-heading {
text-align: center;
margin-bottom: 12px;
}
.menu-page-heading h1 {
font-size: 32px;
font-weight: 700;
margin: 0 0 8px 0;
color: var(--ion-text-color);
}
.job-name-subtitle {
font-size: 24px;
font-weight: 500;
margin: 0;
color: var(--ion-color-primary);
text-transform: capitalize;
}
.refid-chip {
font-size: 14px;
font-weight: 600;
margin-top: 8px;
}
.refid-chip-none {
--background: transparent;
--color: var(--ion-color-medium);
border: 2px solid var(--ion-color-medium);
}
.refid-chip-platinum {
--background: var(--menu-color-platinum, #3db4d6);
--color: #fff;
}
.refid-chip-gold {
--background: var(--menu-color-gold, #ffd83b);
--color: #000;
}
.refid-chip-silver {
--background: var(--menu-color-silver, #bfbfbf);
--color: #000;
}
.refid-chip-bronze {
--background: var(--menu-color-bronze, #ffad2b);
--color: #000;
}
.refid-chip-bandaid {
--background: var(--menu-color-bandaid, #ff8073);
--color: #fff;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 300px;
gap: 16px;
}
.empty-state p {
font-size: 18px;
color: var(--ion-color-medium);
}
.menu-grid {
display: flex;
flex-direction: column;
gap: 0;
}
.agreement-nav-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
width: 100%;
}
.nav-arrow {
--padding-start: 12px;
--padding-end: 12px;
--background: var(--ion-color-primary);
--background-hover: var(--ion-color-primary-shade);
--color: var(--ion-color-primary-contrast);
--border-radius: 50%;
width: 48px;
height: 48px;
min-width: 48px;
}
.nav-arrow ion-icon {
font-size: 28px;
}
.nav-arrow-disabled {
--background: var(--ion-color-light);
--color: var(--ion-color-medium);
opacity: 0.5;
cursor: default;
}
.agreement-card {
background: var(--ion-color-light);
border-radius: 8px;
padding: 12px 16px;
}
.agreement-row {
display: flex;
align-items: center;
gap: 32px;
}
.agreement-option {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
transition: opacity 0.2s ease;
}
.agreement-option:hover {
opacity: 0.8;
}
.agreement-icon {
font-size: 24px;
color: var(--ion-color-medium);
transition: color 0.2s ease;
margin: 0;
padding: 0;
}
.agreement-icon.checked {
color: var(--ion-color-success);
}
.agreement-label {
font-size: 16px;
font-weight: 500;
color: var(--ion-text-color);
}
/* Tech Handbook View */
.tech-handbook-view {
padding: 16px;
background: var(--ion-background-color);
}
.tech-handbook-description {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--ion-color-light);
}
.tech-handbook-description h3 {
font-size: 18px;
font-weight: 600;
margin: 0 0 8px 0;
color: var(--ion-text-color);
}
.tech-handbook-description p {
font-size: 16px;
line-height: 1.5;
margin: 0;
color: var(--ion-text-color);
}
.tech-handbook-tier {
margin-bottom: 24px;
}
.tech-handbook-tier-name {
font-size: 18px;
font-weight: 600;
margin: 0 0 12px 0;
color: var(--ion-text-color);
text-transform: uppercase;
}
.tech-handbook-list {
margin: 0;
padding-left: 20px;
list-style-type: disc;
}
.tech-handbook-list li {
font-size: 15px;
line-height: 1.6;
color: var(--ion-text-color);
margin-bottom: 6px;
}
.tech-handbook-empty {
font-size: 14px;
color: var(--ion-color-medium);
font-style: italic;
margin: 0;
}
</style>