Hello from MCP server
<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>