Hello from MCP server

List Files | Just Commands | Repo | Logs

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