Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
<template>
  <BaseLayout title="Menu">
    <template #header-buttons>
      <ion-button fill="clear" @click="toggleTechHandbook">
        <ion-icon :icon="showTechHandbook ? closeOutline : helpCircleOutline" color="primary" style="font-size: 36px;" />
      </ion-button>
    </template>

    <div class="menu-container">
      <!-- Menu Page Heading -->
      <div class="menu-page-heading">
        <h1>What Should We Do?</h1>
        <h2 v-if="currentJobName" class="job-name-subtitle">{{ currentJobName }}</h2>
        <ion-chip v-if="currentOfferRefId" :class="refIdChipClass">{{ currentOfferRefId }}</ion-chip>
      </div>

      <div v-if="!currentJob" class="empty-state">
        <p>Job not found</p>
        <ion-button @click="router.push('/cart')">Back to Cart</ion-button>
      </div>

      <template v-else>
        <!-- Agreement Options with Navigation Arrows -->
        <div v-if="!showTechHandbook" class="agreement-nav-row">
          <ion-button
            fill="solid"
            class="nav-arrow nav-arrow-left"
            :class="{ 'nav-arrow-disabled': isSingleMenu }"
            :disabled="isSingleMenu"
            @click="handlePrevClick"
          >
            <ion-icon slot="icon-only" :icon="chevronBackOutline" />
          </ion-button>

          <div class="agreement-card">
            <div class="agreement-row">
              <div class="agreement-option" @click="toggleServiceAgreement">
                <ion-icon
                  :icon="tnfrStore.applyDiscount ? checkmarkCircle : ellipseOutline"
                  :class="{ 'checked': tnfrStore.applyDiscount }"
                  class="agreement-icon"
                />
                <span class="agreement-label">Service Agreement</span>
              </div>
              <div class="agreement-option" @click="togglePaymentPlan">
                <ion-icon
                  :icon="tnfrStore.showPlan ? checkmarkCircle : ellipseOutline"
                  :class="{ 'checked': tnfrStore.showPlan }"
                  class="agreement-icon"
                />
                <span class="agreement-label">Payment Plan</span>
              </div>
            </div>
          </div>

          <ion-button
            fill="solid"
            class="nav-arrow nav-arrow-right"
            :class="{ 'nav-arrow-disabled': isSingleMenu }"
            :disabled="isSingleMenu"
            @click="handleNextClick"
          >
            <ion-icon slot="icon-only" :icon="chevronForwardOutline" />
          </ion-button>
        </div>

        <!-- Tech Handbook View -->
        <div v-if="showTechHandbook" class="tech-handbook-view">
          <div class="tech-handbook-description" v-if="problemDescription">
            <h3>Description</h3>
            <p>{{ problemDescription }}</p>
          </div>
          <div v-for="tier in menuTiers" :key="tier.id || tier.name" class="tech-handbook-tier">
            <h3 class="tech-handbook-tier-name">{{ tier.name }}</h3>
            <ul v-if="tier.offer?.techHandbookExpanded?.length || tier.offer?.techHandbook?.length" class="tech-handbook-list">
              <li v-for="(item, idx) in (tier.offer?.techHandbookExpanded || tier.offer?.techHandbook || [])" :key="idx">
                {{ item.content || item }}
              </li>
            </ul>
            <p v-else class="tech-handbook-empty">No tech handbook content available</p>
          </div>
        </div>

        <!-- Menu Grid View -->
        <div v-else class="menu-grid">
          <LegacyMenuSection
            v-for="tier in menuTiers"
            :key="tier.id || tier.name"
            :tier-name="tier.name"
            :tier-title="tier.contentItems?.map((item: any) => item.content).join(' ') || tier.title"
            :tier-class="getTierClass(tier.name)"
            :menu-copy="tier.menuCopy"
            :content-items="tier.contentItems"
            :price="tier.price"
            :price-converted="tier.priceConverted"
            :discount-price="tier.discountPrice"
            :discount-price-converted="tier.discountPriceConverted"
            :warranty="tier.warranty"
            :show-discount="tnfrStore.applyDiscount"
            :show-plan="tnfrStore.showPlan"
            :selected="isTierSelected(tier)"
            @click="selectTier(tier)"
          />
        </div>

      </template>
    </div>
  </BaseLayout>
</template>

<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { IonButton, IonIcon, IonChip } from "@ionic/vue";
import { checkmarkCircle, ellipseOutline, chevronBackOutline, chevronForwardOutline, helpCircleOutline, closeOutline } from "ionicons/icons";
import BaseLayout from "@/components/BaseLayout.vue";
import LegacyMenuSection from "@/components/LegacyMenuSection.vue";
import { useTnfrStore } from "@/stores/tnfr";

const route = useRoute();
const router = useRouter();
const tnfrStore = useTnfrStore();

// Tech handbook toggle state
const showTechHandbook = ref(false);

function toggleTechHandbook() {
  showTechHandbook.value = !showTechHandbook.value;
}

// Load session state from database on mount
onMounted(async () => {
  await tnfrStore.load();
});

// Get job ID from route
const jobId = computed(() => route.params.jobId as string);

// Jobs sorted by highest price offer to lowest
const sortedJobs = computed(() => {
  return [...tnfrStore.jobs].sort((a, b) => {
    const priceA = a.menus?.[0]?.tiers?.[0]?.price || 0;
    const priceB = b.menus?.[0]?.tiers?.[0]?.price || 0;
    return priceB - priceA; // highest first
  });
});

// Check if there's only one menu
const isSingleMenu = computed(() => {
  return sortedJobs.value.length <= 1;
});

// Current job from jobs array only
const currentJob = computed(() => {
  if (!jobId.value) return null;
  return tnfrStore.jobs.find(j => j.id === jobId.value) || null;
});

// Job name for display (from menu name)
const currentJobName = computed(() => {
  return currentJob.value?.menus?.[0]?.name || '';
});

// Offer refId for display
const currentOfferRefId = computed(() => {
  const job = currentJob.value;
  if (!job) return '';

  // If there's a selected offer, show its refId
  if (job.selectedOffer?.refId) {
    return job.selectedOffer.refId;
  }

  // Otherwise show first tier's refId with last character removed
  const baseRefId = job.menus?.[0]?.tiers?.[0]?.offer?.refId || '';
  return baseRefId.slice(0, -1);
});

// Chip class based on selected tier
const refIdChipClass = computed(() => {
  const job = currentJob.value;
  if (!job?.selectedTierName) {
    return 'refid-chip refid-chip-none';
  }

  const tierName = job.selectedTierName.toLowerCase();
  if (tierName.includes('platinum')) return 'refid-chip refid-chip-platinum';
  if (tierName.includes('gold')) return 'refid-chip refid-chip-gold';
  if (tierName.includes('silver')) return 'refid-chip refid-chip-silver';
  if (tierName.includes('bronze')) return 'refid-chip refid-chip-bronze';
  if (tierName.includes('band')) return 'refid-chip refid-chip-bandaid';
  return 'refid-chip refid-chip-none';
});

// Problem description for info modal
const problemDescription = computed(() => {
  return currentJob.value?.problem?.description || '';
});

// Service agreement discount rate (3% discount)
const SERVICE_AGREEMENT_RATE = 0.97;

// Warranty text based on tier name
function getWarrantyText(tierName: string): string {
  const name = tierName?.toLowerCase() || '';
  if (name.includes('diamond') || name.includes('platinum')) return '2 year limited warranty';
  if (name.includes('gold')) return '18 month limited warranty';
  if (name.includes('silver')) return '1 year limited warranty';
  if (name.includes('bronze')) return '6 month limited warranty';
  if (name.includes('economy') || name.includes('band')) return '30 day limited warranty';
  return '';
}

// Menu tiers from the job
const menuTiers = computed(() => {
  const menu = currentJob.value?.menus?.[0];
  if (!menu?.tiers) return [];

  return menu.tiers.map((t: any) => {
    const price = t.price || 0;
    const priceConverted = t.priceConverted?.formatted || '';
    // Calculate discount price using service agreement rate
    const discountPrice = t.discountPrice || (price * SERVICE_AGREEMENT_RATE);
    // Calculate converted discount price
    let discountPriceConverted = '';
    if (t.priceConverted) {
      const symbol = t.priceConverted.symbol || '';
      const discountValue = (t.priceConverted.value || 0) * SERVICE_AGREEMENT_RATE;
      discountPriceConverted = `${symbol}${Math.ceil(discountValue).toLocaleString()}`;
    }

    const tierName = t.tier?.name || t.name;
    return {
      id: t.id,
      refId: t.refId,
      name: tierName,
      title: tierName,
      rank: t.tier?.rank,
      warranty: t.tier?.warrantyCopy || getWarrantyText(tierName),
      offer: t.offer,
      menuCopy: t.menuCopy,
      contentItems: t.contentItems,
      price,
      priceConverted,
      discountPrice,
      discountPriceConverted,
    };
  });
});

// Get CSS class for tier styling
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 '';
}

// Toggle service agreement
async function toggleServiceAgreement() {
  tnfrStore.applyDiscount = !tnfrStore.applyDiscount;
  await tnfrStore.save();
}

// Toggle payment plan
async function togglePaymentPlan() {
  tnfrStore.showPlan = !tnfrStore.showPlan;
  await tnfrStore.save();
}

// Check if tier is selected
function isTierSelected(tier: any): boolean {
  const job = currentJob.value;
  if (tier.offer && job?.selectedOffer) {
    return job.selectedOffer.id === tier.offer.id;
  }
  return false;
}

// Handle tier selection
async function selectTier(tier: any) {
  const job = currentJob.value;
  if (!job || !tier.offer) return;

  const price = tier.price;
  const tierBaseHours = tier.offer?.costsTime?.reduce(
    (sum: number, cost: any) => sum + (cost.hours || 0),
    0
  ) || 0;

  // Extract content from menuCopy or contentItems
  const tierContent: string[] = [];
  if (tier.menuCopy?.length > 0) {
    tier.menuCopy.forEach((copy: any) => {
      copy.contentItems?.forEach((item: any) => {
        if (item.content) tierContent.push(item.content);
      });
    });
  } else if (tier.contentItems?.length > 0) {
    tier.contentItems.forEach((item: any) => {
      if (item.content) tierContent.push(item.content);
    });
  }

  await tnfrStore.setSelectedOffer(
    job.id,
    tier.offer,
    price.toString(),
    tier.name,
    tier.title,
    tierBaseHours,
    tierContent
  );
}

// Navigate to next menu (cycles through, never goes to payment)
function navigateToNext() {
  const job = currentJob.value;
  if (!job) return;

  const jobs = sortedJobs.value;
  if (jobs.length === 0) return;

  const idx = jobs.findIndex(j => j.id === job.id);
  if (idx === -1) {
    router.push(`/menu/${jobs[0].id}`);
    return;
  }

  // Cycle to next menu (wrap around at end)
  const nextIdx = (idx + 1) % jobs.length;
  router.push(`/menu/${jobs[nextIdx].id}`);
}

// Navigate to previous menu (cycles through)
function navigatePrev() {
  const job = currentJob.value;
  if (!job) return;

  const jobs = sortedJobs.value;
  if (jobs.length === 0) return;

  const idx = jobs.findIndex(j => j.id === job.id);
  if (idx === -1) {
    router.push(`/menu/${jobs[0].id}`);
    return;
  }

  // Cycle to previous menu (wrap around at beginning)
  const prevIdx = (idx - 1 + jobs.length) % jobs.length;
  router.push(`/menu/${jobs[prevIdx].id}`);
}

// Handle prev arrow click
function handlePrevClick() {
  if (!isSingleMenu.value) {
    navigatePrev();
  }
}

// Handle next arrow click
function handleNextClick() {
  if (!isSingleMenu.value) {
    navigateToNext();
  }
}
</script>

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

.menu-page-heading {
  text-align: center;
  margin-bottom: 12px;
}

.menu-page-heading h1 {
  font-size: 32px;
  font-weight: 700;
  margin: 0 0 8px 0;
  color: var(--ion-text-color);
}

.job-name-subtitle {
  font-size: 24px;
  font-weight: 500;
  margin: 0;
  color: var(--ion-color-primary);
  text-transform: capitalize;
}

.refid-chip {
  font-size: 14px;
  font-weight: 600;
  margin-top: 8px;
}

.refid-chip-none {
  --background: transparent;
  --color: var(--ion-color-medium);
  border: 2px solid var(--ion-color-medium);
}

.refid-chip-platinum {
  --background: var(--menu-color-platinum, #3db4d6);
  --color: #fff;
}

.refid-chip-gold {
  --background: var(--menu-color-gold, #ffd83b);
  --color: #000;
}

.refid-chip-silver {
  --background: var(--menu-color-silver, #bfbfbf);
  --color: #000;
}

.refid-chip-bronze {
  --background: var(--menu-color-bronze, #ffad2b);
  --color: #000;
}

.refid-chip-bandaid {
  --background: var(--menu-color-bandaid, #ff8073);
  --color: #fff;
}

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 300px;
  gap: 16px;
}

.empty-state p {
  font-size: 18px;
  color: var(--ion-color-medium);
}

.menu-grid {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.agreement-nav-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
  width: 100%;
}

.nav-arrow {
  --padding-start: 12px;
  --padding-end: 12px;
  --background: var(--ion-color-primary);
  --background-hover: var(--ion-color-primary-shade);
  --color: var(--ion-color-primary-contrast);
  --border-radius: 50%;
  width: 48px;
  height: 48px;
  min-width: 48px;
}

.nav-arrow ion-icon {
  font-size: 28px;
}

.nav-arrow-disabled {
  --background: var(--ion-color-light);
  --color: var(--ion-color-medium);
  opacity: 0.5;
  cursor: default;
}

.agreement-card {
  background: var(--ion-color-light);
  border-radius: 8px;
  padding: 12px 16px;
}

.agreement-row {
  display: flex;
  align-items: center;
  gap: 32px;
}

.agreement-option {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  transition: opacity 0.2s ease;
}

.agreement-option:hover {
  opacity: 0.8;
}

.agreement-icon {
  font-size: 24px;
  color: var(--ion-color-medium);
  transition: color 0.2s ease;
  margin: 0;
  padding: 0;
}

.agreement-icon.checked {
  color: var(--ion-color-success);
}

.agreement-label {
  font-size: 16px;
  font-weight: 500;
  color: var(--ion-text-color);
}

/* Tech Handbook View */
.tech-handbook-view {
  padding: 16px;
  background: var(--ion-background-color);
}

.tech-handbook-description {
  margin-bottom: 24px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--ion-color-light);
}

.tech-handbook-description h3 {
  font-size: 18px;
  font-weight: 600;
  margin: 0 0 8px 0;
  color: var(--ion-text-color);
}

.tech-handbook-description p {
  font-size: 16px;
  line-height: 1.5;
  margin: 0;
  color: var(--ion-text-color);
}

.tech-handbook-tier {
  margin-bottom: 24px;
}

.tech-handbook-tier-name {
  font-size: 18px;
  font-weight: 600;
  margin: 0 0 12px 0;
  color: var(--ion-text-color);
  text-transform: uppercase;
}

.tech-handbook-list {
  margin: 0;
  padding-left: 20px;
  list-style-type: disc;
}

.tech-handbook-list li {
  font-size: 15px;
  line-height: 1.6;
  color: var(--ion-text-color);
  margin-bottom: 6px;
}

.tech-handbook-empty {
  font-size: 14px;
  color: var(--ion-color-medium);
  font-style: italic;
  margin: 0;
}
</style>