Hello from MCP server
<template>
<BaseLayout title="Job Preview">
<ion-grid :fixed="true">
<ion-row>
<ion-col>
<!-- Problem Information -->
<div class="problem-header" v-if="job?.problem">
<h2>{{ job.problem.name || "Loading..." }}</h2>
<p class="problem-description" v-if="job.problem.description">
{{ job.problem.description }}
</p>
</div>
<div class="divider"></div>
<!-- Applied Adjustments Summary -->
<div class="adjustments-summary" v-if="job">
<h3>Applied Adjustments</h3>
<div class="adjustment-values">
<div class="adjustment-item">
<span class="adjustment-label">Time Multiplier:</span>
<span class="adjustment-value">{{ job.extraTime || 1 }}x</span>
</div>
<div class="adjustment-item">
<span class="adjustment-label">Material Multiplier:</span>
<span class="adjustment-value">{{ job.extraMaterial || 1 }}x</span>
</div>
</div>
</div>
<div class="divider"></div>
<!-- Service Options with Pricing -->
<div class="offers-section" v-if="enhancedOffers.length > 0">
<h3>Service Options & Final Pricing</h3>
<div class="offers-grid">
<div
v-for="(offer, index) in enhancedOffers"
:key="offer.id"
class="offer-card"
>
<div class="tier-badge" v-if="offer.tierName">
{{ offer.tierName }}
</div>
<div class="offer-name">{{ offer.name }}</div>
<div class="offer-subtitle" v-if="offer.tierTitle">
{{ offer.tierTitle }}
</div>
<!-- Price Display -->
<div class="price-display">
<span class="price-label">Price:</span>
<show-currency
:currencyIn="offersPreview[index]?.price || '0'"
/>
</div>
</div>
</div>
</div>
<!-- Confirmation Section -->
<div class="confirmation-section">
<p class="confirmation-text">
Please review the service options and pricing above. Once confirmed, you'll be able to present the menu to the customer.
</p>
<div class="button-group">
<ion-button size="large" @click="showMenu" class="primary-action">
<ion-icon :icon="checkmarkCircleOutline" slot="start"></ion-icon>
Looks Good, Show the Menu
</ion-button>
<ion-button color="medium" fill="outline" @click="goBack">
<ion-icon :icon="arrowBackOutline" slot="start"></ion-icon>
Back to Adjustments
</ion-button>
<ion-button color="tertiary" fill="outline" @click="goToSession">
Session Home
</ion-button>
</div>
</div>
</ion-col>
</ion-row>
</ion-grid>
</BaseLayout>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { useRoute, useRouter, RouterLink } from "vue-router";
import { IonButton, IonGrid, IonRow, IonCol, IonIcon } from "@ionic/vue";
import { checkmarkCircleOutline, arrowBackOutline } from "ionicons/icons";
import BaseLayout from "@/components/BaseLayout.vue";
import ShowCurrency from "@/components/ShowCurrency.vue";
import { useSessionStore } from "@/stores/session";
import { useOrganizationStore } from "@/stores/organization";
import { getDb } from "@/dataAccess/getDb";
import { OfferData } from "@/dataAccess/getDb";
import { OffersRecord } from "@/pocketbase-types";
import calculatePrice from "@/framework/calculatePrice";
import legacyFormula from "@/lib/legacyFormula";
interface Formula {
vars: Record<string, any>;
derivedVars: Record<string, any>;
operations: Array<{
op: string;
in1: string;
in2: string;
out: string;
}>;
}
const route = useRoute();
const router = useRouter();
const sessionStore = useSessionStore();
const orgStore = useOrganizationStore();
const jobId = route.params.id as string;
const offers = ref<OfferData[]>([]);
const offersPreview = ref([{ offer: "", price: "0" }]);
const enhancedOffers = ref<any[]>([]);
const job = computed(() => {
return sessionStore.jobs.find((e) => e.id == jobId);
});
const showMenu = () => {
router.push(`/job/${jobId}/show/legacy`);
};
const goToSession = () => {
router.push("/service-call");
};
const goBack = () => {
router.push(`/job/${jobId}/adjust`);
};
function populateFormulaVars(
f: Formula,
offer: OffersRecord,
extraCostMultiplier: number,
): Formula {
f.vars.hourlyFee = orgStore.hourlyFee;
f.vars.serviceCallFee = orgStore.serviceCallFee;
f.vars.saDiscount = orgStore.saDiscount;
f.vars.salesTax = orgStore.saDiscount;
f.vars.multiplier = parseFloat(offer.multiplier || "1");
f.vars.extraCostMultiplier = extraCostMultiplier;
return f;
}
async function calculatePrices() {
const timeMult = job.value?.extraTime || 1;
const materialMult = job.value?.extraMaterial || 1;
offersPreview.value = await Promise.all(
offers.value.map(async (offer) => {
const extraCostMultiplier = timeMult * materialMult;
const f = populateFormulaVars(
legacyFormula(),
offer.offer,
extraCostMultiplier,
);
const result = await calculatePrice(offer.costsMaterial, offer.costsTime, f);
const price = String(result.finalPrice);
const priceConverted = result.finalPriceConverted?.formatted || price;
return { offer: offer.offer.name as string, price, priceConverted };
})
);
}
onMounted(async () => {
await orgStore.loadVariables();
await sessionStore.load();
const db = await getDb();
if (db && job.value?.problem?.menus) {
const menuIds = job.value.problem.menus as string[];
offers.value = [];
enhancedOffers.value = [];
// Fetch menus with tiers to get enhanced offer data
for (const menuId of menuIds) {
const menuWithTiers = await db.menus.byMenuId(menuId);
if (menuWithTiers && menuWithTiers.tiers) {
for (const tier of menuWithTiers.tiers) {
if (tier.offer) {
// Get offer data for price calculation
const offerData = await db.offerData(tier.offer.id);
offers.value.push(offerData);
// Find the title from content items
let title = "";
if (tier.contentItems && Array.isArray(tier.contentItems)) {
const titleItem = tier.contentItems.find(
(item: any) => item.refId && item.refId.includes("_title"),
);
if (titleItem) {
title = titleItem.content;
}
}
// Create enhanced offer with tier information
const enhancedOffer = {
...tier.offer,
id: tier.offer.id,
name: tier.offer.name,
tierName: tier.name,
tierTitle: title,
contentItems: tier.contentItems || [],
};
enhancedOffers.value.push(enhancedOffer);
}
}
}
}
calculatePrices();
}
});
</script>
<style scoped>
.button-group {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 24px;
}
.problem-header {
margin-bottom: 20px;
}
.problem-header h2 {
margin: 0 0 12px 0;
font-size: 1.8rem;
font-weight: 600;
color: var(--ion-color-primary);
}
.problem-description {
font-size: 1.1rem;
line-height: 1.6;
color: var(--ion-text-color);
margin: 0;
}
.divider {
border-top: 1px solid var(--ion-color-medium);
margin: 24px 0;
}
.adjustments-summary {
margin-bottom: 24px;
}
.adjustments-summary h3 {
font-size: 1.3rem;
font-weight: 500;
margin-bottom: 16px;
color: var(--ion-color-dark);
}
.adjustment-values {
background: var(--ion-color-light);
border-radius: 8px;
padding: 16px;
border-left: 4px solid var(--ion-color-success);
}
.adjustment-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.adjustment-item:last-child {
margin-bottom: 0;
}
.adjustment-label {
font-size: 1rem;
color: var(--ion-color-medium-shade);
}
.adjustment-value {
font-size: 1.2rem;
font-weight: 600;
color: var(--ion-color-success);
}
.offers-section {
margin-bottom: 24px;
}
.offers-section h3 {
font-size: 1.3rem;
font-weight: 500;
margin-bottom: 16px;
color: var(--ion-color-dark);
}
.offers-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
margin-bottom: 32px;
}
.offer-card {
padding: 20px;
background: var(--ion-color-light);
border-radius: 8px;
border: 1px solid var(--ion-color-medium-tint);
position: relative;
}
.tier-badge {
position: absolute;
top: 12px;
right: 12px;
padding: 4px 10px;
background: var(--ion-color-medium);
color: white;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.offer-name {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 4px;
color: var(--ion-color-dark);
padding-right: 80px;
}
.offer-subtitle {
font-size: 0.95rem;
color: var(--ion-color-medium-shade);
margin-bottom: 12px;
font-style: italic;
}
.price-display {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: var(--ion-color-success-tint);
border-radius: 6px;
margin-top: 12px;
}
.price-label {
font-size: 1rem;
color: var(--ion-color-dark);
font-weight: 500;
}
.price-value {
font-size: 1.4rem;
font-weight: 700;
color: var(--ion-color-success-shade);
}
.confirmation-section {
background: var(--ion-color-light);
border-radius: 12px;
padding: 24px;
margin-top: 32px;
text-align: center;
}
.confirmation-text {
font-size: 1rem;
color: var(--ion-color-medium-shade);
margin-bottom: 24px;
line-height: 1.6;
}
.primary-action {
--background: var(--ion-color-success);
--background-hover: var(--ion-color-success-shade);
--color: white;
font-weight: 600;
font-size: 1.1rem;
margin-bottom: 12px;
}
.primary-action ion-icon {
font-size: 1.4rem;
margin-right: 8px;
}
</style>