Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
<template>
  <BaseLayout :title="menu.name">
    <div class="menu-container">
      <div class="header-section">
        <div class="title-container">
          <div class="title-icons">
            <ion-icon :icon="arrowBack" @click="goBack" class="clickable-icon"></ion-icon>
          </div>
          <h1 class="page-title">{{ getMenuTitle() || 'Menu Options' }}</h1>
          <div class="title-icons-right">
            <ion-button
              fill="solid"
              :color="isClicked ? 'primary' : 'medium'"
              size="small"
              class="sa-button"
              @click="toggleButton"
            >
              SA
            </ion-button>
          </div>
        </div>
      </div>

      <div class="content-section">
        <div class="tiers-grid">
          <div
            v-for="tier in menu?.tiers"
            :key="tier.id || tier.name"
            v-show="!isTierHidden(tier.id)"
            class="tier-card"
            :class="getTierClass(tier.name)"
            @click="selectTier(tier)"
          >
            <!-- Tier Badge -->
            <div class="tier-badge" :class="getTierBadgeClass(tier.name)">
              <span class="tier-name">{{ tier.name }}</span>
            </div>

            <!-- Tier Content -->
            <div class="tier-content">
              <h3 class="tier-title">{{ getTierOfferTitle(tier) || tier.title?.toUpperCase() || tier.name }}</h3>

              <!-- Features List -->
              <div class="features-section">
                <template v-if="tier.menuCopy">
                  <div
                    v-for="copy in tier.menuCopy"
                    :key="copy.id"
                    class="feature-item"
                  >
                    <div
                      v-for="item in copy.contentItems"
                      :key="item.id"
                      class="feature-text"
                    >
                      <ion-icon :icon="checkmarkCircle" class="feature-icon"></ion-icon>
                      <span>{{ item.content }}</span>
                    </div>
                  </div>
                </template>
                <template v-else-if="tier.contentItems">
                  <div
                    v-for="item in tier.contentItems"
                    :key="item.id"
                    v-show="!item.refId?.includes('_title')"
                    class="feature-text"
                  >
                    <ion-icon :icon="checkmarkCircle" class="feature-icon"></ion-icon>
                    <span>{{ item.content }}</span>
                  </div>
                </template>
              </div>

              <!-- Pricing Section -->
              <div class="pricing-section">
                <div v-if="isClicked && tier.discountPrice" class="discount-price">
                  <show-currency :currencyIn="tier.discountPrice" />
                </div>
                <div class="price-amount" :class="{ 'price-crossed': isClicked && tier.discountPrice }">
                  <show-currency :currencyIn="tier.price" />
                </div>
                <div class="warranty-text" v-if="tier.warranty">
                  {{ tier.warranty }}
                </div>
              </div>

              <!-- Select Button -->
              <div class="select-button-wrapper">
                <ion-button expand="block" color="primary" class="select-button">
                  Select {{ tier.name }}
                </ion-button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </BaseLayout>
</template>

<script setup lang="ts">
import { onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { getDb } from "@/dataAccess/getDb";
import BaseLayout from "@/components/BaseLayout.vue";
import ShowCurrency from "@/components/ShowCurrency.vue";
import calculatePrice from "@/framework/calculatePrice";
import legacyFormula from "@/lib/legacyFormula";
import legacyFormulaDiscount from "@/lib/legacyFormulaDiscount";
import { useSessionStore } from "@/stores/session";
import { useOrganizationStore } from "@/stores/organization";
import {
  IonIcon,
  IonButton,
} from "@ionic/vue";
import { arrowBack, checkmarkCircle } from "ionicons/icons";

const sessionStore = useSessionStore();
const orgStore = useOrganizationStore();
const route = useRoute();
const router = useRouter();
const menu = ref<any>({ name: "" });
const menuId = ref<string>("");
const isClicked = ref(false);

const goBack = () => {
  router.back();
};

// Get current job and its menu modifications
const getCurrentJob = () => {
  const jobId = route.params.id as string;
  return sessionStore.jobs.find((j) => j.id === jobId);
};

// Get modified menu title or default
const getMenuTitle = () => {
  const job = getCurrentJob();
  return job?.menuModifications?.menuTitle || menu.value.name;
};

// Check if tier is hidden
const isTierHidden = (tierId: string) => {
  const job = getCurrentJob();
  return job?.menuModifications?.tierModifications?.[tierId]?.hidden || false;
};

// Get modified offer title for a tier
const getTierOfferTitle = (tier: any) => {
  const job = getCurrentJob();
  return job?.menuModifications?.tierModifications?.[tier.id]?.offerTitle || null;
};

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 getTierBadgeClass(tierName: string): string {
  const name = tierName?.toLowerCase() || "";
  if (name.includes("platinum")) return "badge-platinum";
  if (name.includes("gold")) return "badge-gold";
  if (name.includes("silver")) return "badge-silver";
  if (name.includes("bronze")) return "badge-bronze";
  if (name.includes("band")) return "badge-bandaid";
  return "";
}

function selectTier(tier: any) {
  const currentMenuId = menuId.value;
  const tierId = tier.id;

  if ((route.name as string)?.includes("job.show")) {
    const jobId = route.params.id;
    router.push(`/job/${jobId}/confirm/${tierId}`);
  } else {
    router.push(`/menus/${currentMenuId}/confirm/${tierId}`);
  }
}

function populateFormulaVars(f: any, offer: any, extraCostMultiplier: number, useDiscount = false) {
  f.vars.hourlyFee = orgStore.hourlyFee;
  f.vars.serviceCallFee = orgStore.serviceCallFee;
  f.vars.saDiscount = useDiscount ? 0.97 : 1; // 3% discount when active
  f.vars.salesTax = orgStore.salesTax;
  f.vars.multiplier = parseFloat(offer.multiplier);
  f.vars.extraCostMultiplier = extraCostMultiplier;
  return f;
}

onMounted(async () => {
  try {
    const db = await getDb();

    await orgStore.loadVariables();
    await sessionStore.load();

    const job = sessionStore.jobs.find((e) => e.id == route.params.id);

    let currentMenuId = "";
    let menuResponse = null;

    if ((route.name as string).includes("job.show")) {
      // Job-based menu display - use cached menu data from job
      if (job) {
        if (job.menuData) {
          // Use cached menu data - no database fetch needed!
          console.log('[MenuTemplateDev] Using cached menu data from job');
          menuResponse = job.menuData;
          menuId.value = menuResponse.id;
        } else if (db && job.problem?.menus) {
          // Fallback: fetch from database if not cached (shouldn't happen with new flow)
          console.log('[MenuTemplateDev] Menu data not cached, fetching from database');
          const menus = job.problem.menus;

          if (typeof menus === 'string') {
            try {
              const parsed = JSON.parse(menus);
              currentMenuId = Array.isArray(parsed) ? parsed[0] : menus;
            } catch (e) {
              currentMenuId = menus;
            }
          } else if (Array.isArray(menus)) {
            currentMenuId = menus[0];
          }

          menuId.value = currentMenuId;
          menuResponse = await db.menus.byMenuId(currentMenuId);
        }
      }
    } else if (db) {
      // Direct menu browsing (not from job) - fetch from database
      currentMenuId = route.params.id as string;
      menuId.value = currentMenuId;
      menuResponse = await db.menus.byMenuId(currentMenuId);
    }

    if (menuResponse) {
      menu.value = menuResponse;
      let extraCostMultiplier = 1;
      if (job) {
        extraCostMultiplier = job.extraTime * job.extraMaterial;
      }
      for (const tier of menu.value.tiers) {
        const f = populateFormulaVars(
          legacyFormula(),
          tier.offer,
          extraCostMultiplier,
        );
        const result = await calculatePrice(
          tier.offer.costsMaterial,
          tier.offer.costsTime,
          f,
        );
        tier.price = result.finalPrice;
        tier.priceConverted = result.finalPriceConverted;

        const fDiscount = populateFormulaVars(
          legacyFormulaDiscount(),
          tier.offer,
          extraCostMultiplier,
          true, // Apply 3% discount
        );
        const discountResult = await calculatePrice(
          tier.offer.costsMaterial,
          tier.offer.costsTime,
          fDiscount,
        );
        tier.discountPrice = discountResult.finalPrice;
        tier.discountPriceConverted = discountResult.finalPriceConverted;

        const titleItem = tier.contentItems?.find((e: any) =>
          e.refId?.includes("_title"),
        );
        if (titleItem) {
          tier.title = titleItem.content;
        }

        const tierName = tier.name?.toLowerCase() || "";
        if (tierName.includes("platinum")) {
          tier.warranty = "2 year limited warranty";
        } else if (tierName.includes("gold")) {
          tier.warranty = "18 month limited warranty";
        } else if (tierName.includes("silver")) {
          tier.warranty = "1 year limited warranty";
        } else if (tierName.includes("bronze")) {
          tier.warranty = "6 month limited warranty";
        } else if (tierName.includes("band")) {
          tier.warranty = "30 day limited warranty";
        }
      }
    }
  } catch (error) {
    console.error("Error loading menu:", error);
  }
});

async function toggleButton() {
  isClicked.value = !isClicked.value;

  if (menu.value?.tiers) {
    const job = sessionStore.jobs.find((e) => e.id == route.params.id);
    let extraCostMultiplier = 1;
    if (job) {
      extraCostMultiplier = job.extraTime * job.extraMaterial;
    }

    for (const tier of menu.value.tiers) {
      if (isClicked.value) {
        const fDiscount = populateFormulaVars(
          legacyFormulaDiscount(),
          tier.offer,
          extraCostMultiplier,
          true, // Apply 3% discount
        );
        const discountResult = await calculatePrice(
          tier.offer.costsMaterial,
          tier.offer.costsTime,
          fDiscount,
        );
        tier.discountPrice = discountResult.finalPrice;
        tier.discountPriceConverted = discountResult.finalPriceConverted;
      }
    }
  }
}
</script>

<style scoped>
.menu-container {
  padding: 20px;
  max-width: 1400px;
  margin: 0 auto;
}

.header-section {
  margin-bottom: 32px;
}

.title-container {
  position: relative;
  margin-bottom: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.title-icons {
  position: absolute;
  left: 12px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  gap: 16px;
}

.title-icons ion-icon {
  font-size: 48px;
}

.title-icons-right {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
}

.clickable-icon {
  cursor: pointer;
  transition: transform 0.2s ease, color 0.2s ease;
  color: var(--ion-color-light);
}

.clickable-icon:hover {
  transform: scale(1.1);
  color: var(--ion-color-primary-shade);
}

.clickable-icon:active {
  transform: scale(0.95);
}

.page-title {
  margin: 0;
  color: #ffffff;
  font-size: 48px;
  font-weight: 700;
  text-align: center;
  font-variant: small-caps;
}

.sa-button {
  --padding-start: 12px;
  --padding-end: 12px;
  font-weight: 700;
  font-size: 14px;
  height: 40px;
}

.content-section {
  padding: 0 20px 20px 20px;
}

.tiers-grid {
  display: flex;
  flex-direction: column;
  gap: 24px;
  max-width: 800px;
  margin: 0 auto;
}

.tier-card {
  background-color: var(--ion-color-dark);
  border-radius: 12px;
  overflow: hidden;
  cursor: pointer;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
  border: 2px solid transparent;
  display: flex;
  flex-direction: column;
}

.tier-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}

/* Tier-specific border colors */
.tier-platinum {
  border-color: var(--menu-color-platinum, #3db4d6);
}

.tier-gold {
  border-color: var(--menu-color-gold, #ffd83b);
}

.tier-silver {
  border-color: var(--menu-color-silver, #bfbfbf);
}

.tier-bronze {
  border-color: var(--menu-color-bronze, #ffad2b);
}

.tier-bandaid {
  border-color: var(--menu-color-bandaid, #ff8073);
}

.tier-badge {
  padding: 16px 20px;
  text-align: center;
  font-weight: 700;
  font-size: 24px;
  text-transform: uppercase;
  color: #000;
}

.badge-platinum {
  background: linear-gradient(135deg, var(--menu-color-platinum, #3db4d6) 0%, var(--menu-color-platinum-tint, #5ec3dd) 100%);
}

.badge-gold {
  background: linear-gradient(135deg, var(--menu-color-gold, #ffd83b) 0%, var(--menu-color-gold-tint, #ffe066) 100%);
}

.badge-silver {
  background: linear-gradient(135deg, var(--menu-color-silver, #bfbfbf) 0%, var(--menu-color-silver-tint, #d4d4d4) 100%);
}

.badge-bronze {
  background: linear-gradient(135deg, var(--menu-color-bronze, #ffad2b) 0%, var(--menu-color-bronze-tint, #ffc04d) 100%);
}

.badge-bandaid {
  background: linear-gradient(135deg, var(--menu-color-bandaid, #ff8073) 0%, var(--menu-color-bandaid-tint, #ff9a8f) 100%);
}

.tier-name {
  display: block;
  line-height: 1.2;
}

.tier-content {
  padding: 24px;
  flex: 1;
  display: flex;
  flex-direction: column;
}

.tier-title {
  margin: 0 0 20px 0;
  color: #ffffff;
  font-size: 20px;
  font-weight: 600;
  text-align: center;
  line-height: 1.3;
}

.features-section {
  flex: 1;
  margin-bottom: 24px;
}

.feature-item {
  margin-bottom: 8px;
}

.feature-text {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin-bottom: 12px;
  color: var(--ion-color-light);
  font-size: 15px;
  line-height: 1.5;
}

.feature-icon {
  color: var(--ion-color-primary);
  font-size: 20px;
  flex-shrink: 0;
  margin-top: 2px;
}

.pricing-section {
  text-align: center;
  margin-bottom: 20px;
  padding-top: 20px;
  border-top: 1px solid var(--ion-color-medium);
}

.discount-price {
  color: var(--ion-color-primary);
  font-size: 24px;
  font-weight: 700;
  margin-bottom: 8px;
}

.price-amount {
  color: #ffffff;
  font-size: 32px;
  font-weight: 700;
  margin-bottom: 12px;
}

.price-crossed {
  text-decoration: line-through;
  opacity: 0.5;
  font-size: 24px;
}

.warranty-text {
  color: var(--ion-color-medium);
  font-size: 13px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.select-button-wrapper {
  margin-top: auto;
}

.select-button {
  --padding-top: 16px;
  --padding-bottom: 16px;
  font-weight: 600;
  font-size: 16px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

/* Mobile adjustments */
@media (max-width: 767px) {
  .menu-container {
    padding: 16px;
  }

  .page-title {
    font-size: 36px;
  }

  .tier-badge {
    font-size: 20px;
    padding: 12px 16px;
  }

  .tier-title {
    font-size: 18px;
  }

  .feature-text {
    font-size: 14px;
  }

  .price-amount {
    font-size: 28px;
  }
}
</style>