Hello from MCP server
<template>
<BaseLayout title="Job Adjustment">
<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>
<!-- Adjustment Controls -->
<div class="adjustment-section">
<h3>Final Price Adjustment</h3>
<p class="adjustment-description">
Increase the final price of the job to account for any extra
costs.
</p>
<div class="multiplier-controls">
<div class="multiplier-item">
<ion-range
label-placement="start"
label="Price Increase"
v-model="timeMult"
:ticks="true"
:snaps="true"
:min="1"
:max="3"
:step="0.05"
></ion-range>
<span class="multiplier-value"
>+{{ Math.round((timeMult - 1) * 100) }}%</span
>
</div>
</div>
</div>
<div class="divider"></div>
<!-- Price Preview -->
<div class="offers-section" v-if="sortedOffers.length > 0">
<h3>Price Preview</h3>
<div class="price-table">
<div class="table-header">
<div class="col-service">Service Option</div>
<div class="col-description">Description</div>
<div class="col-price">Estimated Price</div>
</div>
<div
v-for="(offer, index) in sortedOffers"
:key="offer.id"
class="table-row"
:class="{ even: index % 2 === 0 }"
>
<div class="col-service">
<div class="service-name">{{ offer.name }}</div>
</div>
<div class="col-description">
<div class="service-description" v-if="offer.tierTitle">
{{ offer.tierTitle }}
</div>
<div class="service-description" v-else>
Service tier option
</div>
</div>
<div class="col-price">
<show-currency
:currencyIn="getOfferPrice(offer.id)"
class="price-amount"
/>
</div>
</div>
</div>
</div>
<div class="button-group">
<ion-button @click="showMenu">Show Menu</ion-button>
<ion-button color="tertiary" fill="outline" @click="goToSession"
>Session Home</ion-button
>
</div>
</ion-col>
</ion-row>
</ion-grid>
</BaseLayout>
</template>
<script setup lang="ts">
import { computed, onMounted, watch, ref } from "vue";
import { useRoute, useRouter, RouterLink } from "vue-router";
import { IonButton, IonRange, IonGrid, IonRow, IonCol } from "@ionic/vue";
import BaseLayout from "@/components/BaseLayout.vue";
import { useSessionStore } from "@/stores/session";
import { useOrganizationStore } from "@/stores/organization";
import calculatePrice from "@/framework/calculatePrice";
import legacyFormula from "@/lib/legacyFormula";
import { getDb } from "@/dataAccess/getDb";
import { OffersRecord } from "@/pocketbase-types";
import ShowCurrency from "@/components/ShowCurrency.vue";
import { OfferData } from "@/dataAccess/getDb";
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 timeMult = ref(1);
const materialMult = ref(1); // Always 1, no UI control
const job = computed(() => {
return sessionStore.jobs.find((e) => e.id == jobId);
});
// Define tier order for sorting
const tierOrder = ["Platinum", "Gold", "Silver", "Bronze", "Band"];
const sortedOffers = computed(() => {
return [...enhancedOffers.value].sort((a, b) => {
const aIndex = tierOrder.findIndex((tier) =>
a.tierName?.toLowerCase().includes(tier.toLowerCase()),
);
const bIndex = tierOrder.findIndex((tier) =>
b.tierName?.toLowerCase().includes(tier.toLowerCase()),
);
// If both have recognized tier names, sort by tier order
if (aIndex !== -1 && bIndex !== -1) {
return aIndex - bIndex;
}
// If only one has a recognized tier, put it first
if (aIndex !== -1) return -1;
if (bIndex !== -1) return 1;
// If neither has a recognized tier, sort alphabetically
return (a.tierName || "").localeCompare(b.tierName || "");
});
});
const showMenu = () => {
console.log('[JobAdjust] showMenu called with jobId:', jobId);
console.log('[JobAdjust] SessionStore jobs at navigation time:', JSON.stringify(sessionStore.jobs, null, 2));
const jobData = sessionStore.jobs.find(j => j.id === jobId);
console.log('[JobAdjust] Job data being navigated to:', jobData ? {
id: jobData.id,
title: jobData.title,
hasProblem: !!jobData.problem,
problemMenus: jobData.problem?.menus,
problemMenusType: typeof jobData.problem?.menus
} : 'NOT FOUND');
router.push(`/job/${jobId}/show/legacy`);
};
const goToSession = () => {
router.push("/service-call");
};
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 getOfferPrice(offerId: string): string {
const offerIndex = enhancedOffers.value.findIndex(
(offer) => offer.id === offerId,
);
return offersPreview.value[offerIndex]?.price || "0";
}
// TODO: these values need to go into database as org vars
function populateFormulaVars(
f: Formula,
offer: OffersRecord,
extraCostMultiplier: number,
): Formula {
// const extraCostMultiplier = timeMult.value * materialMult.value;
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 updatePrices() {
offersPreview.value = await Promise.all(
offers.value!.map(async (offer) => {
const extraCostMultiplier = timeMult.value * materialMult.value;
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 };
})
);
}
watch(timeMult, async () => {
await sessionStore.updateExtraTime(jobId, timeMult.value);
// Keep materialMult at 1 and update it in the session
await sessionStore.updateExtraMaterial(jobId, 1);
await updatePrices();
});
onMounted(async () => {
await orgStore.loadVariables();
await sessionStore.load();
timeMult.value = job.value?.extraTime || 1;
materialMult.value = 1; // Always keep at 1
// Ensure materialMult is set to 1 in the session
await sessionStore.updateExtraMaterial(jobId, 1);
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);
}
}
}
}
updatePrices();
}
});
</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;
}
.adjustment-section {
margin-bottom: 24px;
}
.adjustment-section h3,
.offers-section h3 {
font-size: 1.3rem;
font-weight: 500;
margin-bottom: 8px;
color: var(--ion-color-dark);
}
.adjustment-description {
font-size: 0.95rem;
color: var(--ion-color-medium-shade);
margin-bottom: 20px;
}
.multiplier-controls {
background: var(--ion-color-light);
border-radius: 8px;
padding: 16px;
}
.multiplier-item {
display: flex;
align-items: center;
margin-bottom: 16px;
gap: 16px;
}
.multiplier-item:last-child {
margin-bottom: 0;
}
.multiplier-item ion-range {
flex: 1;
}
.multiplier-value {
min-width: 50px;
font-weight: 600;
font-size: 1.1rem;
color: var(--ion-color-primary);
text-align: right;
}
.offers-section {
margin-bottom: 24px;
}
.price-table {
background: #ffffff;
border: 1px solid #cccccc;
border-radius: 4px;
overflow: hidden;
font-family: "Courier New", monospace;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.table-header {
display: grid;
grid-template-columns: 2fr 3fr 1.5fr;
background: #f8f9fa;
border-bottom: 2px solid #cccccc;
font-weight: 600;
font-size: 0.9rem;
color: #495057;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.table-header > div {
padding: 12px 16px;
border-right: 1px solid #dddddd;
}
.table-header > div:last-child {
border-right: none;
}
.table-row {
display: grid;
grid-template-columns: 2fr 3fr 1.5fr;
border-bottom: 1px solid #eeeeee;
transition: background-color 0.1s;
}
.table-row:hover {
background: #f8f9fa;
}
.table-row.even {
background: #fdfdfd;
}
.table-row.even:hover {
background: #f8f9fa;
}
.table-row:last-child {
border-bottom: none;
}
.table-row > div {
padding: 12px 16px;
border-right: 1px solid #eeeeee;
display: flex;
align-items: center;
}
.table-row > div:last-child {
border-right: none;
}
.col-service {
font-weight: 500;
}
.col-description {
color: #6c757d;
}
.col-price {
justify-content: flex-end;
font-weight: 600;
}
.service-name {
font-size: 0.95rem;
color: #212529;
font-weight: 500;
}
.service-description {
font-size: 0.85rem;
color: #6c757d;
line-height: 1.3;
}
.price-amount {
font-size: 1rem;
font-weight: 600;
color: #212529;
font-family: "Courier New", monospace;
}
</style>