Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
<template>
  <BaseLayout title="Technician - Confirm Job">
    <!-- Toolbar at top of screen -->
    <Toolbar :force-step2="true" @help-clicked="openInfoModal" />

    <div class="confirm-job-container">
      <h2 class="confirm-job-title">Is this the job you're doing?</h2>

      <!-- Yes/No Buttons -->
      <div class="yes-no-buttons">
        <YesButton :disabled="!allSectionsConfirmed" @click="handleYes" />
        <NoButton :disabled="!allSectionsConfirmed" @click="handleNo" />
      </div>
      <p v-if="!allSectionsConfirmed" class="confirm-message">Confirm the sections below to continue</p>

      <div class="accept-all-button-container">
        <ion-button
          :disabled="preferencesStore.role === 'tech-in-training'"
          @click="handleAcceptAll"
          expand="block"
        >
          Accept All
        </ion-button>
      </div>

      <Divider />

      <!-- Section 1: Job Description -->
      <ConfirmationSection :confirmed="jobDescriptionConfirmed" @confirm="confirmJobDescription">
        <!-- Edit Mode: Pencil icon to edit offer -->
        <div v-if="sessionStore.editMode && firstOfferId" class="edit-offer-link">
          <router-link :to="`/editor/offers/${firstOfferId}`" target="_blank">
            <ion-icon :icon="pencil" class="edit-pencil-icon"></ion-icon>
          </router-link>
        </div>
        <h3 class="section-heading">Job Description</h3>
        <h4 class="job-name">{{ problem?.name || "Loading..." }}</h4>
        <div
          class="description"
          v-html="formatDescriptionWithHashtags(problem?.description)"
        ></div>
      </ConfirmationSection>

      <!-- Section 2: Job Steps -->
      <ConfirmationSection :confirmed="jobStepsConfirmed" @confirm="confirmJobSteps">
        <h3 class="section-heading">Job Steps</h3>
        <p class="section-message">These steps are for the minimum service. The customer may choose more options.</p>
        <div class="steps-list">
          <ol>
            <li v-for="(step, index) in techHandbookSteps" :key="index">
              {{ step }}
            </li>
          </ol>
        </div>
      </ConfirmationSection>

      <!-- Section 3: Menu Preview -->
      <ConfirmationSection :confirmed="menuPreviewConfirmed" @confirm="confirmMenuPreview">
        <h3 class="section-heading">Menu Preview</h3>
        <p class="section-message">These are the offers we will show the customer.</p>
        <div class="menu-preview-list">
          <div v-if="menuOffers.length === 0" class="no-menu-message">
            Loading menu offers...
          </div>
          <div v-else>
            <div
              v-for="(offer, index) in menuOffers"
              :key="index"
              class="menu-offer-item"
            >
              <h4 class="offer-title">{{ offer.tierName }}</h4>
              <ul class="offer-items">
                <li v-for="(item, itemIndex) in offer.contentItems" :key="itemIndex">
                  {{ item }}
                </li>
              </ul>
            </div>
          </div>
        </div>
      </ConfirmationSection>

      <!-- Debug Panel (Secret Mode Only) -->
      <div v-if="sessionStore.secretMode" class="debug-panel">
        <button class="debug-toggle" @click="debugPanelOpen = !debugPanelOpen">
          {{ debugPanelOpen ? '▼' : '▶' }} Debug Info
        </button>
        <div v-if="debugPanelOpen" class="debug-content">
          <table class="debug-table">
            <tr>
              <td>Problem ID:</td>
              <td>{{ debugInfo.problemId }}</td>
            </tr>
            <tr>
              <td>Menu IDs:</td>
              <td>{{ debugInfo.menuIds.join(', ') || '(none)' }}</td>
            </tr>
            <tr>
              <td>Tiers Count:</td>
              <td>{{ debugInfo.tiersCount }}</td>
            </tr>
            <tr>
              <td>techHandbook (raw):</td>
              <td class="debug-mono">{{ debugInfo.firstOfferTechHandbook }}</td>
            </tr>
            <tr>
              <td>contentItems found:</td>
              <td>{{ debugInfo.contentItemsCount }}</td>
            </tr>
            <tr>
              <td>Schema Version:</td>
              <td>{{ debugInfo.schemaVersion ?? '(unknown)' }}</td>
            </tr>
            <tr>
              <td>Has techHandbook col:</td>
              <td :class="debugInfo.hasTechHandbookColumn ? 'debug-ok' : 'debug-error'">
                {{ debugInfo.hasTechHandbookColumn === null ? '(unknown)' : debugInfo.hasTechHandbookColumn ? 'Yes' : 'NO' }}
              </td>
            </tr>
            <tr>
              <td>Using defaults:</td>
              <td :class="offerTechHandbook.length === 0 ? 'debug-error' : 'debug-ok'">
                {{ offerTechHandbook.length === 0 ? 'YES (problem!)' : 'No' }}
              </td>
            </tr>
          </table>
        </div>
      </div>
    </div>

    <!-- Info Modal -->
    <ion-modal :is-open="isInfoModalOpen" @didDismiss="closeInfoModal">
      <ion-header>
        <ion-toolbar>
          <ion-title>Session Store</ion-title>
          <ion-buttons slot="end">
            <ion-button @click="closeInfoModal">Close</ion-button>
          </ion-buttons>
        </ion-toolbar>
      </ion-header>
      <ion-content class="ion-padding">
        <div class="info-content">
          <pre>{{ JSON.stringify(sessionStore.$state, null, 2) }}</pre>
        </div>
      </ion-content>
    </ion-modal>
  </BaseLayout>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import {
  IonIcon,
  IonModal,
  IonHeader,
  IonToolbar,
  IonTitle,
  IonButtons,
  IonButton,
  IonContent,
  toastController,
} from "@ionic/vue";
import { checkmarkCircle, pencil } from "ionicons/icons";
import BaseLayout from "@/components/BaseLayout.vue";
import Toolbar from "@/components/Toolbar.vue";
import Divider from "@/components/Divider.vue";
import YesButton from "@/components/YesButton.vue";
import NoButton from "@/components/NoButton.vue";
import ConfirmationSection from "@/components/ConfirmationSection.vue";
import { getDb } from "@/dataAccess/getDb";
import { useSessionStore } from "@/stores/session";
import { usePreferencesStore } from "@/stores/preferences";
import type { ProblemsRecord } from "@/pocketbase-types";

const route = useRoute();
const router = useRouter();
const sessionStore = useSessionStore();
const preferencesStore = usePreferencesStore();

const problem = ref<ProblemsRecord | null>(null);
const isInfoModalOpen = ref(false);

// Confirmation states for each section
const jobDescriptionConfirmed = ref(false);
const jobStepsConfirmed = ref(false);
const menuPreviewConfirmed = ref(false);

// Menu offers data
const menuOffers = ref<Array<{ tierName: string; contentItems: string[]; offerId?: string }>>([]);

// First offer ID for edit link
const firstOfferId = ref<string | null>(null);

// Debug info for troubleshooting
const debugInfo = ref<{
  problemId: string;
  menuIds: string[];
  tiersCount: number;
  firstOfferTechHandbook: string | null;
  contentItemsCount: number;
  schemaVersion: number | null;
  hasTechHandbookColumn: boolean | null;
}>({
  problemId: '',
  menuIds: [],
  tiersCount: 0,
  firstOfferTechHandbook: null,
  contentItemsCount: 0,
  schemaVersion: null,
  hasTechHandbookColumn: null,
});
const debugPanelOpen = ref(false);

// Tech handbook steps from the lowest tier offer
const offerTechHandbook = ref<string[]>([]);

// Default tech handbook steps
const DEFAULT_STEPS = [
  "Do some work",
  "Do some more work",
  "And there are different steps.",
  "Clean up the job site.",
  "Tell the customer thank you."
];

// Get tech handbook steps from offer (lowest tier) or use default
const techHandbookSteps = computed(() => {
  if (offerTechHandbook.value.length > 0) {
    return offerTechHandbook.value;
  }
  return DEFAULT_STEPS;
});

// Check if all sections are confirmed
const allSectionsConfirmed = computed(() => {
  return jobDescriptionConfirmed.value &&
         jobStepsConfirmed.value &&
         menuPreviewConfirmed.value;
});

// Function to reset state and load job
const resetAndLoadJob = async () => {
  // Reset confirmation states
  jobDescriptionConfirmed.value = false;
  jobStepsConfirmed.value = false;
  menuPreviewConfirmed.value = false;
  menuOffers.value = [];
  offerTechHandbook.value = [];
  firstOfferId.value = null;

  // Reset debug info
  debugInfo.value = {
    problemId: '',
    menuIds: [],
    tiersCount: 0,
    firstOfferTechHandbook: null,
    contentItemsCount: 0,
    schemaVersion: null,
    hasTechHandbookColumn: null,
  };

  const problemId = route.params.problemId as string;
  debugInfo.value.problemId = problemId;
  const db = await getDb();

  if (db && problemId) {
    // Load the problem
    problem.value = await db.problems.byId(problemId);

    // Load menu offers for preview
    if (problem.value && problem.value.menus) {
      let menuIds: string[] = [];
      if (typeof problem.value.menus === 'string') {
        try {
          menuIds = JSON.parse(problem.value.menus);
        } catch (e) {
          menuIds = [problem.value.menus];
        }
      } else if (Array.isArray(problem.value.menus)) {
        menuIds = problem.value.menus;
      }

      console.log('[ConfirmJob] menuIds:', menuIds);
      debugInfo.value.menuIds = menuIds;

      // Fetch menu data with offers
      for (const menuId of menuIds) {
        const menuData = await db.menus.byMenuId(menuId);
        console.log('[ConfirmJob] menuId:', menuId, 'menuData:', menuData);
        if (menuData && menuData.tiers) {
          console.log('[ConfirmJob] tiers count:', menuData.tiers.length);
          debugInfo.value.tiersCount = menuData.tiers.length;

          // Populate menu offers for preview
          for (const tier of menuData.tiers) {
            const contentItems: string[] = [];

            // Extract content items from menuCopy or contentItems
            if (tier.menuCopy && Array.isArray(tier.menuCopy)) {
              for (const copy of tier.menuCopy) {
                if (copy.contentItems && Array.isArray(copy.contentItems)) {
                  for (const item of copy.contentItems) {
                    if (item.content) {
                      contentItems.push(item.content);
                    }
                  }
                }
              }
            } else if (tier.contentItems && Array.isArray(tier.contentItems)) {
              for (const item of tier.contentItems) {
                // Skip title items
                if (item.content && !item.refId?.includes('_title')) {
                  contentItems.push(item.content);
                }
              }
            }

            menuOffers.value.push({
              tierName: tier.name || 'Unknown Tier',
              contentItems,
              offerId: tier.offer?.id,
            });

            // Store first offer ID for edit link
            if (!firstOfferId.value && tier.offer?.id) {
              firstOfferId.value = tier.offer.id;
            }
          }

          // Extract offers from tiers (in order: Platinum, Gold, Silver, Bronze, Band-Aid)
          // Find the last tier (band-aid/cheapest) to get its tech handbook
          const lastTier = menuData.tiers[menuData.tiers.length - 1];

          if (lastTier && lastTier.offer) {
            const fullOffer = await db.offers.byOfferId(lastTier.offer.id);
            console.log('[ConfirmJob] fullOffer for last tier', lastTier.name, ':', fullOffer);
            console.log('[ConfirmJob] techHandbookExpanded for last tier:', fullOffer?.techHandbookExpanded);

            // Capture debug info for first offer (still useful to see the first one)
            if (debugInfo.value.firstOfferTechHandbook === null) {
              debugInfo.value.firstOfferTechHandbook = fullOffer?.techHandbook || '(not set)';
              debugInfo.value.contentItemsCount = fullOffer?.techHandbookExpanded?.length || 0;
            }

            if (fullOffer?.techHandbookExpanded && fullOffer.techHandbookExpanded.length > 0) {
              // Extract content from each contentItem and reverse to get correct order
              offerTechHandbook.value = fullOffer.techHandbookExpanded.map(
                (item: any) => item.content
              ).filter((content: string) => content).reverse();
              console.log('[ConfirmJob] offerTechHandbook set from last tier:', offerTechHandbook.value);
            } else {
              console.log('[ConfirmJob] No techHandbookExpanded on last tier offer, will use defaults');
            }
          }

        }
      }
    }

    // Gather schema debug info
    try {
      const schemaVersionResult = await db.dbConn.query('PRAGMA user_version');
      debugInfo.value.schemaVersion = schemaVersionResult.values?.[0]?.user_version ?? null;

      const tableInfoResult = await db.dbConn.query('PRAGMA table_info(offers)');
      const columns = tableInfoResult.values?.map((row: any) => row.name) || [];
      debugInfo.value.hasTechHandbookColumn = columns.includes('techHandbook');
      console.log('[ConfirmJob] Schema debug:', debugInfo.value);
    } catch (e) {
      console.error('[ConfirmJob] Error getting schema info:', e);
    }
  }

  // Load session first to ensure we have the latest data
  await sessionStore.load();

  // In edit mode, auto-confirm all sections
  if (sessionStore.editMode) {
    jobDescriptionConfirmed.value = true;
    jobStepsConfirmed.value = true;
    menuPreviewConfirmed.value = true;
  }

  // Set step1 to true when viewing a job
  sessionStore.appProgress.step1 = true;
  await sessionStore.save();
};

// Watch for route changes to reset state when switching jobs
watch(() => route.params.problemId, async (newProblemId, oldProblemId) => {
  if (newProblemId && newProblemId !== oldProblemId) {
    await resetAndLoadJob();
  }
});

onMounted(async () => {
  await preferencesStore.getPreferences();
  await resetAndLoadJob();
});

const openInfoModal = () => {
  isInfoModalOpen.value = true;
};

const closeInfoModal = () => {
  isInfoModalOpen.value = false;
};

const formatDescriptionWithHashtags = (description: string | undefined) => {
  if (!description) return "";

  // Split into lines and process each one
  const lines = description.split("\n");
  const processedLines = lines.map((line) => {
    if (line.includes(":")) {
      return '<span class="colon-line">' + line + "</span>";
    }
    return line;
  });

  // Join lines with <br> tags
  let formatted = processedLines.join("<br>");

  // Find the first # and style everything from there onwards
  const hashIndex = formatted.indexOf("#");
  if (hashIndex === -1) return formatted;

  const beforeHash = formatted.substring(0, hashIndex);
  const afterHash = formatted.substring(hashIndex);

  return beforeHash + '<span class="hashtag">' + afterHash + "</span>";
};

const confirmJobDescription = () => {
  jobDescriptionConfirmed.value = true;
};

const confirmJobSteps = () => {
  jobStepsConfirmed.value = true;
};

const confirmMenuPreview = () => {
  menuPreviewConfirmed.value = true;
};

const handleAcceptAll = async () => {
  if (preferencesStore.role === 'tech-in-training') {
    const toast = await toastController.create({
      message: "This feature is not available for your role.",
      duration: 2000,
      color: "warning",
    });
    await toast.present();
    return;
  }
  jobDescriptionConfirmed.value = true;
  jobStepsConfirmed.value = true;
  menuPreviewConfirmed.value = true;
  await handleYes();
};

const handleYes = async () => {
  if (problem.value) {
    // Create a new job in the session store
    const newJobId = `${sessionStore.sessionId}-${Date.now()}`;
    await sessionStore.addJob(newJobId);
    await sessionStore.addProblemToJob(newJobId, problem.value);

    // Set step2 to true when confirm job is completed
    sessionStore.appProgress.step2 = true;
    await sessionStore.save();

    // Navigate to check-hours with problem ID and job ID
    router.push(`/check-hours/${problem.value.id}?jobId=${newJobId}`);
  }
};

const handleNo = async () => {
  // Set step2 to false
  sessionStore.appProgress.step2 = false;
  await sessionStore.save();

  // Navigate back to find-job (step 1)
  router.push('/find-job');
};
</script>

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

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

.confirm-message {
  margin: 16px 0 0 0;
  text-align: center;
  color: var(--ion-text-color);
  font-size: 28px;
  font-weight: 500;
  font-variant: small-caps;
}

.yes-no-buttons {
  display: flex;
  justify-content: center;
  gap: 16px;
  margin-bottom: 24px;
}

.accept-all-button-container {
  display: flex;
  justify-content: center;
  margin-top: 24px;
  margin-bottom: 32px;
}

.accept-all-button-container ion-button {
  --border-radius: 8px;
  max-width: 300px; /* Adjust as needed */
}

/* Section Content Styles */
.section-heading {
  margin: 0 0 16px 0;
  color: var(--ion-text-color);
  font-size: 24px;
  font-weight: 700;
  text-align: center;
}

.section-message {
  margin: 0 0 24px 0;
  color: var(--ion-color-medium);
  font-size: 16px;
  font-style: italic;
  text-align: center;
}

.job-name {
  margin: 0 0 16px 0;
  color: var(--ion-text-color);
  font-size: 22px;
  font-weight: 600;
}

.description {
  color: var(--ion-text-color);
  font-size: 18px;
  line-height: 1.6;
}

.description :deep(.hashtag) {
  color: var(--ion-color-medium);
  font-size: 16px;
}

.description :deep(.colon-line) {
  display: inline-block;
  margin-top: 16px;
}

.steps-list {
  color: var(--ion-text-color);
  font-size: 18px;
  line-height: 1.6;
}

.steps-list ol {
  margin: 0;
  padding-left: 24px;
}

.steps-list li {
  margin-bottom: 12px;
  padding-left: 8px;
}

.menu-preview-list {
  color: var(--ion-text-color);
}

.no-menu-message {
  text-align: center;
  padding: 20px;
  color: var(--ion-color-medium);
  font-style: italic;
}

.menu-offer-item {
  margin-bottom: 32px;
  padding-bottom: 24px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.menu-offer-item:last-child {
  border-bottom: none;
  margin-bottom: 0;
  padding-bottom: 0;
}

.offer-title {
  margin: 0 0 16px 0;
  color: var(--ion-color-primary);
  font-size: 20px;
  font-weight: 600;
}

.offer-items {
  list-style-type: disc;
  padding-left: 24px;
  margin: 0;
}

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

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

  .confirm-job-title {
    font-size: 28px;
  }

  .section-heading {
    font-size: 20px;
  }

  .section-message {
    font-size: 14px;
  }

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

  .description,
  .steps-list {
    font-size: 16px;
  }

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

  .offer-items li {
    font-size: 14px;
  }
}

/* Info Modal Styles */
.info-content {
  color: var(--ion-color-dark);
}

.info-content pre {
  background: var(--ion-color-light);
  padding: 16px;
  border-radius: 8px;
  overflow-x: auto;
  font-size: 12px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-wrap: break-word;
}

/* Debug Panel Styles */
.debug-panel {
  margin-top: 32px;
  background: rgba(255, 165, 0, 0.1);
  border: 2px solid orange;
  border-radius: 8px;
  padding: 12px;
}

.debug-toggle {
  background: none;
  border: none;
  color: orange;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  padding: 4px 8px;
}

.debug-content {
  margin-top: 12px;
}

.debug-table {
  width: 100%;
  font-size: 14px;
  border-collapse: collapse;
}

.debug-table td {
  padding: 6px 8px;
  border-bottom: 1px solid rgba(255, 165, 0, 0.3);
}

.debug-table td:first-child {
  font-weight: 600;
  color: orange;
  width: 160px;
}

.debug-mono {
  font-family: monospace;
  font-size: 12px;
  word-break: break-all;
}

.debug-ok {
  color: #4CAF50;
  font-weight: 600;
}

.debug-error {
  color: #f44336;
  font-weight: 600;
}

/* Edit Mode Pencil Icon */
.edit-offer-link {
  display: flex;
  justify-content: center;
  margin-bottom: 16px;
}

.edit-pencil-icon {
  font-size: 48px;
  color: var(--ion-color-primary);
  cursor: pointer;
  transition: transform 0.2s ease, color 0.2s ease;
}

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

.edit-pencil-icon:active {
  transform: scale(0.95);
}
</style>