Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
<template>
  <BaseLayout title="Technician" @debug="isDebugOpen = true">
    <div class="job-content-container">
      <h1 class="page-title">{{ jobTitle }}</h1>
      <p v-if="jobRefId" class="page-subtitle">{{ jobRefId }}</p>

      <div v-if="isLoading" class="loading-message">
        Loading tech handbook...
      </div>

      <div v-else-if="!currentJob && !jobContentHierarchy" class="error-message">
        Job not found
      </div>

      <div v-else>
        <!-- Action Buttons -->
        <div class="action-buttons">
          <template v-if="tnfrStore.invoice.paymentConfirmed">
            <ion-button fill="solid" color="primary" @click="router.push('/review')">
              Go to Review
            </ion-button>
          </template>
          <template v-else>
            <ion-button fill="solid" color="success" @click="addToCart">
              Select this Menu
            </ion-button>
            <ion-button fill="solid" color="danger" @click="router.push('/directory')">
              Select Another Menu
            </ion-button>
          </template>
        </div>

        <!-- Accordion Sections -->
        <ion-accordion-group :multiple="true">
          <!-- Job Description -->
          <ion-accordion value="job-description">
            <ion-item slot="header" class="accordion-header">
              <ion-label>Job Description</ion-label>
            </ion-item>
            <div slot="content" class="accordion-content">
              <div v-if="jobContentHierarchy?.problem?.description">
                <p class="job-description-text">{{ jobContentHierarchy.problem.description }}</p>
                <p v-if="jobRefId" class="job-ref-id">{{ jobRefId }}</p>
              </div>
              <p v-else class="no-content">No job description available.</p>
            </div>
          </ion-accordion>

          <!-- Tech Handbook -->
          <ion-accordion value="tech-handbook">
            <ion-item slot="header" class="accordion-header">
              <ion-label>Tech Handbook</ion-label>
            </ion-item>
            <div slot="content" class="accordion-content">
              <div v-if="displayedTechHandbook.length > 0">
                <div v-for="tier in displayedTechHandbook" :key="tier.tierName" class="tier-section">
                  <h4 class="tier-name">{{ tier.tierName }} Tech Handbook</h4>
                  <ol class="handbook-list">
                    <li v-for="(item, index) in tier.items" :key="index">{{ item }}</li>
                  </ol>
                </div>
              </div>
              <p v-else class="no-content">No tech handbook information available.</p>
            </div>
          </ion-accordion>

          <!-- Menu Preview -->
          <ion-accordion value="menu-preview">
            <ion-item slot="header" class="accordion-header">
              <ion-label>Menu Preview</ion-label>
            </ion-item>
            <div slot="content" class="accordion-content">
              <div v-if="displayedMenuContent.length > 0">
                <div v-for="tier in displayedMenuContent" :key="tier.tierName" class="tier-section">
                  <h4 class="tier-name">{{ tier.tierName }}</h4>
                  <h5 v-if="tier.items.length > 0" class="menu-item-title">{{ tier.items[0] }}</h5>
                  <ul v-if="tier.items.length > 1" class="menu-content-list">
                    <li v-for="(item, index) in tier.items.slice(1)" :key="index">{{ item }}</li>
                  </ul>
                </div>
              </div>
              <p v-else class="no-content">No menu content available.</p>
            </div>
          </ion-accordion>

          <!-- Costs (Author/Admin only) -->
          <ion-accordion v-if="showCosts" value="costs">
            <ion-item slot="header" class="accordion-header">
              <ion-label>Costs</ion-label>
            </ion-item>
            <div slot="content" class="accordion-content">
              <div v-if="displayedCosts.length > 0">
                <div v-for="tier in displayedCosts" :key="tier.tierName" class="tier-section">
                  <h4 class="tier-name">{{ tier.tierName }}</h4>

                  <!-- Time Costs -->
                  <div v-if="tier.costsTime.length > 0" class="costs-subsection">
                    <h5 class="costs-subtitle">Time</h5>
                    <ul class="costs-list">
                      <li v-for="cost in tier.costsTime" :key="cost.id">
                        {{ cost.name }}: {{ cost.hours }} hr{{ cost.hours !== 1 ? 's' : '' }}
                      </li>
                    </ul>
                  </div>

                  <!-- Material Costs -->
                  <div v-if="tier.costsMaterial.length > 0" class="costs-subsection">
                    <h5 class="costs-subtitle">Materials</h5>
                    <ul class="costs-list">
                      <li v-for="cost in tier.costsMaterial" :key="cost.id">
                        {{ cost.name }}: <ShowCurrency :currency-in="cost.quantity" />
                      </li>
                    </ul>
                  </div>
                </div>
              </div>
              <p v-else class="no-content">No cost information available.</p>
            </div>
          </ion-accordion>
        </ion-accordion-group>
      </div>
    </div>

    <!-- Debug Console -->
    <DebugConsole :is-open="isDebugOpen" @close="isDebugOpen = false">
      <div class="debug-section">
        <h3 class="debug-section-title">Job Info</h3>
        <table class="debug-table">
          <tr>
            <td>Job ID:</td>
            <td>{{ currentJob?.id || 'N/A' }}</td>
          </tr>
          <tr>
            <td>Title:</td>
            <td>{{ currentJob?.title || 'N/A' }}</td>
          </tr>
          <tr>
            <td>Problem ID:</td>
            <td>{{ currentJob?.problem?.id || 'N/A' }}</td>
          </tr>
          <tr>
            <td>Problem Name:</td>
            <td>{{ currentJob?.problem?.name || 'N/A' }}</td>
          </tr>
          <tr>
            <td>Base Hours:</td>
            <td>{{ currentJob?.baseHours || 0 }}</td>
          </tr>
          <tr>
            <td>Extra Time:</td>
            <td>{{ currentJob?.extraTime || 0 }}</td>
          </tr>
          <tr>
            <td>Selected Tier:</td>
            <td>{{ currentJob?.selectedTierName || 'None' }}</td>
          </tr>
          <tr>
            <td>Selected Price:</td>
            <td>{{ currentJob?.selectedPrice || 'N/A' }}</td>
          </tr>
        </table>
      </div>

      <div class="debug-section">
        <h3 class="debug-section-title">Menu Data</h3>
        <table class="debug-table">
          <tr>
            <td>Has Menu Data:</td>
            <td :class="currentJob?.menuData ? 'debug-success' : 'debug-error'">
              {{ currentJob?.menuData ? 'Yes' : 'No' }}
            </td>
          </tr>
          <tr>
            <td>Tiers Count:</td>
            <td>{{ currentJob?.menuData?.tiers?.length || 0 }}</td>
          </tr>
        </table>
      </div>

      <div class="debug-section">
        <h3 class="debug-section-title">Tech Handbook (from hierarchy)</h3>
        <table class="debug-table">
          <tr>
            <td>Tiers with Handbook:</td>
            <td>{{ displayedTechHandbook.length }}</td>
          </tr>
          <tr v-for="tier in displayedTechHandbook" :key="tier.tierName">
            <td>{{ tier.tierName }}:</td>
            <td>{{ tier.items.length }} items</td>
          </tr>
        </table>
      </div>

      <div class="debug-section">
        <h3 class="debug-section-title">Raw Job Data</h3>
        <pre class="debug-json">{{ JSON.stringify(currentJob, null, 2) }}</pre>
      </div>

      <div class="debug-section">
        <h3 class="debug-section-title">Job Content Hierarchy (loadJobContentByProblemId)</h3>
        <pre class="debug-json">{{ JSON.stringify(jobContentHierarchy, null, 2) }}</pre>
      </div>
    </DebugConsole>
  </BaseLayout>
</template>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { IonButton, IonAccordionGroup, IonAccordion, IonItem, IonLabel, onIonViewWillEnter } from '@ionic/vue';
import BaseLayout from '@/components/BaseLayout.vue';
import DebugConsole from '@/components/DebugConsole.vue';
import ShowCurrency from '@/components/ShowCurrency.vue';
import { useTnfrStore } from '@/stores/tnfr';
import { loadJobContentByProblemId, type JobContentHierarchy } from '@/framework/jobContent';

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

const isLoading = ref(true);
const isDebugOpen = ref(false);
const jobContentHierarchy = ref<JobContentHierarchy | null>(null);

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

// Get all jobs in cart with this problem ID
const jobsInCart = computed(() => {
  if (!tnfrStore.jobs || !problemId.value) return [];
  return tnfrStore.jobs.filter(j => j.problem?.id === problemId.value);
});

// Get the current job from session store (find by problem ID) - first one
const currentJob = computed(() => {
  return jobsInCart.value.length > 0 ? jobsInCart.value[0] : null;
});

// Job title - use currentJob if available, otherwise use jobContentHierarchy
const jobTitle = computed(() => {
  if (currentJob.value) {
    return currentJob.value.title || currentJob.value.problem?.name || 'Untitled Job';
  }
  return jobContentHierarchy.value?.problem?.name || 'Untitled Job';
});

// Job ref ID - use currentJob if available, otherwise use jobContentHierarchy
// Remove "_problem" suffix from refId
const jobRefId = computed(() => {
  let refId = '';
  if (currentJob.value) {
    refId = currentJob.value.problem?.refId || '';
  } else {
    refId = jobContentHierarchy.value?.problem?.refId || '';
  }
  return refId.replace(/_problem$/, '');
});

// Build menu content data from jobContentHierarchy
const displayedMenuContent = computed(() => {
  const hierarchy = jobContentHierarchy.value;
  if (!hierarchy?.menus?.length) return [];

  const allTiers: { tierName: string; offerId: string; items: string[] }[] = [];

  for (const menu of hierarchy.menus) {
    for (const menuTier of menu.tiers) {
      // Collect content items: direct contentItems (title) first, then menuCopy (details)
      const items: string[] = [];

      // Add direct content items first (menu item title)
      for (const ci of menuTier.contentItems || []) {
        if (ci.content) {
          items.push(ci.content);
        }
      }

      // Add menuCopy content items (details)
      for (const mc of menuTier.menuCopy || []) {
        for (const ci of mc.contentItems || []) {
          if (ci.content) {
            items.push(ci.content);
          }
        }
      }

      if (items.length > 0) {
        allTiers.push({
          tierName: menuTier.tier.name,
          offerId: menuTier.offer.id,
          items,
        });
      }
    }
  }

  return allTiers;
});

// Build tech handbook data from jobContentHierarchy
const displayedTechHandbook = computed(() => {
  const hierarchy = jobContentHierarchy.value;
  if (!hierarchy?.menus?.length) return [];

  const allTiers: { tierName: string; offerId: string; items: string[] }[] = [];

  for (const menu of hierarchy.menus) {
    for (const menuTier of menu.tiers) {
      const items = menuTier.offer.techHandbook
        .map(item => item.content)
        .filter(content => content)
        .reverse();

      if (items.length > 0) {
        allTiers.push({
          tierName: menuTier.tier.name,
          offerId: menuTier.offer.id,
          items,
        });
      }
    }
  }

  return allTiers;
});

// Check if user has author or admin role AND secret mode is enabled
const showCosts = computed(() => {
  const hasRole = tnfrStore.role === 'author' || tnfrStore.role === 'admin';
  return hasRole && tnfrStore.secretMode;
});

// Build costs data from jobContentHierarchy
const displayedCosts = computed(() => {
  const hierarchy = jobContentHierarchy.value;
  if (!hierarchy?.menus?.length) return [];

  const allTiers: { tierName: string; offerId: string; costsTime: any[]; costsMaterial: any[] }[] = [];

  for (const menu of hierarchy.menus) {
    for (const menuTier of menu.tiers) {
      const costsTime = menuTier.offer.costsTime || [];
      const costsMaterial = menuTier.offer.costsMaterial || [];

      if (costsTime.length > 0 || costsMaterial.length > 0) {
        allTiers.push({
          tierName: menuTier.tier.name,
          offerId: menuTier.offer.id,
          costsTime,
          costsMaterial,
        });
      }
    }
  }

  return allTiers;
});

const addToCart = async () => {
  // Add the problem to the cart and navigate to confirm hours
  const problem = jobContentHierarchy.value?.problem;
  if (problem?.id) {
    await tnfrStore.addJob(problem.id);
    router.push(`/check-hours/${problem.id}`);
  }
};

const loadContent = async () => {
  isLoading.value = true;
  try {
    await tnfrStore.load();

    // Load the full job content hierarchy using problem ID from route
    if (problemId.value) {
      console.log('[JobContent] Loading content for problem ID:', problemId.value);
      jobContentHierarchy.value = await loadJobContentByProblemId(problemId.value);
      console.log('[JobContent] Loaded hierarchy:', jobContentHierarchy.value);
    }
  } catch (error) {
    console.error('[JobContent] Error loading:', error);
  } finally {
    isLoading.value = false;
  }
};

onMounted(loadContent);

// Ionic lifecycle hook - fires every time view becomes active (including back navigation)
onIonViewWillEnter(loadContent);
</script>

<style scoped>
.job-content-container {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
}

.page-title {
  margin: 0 0 24px 0;
  color: var(--ion-text-color);
  font-size: 32px;
  font-weight: 700;
  text-align: center;
  font-variant: small-caps;
}

.page-title + .page-subtitle {
  margin-top: -20px;
}

.page-subtitle {
  margin: 0 0 24px 0;
  color: var(--ion-color-medium);
  font-size: 14px;
  text-align: center;
  font-family: monospace;
}

.job-ref-id {
  margin: 16px 0 0 0;
  color: var(--ion-color-medium);
  font-size: 14px;
  text-align: right;
  font-family: monospace;
}

.action-buttons {
  display: flex;
  justify-content: center;
  gap: 12px;
  margin-bottom: 16px;
}

.job-description-text {
  color: var(--ion-text-color);
  line-height: 1.6;
  font-size: 16px;
  margin: 0;
  text-align: left;
  white-space: pre-line;
}

.accordion-header {
  --background: var(--ion-card-background, var(--ion-background-color));
  font-size: 18px;
  font-weight: 600;
}

.accordion-content {
  padding: 16px 24px 24px 24px;
  background: var(--ion-card-background, var(--ion-background-color));
}

ion-accordion-group {
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.loading-message,
.error-message,
.no-content {
  text-align: center;
  color: var(--ion-color-medium);
  font-style: italic;
  padding: 40px 20px;
}

.error-message {
  color: var(--ion-color-danger);
}

.tier-section {
  margin-bottom: 24px;
}

.tier-section:last-child {
  margin-bottom: 0;
}

.tier-name {
  margin: 0 0 12px 0;
  color: var(--ion-color-primary);
  font-size: 18px;
  font-weight: 600;
}

.handbook-list {
  margin: 0;
  padding-left: 24px;
  color: var(--ion-text-color);
}

.handbook-list li {
  margin-bottom: 8px;
  line-height: 1.5;
  font-size: 16px;
}

.costs-subsection {
  margin-bottom: 16px;
}

.costs-subsection:last-child {
  margin-bottom: 0;
}

.costs-subtitle {
  margin: 0 0 8px 0;
  color: var(--ion-color-medium);
  font-size: 14px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.costs-list {
  margin: 0;
  padding-left: 24px;
  color: var(--ion-text-color);
}

.costs-list li {
  margin-bottom: 6px;
  line-height: 1.4;
  font-size: 15px;
}

.menu-item-title {
  margin: 0 0 12px 0;
  color: var(--ion-text-color);
  font-size: 17px;
  font-weight: 600;
}

.menu-content-list {
  margin: 0;
  padding-left: 24px;
  color: var(--ion-text-color);
}

.menu-content-list li {
  margin-bottom: 8px;
  line-height: 1.5;
  font-size: 16px;
}

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

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

  .page-subtitle {
    font-size: 12px;
  }

  .job-name {
    font-size: 20px;
  }

  .accordion-content {
    padding: 12px 16px 16px 16px;
  }

  .tier-name {
    font-size: 16px;
  }

  .handbook-list li {
    font-size: 14px;
  }
}
</style>