Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
<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>