Hello from MCP server
<template>
<ion-page>
<ion-content>
<div class="page-wrapper">
<!-- Debug Toolbar -->
<DebugToolbar
ref="debugToolbar"
:function-groups="functionGroups"
:reactive-data="reactiveVarsData"
>
<template #info-sections>
<div class="debug-section">
<strong>Route:</strong> {{ route.name }} | ID: {{ route.params.id }}
</div>
<div class="debug-section">
<strong>Current Job:</strong> {{ currentJob?.id || 'Not found' }} | Title: {{ currentJob?.title || 'N/A' }}
</div>
<div class="debug-section">
<strong>Menu:</strong> {{ menu?.name || 'N/A' }} | Tiers: {{ menu?.tiers?.length || 0 }}
</div>
<div class="debug-section">
<strong>Org Vars:</strong> hourlyFee={{ tnfrStore.hourlyFee }}, serviceCallFee={{ tnfrStore.serviceCallFee }}, salesTax={{ tnfrStore.salesTax }}
</div>
<div class="debug-section">
<strong>Jobs in Store:</strong> {{ tnfrStore.jobs.length }}
<span v-for="job in tnfrStore.jobs" :key="job.id" class="debug-job-chip">
{{ job.id.slice(-6) }}
</span>
</div>
</template>
</DebugToolbar>
<!-- Menu Page Heading -->
<div class="menu-page-heading">
<h1>What Should we do?</h1>
<h2 v-if="currentJobName" class="job-name-subtitle">{{ currentJobName }}</h2>
</div>
<div class="menu-grid">
<LegacyMenuSection
v-for="tier in menu?.tiers"
: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"
:discount-price="tier.discountPrice"
:warranty="tier.warranty"
:show-discount="isClicked"
:selected="isTierSelected(tier)"
@click="selectTier(tier)"
/>
</div>
</div>
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import { onMounted, onBeforeMount, onBeforeUnmount, onUnmounted, onActivated, onDeactivated, ref, computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import { IonPage, IonContent, modalController } from "@ionic/vue";
import LegacyMenuSection from "@/components/LegacyMenuSection.vue";
import JobInfoModal from "@/components/JobInfoModal.vue";
import DebugToolbar, { type FunctionGroup } from "@/components/DebugToolbar.vue";
import { useSessionStore } from "@/stores/session";
import { useTnfrStore } from "@/stores/tnfr";
const sessionStore = useSessionStore();
const tnfrStore = useTnfrStore();
const route = useRoute();
const router = useRouter();
const isClicked = ref(false);
const debugToolbar = ref<InstanceType<typeof DebugToolbar> | null>(null);
// Debug function groups
const functionGroups = ref<FunctionGroup[]>([
{
label: 'View Functions',
functions: [
{ label: 'openInfoModal()', value: 'openInfoModal' },
{ label: 'handlePercentClick()', value: 'handlePercentClick' },
{ label: 'handleClearSelection()', value: 'handleClearSelection' },
{ label: 'getJobPrice(job)', value: 'getJobPrice' },
{ label: 'getTierClass(tierName)', value: 'getTierClass' },
{ label: 'isTierSelected(tier)', value: 'isTierSelected' },
{ label: 'selectTier(tier)', value: 'selectTier' },
{ label: 'transformJobContentToMenu(jobContent)', value: 'transformJobContentToMenu' },
],
},
{
label: '@/stores/tnfr',
functions: [
{ label: 'tnfrStore.load()', value: 'tnfrStore.load' },
{ label: 'tnfrStore.save()', value: 'tnfrStore.save' },
{ label: 'tnfrStore.clear()', value: 'tnfrStore.clear' },
{ label: 'tnfrStore.addJob(problemId)', value: 'tnfrStore.addJob' },
{ label: 'tnfrStore.removeJob(jobId)', value: 'tnfrStore.removeJob' },
{ label: 'tnfrStore.setSelectedOffer(...)', value: 'tnfrStore.setSelectedOffer' },
{ label: 'tnfrStore.setJobTitle(jobId, title)', value: 'tnfrStore.setJobTitle' },
{ label: 'tnfrStore.loadOrgVariables()', value: 'tnfrStore.loadOrgVariables' },
],
},
{
label: '@/stores/session',
functions: [
{ label: 'sessionStore.load()', value: 'sessionStore.load' },
{ label: 'sessionStore.updateCustomerProgress(index, completed)', value: 'sessionStore.updateCustomerProgress' },
],
},
{
label: 'vue-router',
functions: [
{ label: 'router.push(path)', value: 'router.push' },
],
},
]);
// Helper to log executions via the toolbar
function logExecution(name: string, args?: any[]) {
debugToolbar.value?.logExecution(name, args);
}
// Track lifecycle
onBeforeMount(() => debugToolbar.value?.logLifecycle('onBeforeMount'));
onBeforeUnmount(() => debugToolbar.value?.logLifecycle('onBeforeUnmount'));
onUnmounted(() => debugToolbar.value?.logLifecycle('onUnmounted'));
onActivated(() => debugToolbar.value?.logLifecycle('onActivated'));
onDeactivated(() => debugToolbar.value?.logLifecycle('onDeactivated'));
// Computed: All reactive variables for debug display
const reactiveVarsData = computed(() => ({
// Refs
isClicked: isClicked.value,
// Computed
menu: menu.value,
currentJob: currentJob.value,
currentJobName: currentJobName.value,
currentStepIndex: currentStepIndex.value,
}));
// Transform jobContent to menu format expected by the template
function transformJobContentToMenu(jobContent: any) {
if (!jobContent?.menus?.[0]) return null;
const firstMenu = jobContent.menus[0];
return {
id: firstMenu.id,
name: firstMenu.name,
refId: firstMenu.refId,
tiers: firstMenu.tiers.map((t: any) => ({
id: t.id,
refId: t.refId,
name: t.tier.name,
title: t.tier.name,
rank: t.tier.rank,
warranty: t.tier.warrantyCopy,
offer: {
id: t.offer.id,
name: t.offer.name,
refId: t.offer.refId,
costsTime: t.offer.costsTime,
costsMaterial: t.offer.costsMaterial,
techHandbookExpanded: t.offer.techHandbook,
},
menuCopy: t.menuCopy,
contentItems: t.contentItems,
price: t.price || 0,
discountPrice: t.discountPrice || 0,
})),
};
}
// Reactive menu computed from currentJob
const menu = computed(() => {
const job = currentJob.value;
if (!job) return null;
return transformJobContentToMenu(job);
});
// Get the current job
const currentJob = computed(() => {
const jobId = route.params.id as string;
return tnfrStore.jobs.find(j => j.id === jobId);
});
// Get the current job's display name
const currentJobName = computed(() => {
const job = currentJob.value;
return job?.title || job?.problem?.name || '';
});
// Help modal showing job description and tech handbook by tier
const openInfoModal = async () => {
logExecution('openInfoModal');
const job = currentJob.value;
const jobDescription = job?.problem?.description || job?.title || 'No description available';
const tiers = menu.value?.tiers || [];
const modal = await modalController.create({
component: JobInfoModal,
componentProps: {
jobDescription,
tiers,
},
});
await modal.present();
};
const handlePercentClick = () => {
logExecution('handlePercentClick');
// Toggle SA discount
isClicked.value = !isClicked.value;
};
const handleClearSelection = async () => {
logExecution('handleClearSelection');
const job = currentJob.value;
if (!job) return;
// Get the band-aid tier's hours to restore when clearing selection
const bandAidHours = job.menuData?.tiers?.[job.menuData.tiers.length - 1]?.offer?.costsTime?.reduce(
(sum: number, cost: any) => sum + (cost.hours || 0),
0
) || job.baseHours || 0;
// Clear the selected offer from the job and restore band-aid hours
await tnfrStore.setSelectedOffer(
job.id,
null,
null,
null,
null,
bandAidHours
);
// Mark this step as not completed in customer progress
await sessionStore.updateCustomerProgress(currentStepIndex.value, false);
};
// Helper to get job price for sorting
function getJobPrice(job: any): number {
if (job.selectedPrice) {
const price = parseFloat(job.selectedPrice);
return isNaN(price) ? 0 : price;
}
return 0;
}
const currentStepIndex = computed(() => {
const jobId = route.params.id as string;
// Sort jobs by price (highest first)
const sortedJobs = [...tnfrStore.jobs].sort((a, b) => {
return getJobPrice(b) - getJobPrice(a);
});
// Find the index of the current job in the sorted list
const index = sortedJobs.findIndex(job => job.id === jobId);
return index >= 0 ? index : 0;
});
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 isTierSelected(tier: any): boolean {
const job = currentJob.value;
// Compare by offer ID if available
if (tier.offer && job?.selectedOffer) {
return job.selectedOffer.id === tier.offer.id;
}
return false;
}
async function selectTier(tier: any) {
logExecution('selectTier', [tier.name]);
const tierId = tier.id;
if ((route.name as string)?.includes("job.show")) {
const job = currentJob.value;
if (!job) return;
if (tier.offer) {
// Use pre-calculated price from tier (calculated in CheckHours.vue)
const price = isClicked.value ? tier.discountPrice : tier.price;
// Calculate the tier's base hours from costsTime
const tierBaseHours = tier.offer?.costsTime?.reduce(
(sum: number, cost: any) => sum + (cost.hours || 0),
0
) || 0;
// Update the job with selected offer details
await tnfrStore.setSelectedOffer(
job.id,
tier.offer,
price.toString(),
tier.name,
tier.title,
tierBaseHours
);
// Mark this step as completed in customer progress
await sessionStore.updateCustomerProgress(currentStepIndex.value, true);
}
// Navigate to next step
const numJobs = tnfrStore.jobs.length;
const nextStep = currentStepIndex.value + 1;
// Sort jobs by price (highest first)
const sortedJobs = [...tnfrStore.jobs].sort((a, b) => {
return getJobPrice(b) - getJobPrice(a);
});
if (nextStep < numJobs) {
const nextJob = sortedJobs[nextStep];
router.push(`/job/${nextJob.id}/show/legacy`);
} else {
router.push('/payment');
}
} else {
const currentMenuId = menu.value?.id || '';
router.push(`/menus/${currentMenuId}/confirm/${tierId}`);
}
}
onMounted(async () => {
debugToolbar.value?.logLifecycle('onMounted');
try {
logExecution('tnfrStore.load');
await tnfrStore.load();
logExecution('tnfrStore.loadOrgVariables');
await tnfrStore.loadOrgVariables();
} catch (error) {
logExecution('error', [(error as Error).message]);
console.error('[MenuTemplateLegacy] Error loading:', error);
}
});
/*
Menu variables will include data for the specific job that we're looking at,
e.g. the menu title, the costs, anything configured from the checklists.
Session variables are for the session, but accessible from all menus in the
session.
Org variables we get from the org, like hourly rate, tax rate, material markup
vars, etc.
It would be great if the menu was self-documenting, so we could always get a
list of all expected vars, their default values, and where they should be set
(menu, session, org).
A formula is NOT attached to a menu template, it is attached to a menu, so it
has it's own variables that it's looking for, all vars should have defaults,
and we still have just those three types (menu, session, org), but when
documenting the expected vars, we need to list both them menu template AND the
formula vars.
*/
</script>
<style scoped>
/* Page wrapper */
.page-wrapper {
min-height: 100vh;
height: 100%;
background: #fff;
padding-bottom: 40px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
/* Menu Content */
.menu-page-heading {
max-width: 1000px;
margin: 24px auto 16px auto;
padding: 0 20px;
text-align: center;
}
.menu-page-heading h1 {
margin: 0;
color: #333;
font-size: 36px;
font-weight: 700;
font-variant: small-caps;
}
.job-name-subtitle {
margin: 8px 0 0 0;
color: #000;
font-size: 24px;
font-weight: 600;
font-variant: small-caps;
}
.menu-grid {
max-width: 1000px;
margin: 0 auto;
padding: 0 20px;
}
</style>