Hello from MCP server
<template>
<BaseLayout title="Session Review">
<ion-grid :fixed="true">
<ion-row>
<ion-col size="12">
<div class="session-info">
<h2>Current Session</h2>
<p>
{{ sessionStore.jobs.length }} job{{
sessionStore.jobs.length === 1 ? "" : "s"
}}
in session
</p>
</div>
</ion-col>
</ion-row>
<ion-row>
<ion-col
v-for="job in sessionStore.jobs"
:key="job.id"
size="12"
size-md="6"
size-lg="4"
>
<ion-card
class="job-tile"
:class="{ 'has-selection': hasSelectedOffer(job) }"
button
@click="viewJob(job)"
>
<ion-card-header>
<ion-card-title class="menu-name">
{{ getMenuName(job) }}
</ion-card-title>
<ion-card-subtitle class="job-title">
{{ job.title || "Untitled Job" }}
</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<div class="job-status">
<ion-icon
:icon="
hasSelectedOffer(job) ? checkmarkCircle : ellipseOutline
"
:color="hasSelectedOffer(job) ? 'success' : 'medium'"
size="large"
></ion-icon>
<div class="status-text">
<div class="status-label">
{{
hasSelectedOffer(job) ? "Offer Selected" : "No Selection"
}}
</div>
<div v-if="hasSelectedOffer(job)" class="offer-details">
<div class="offer-name">
{{ getSelectedOfferName(job) }}
</div>
<div class="offer-price">
<show-currency :currencyIn="job.selectedPrice" />
</div>
</div>
<div v-else class="no-selection-text">
Click to select an offer
</div>
</div>
</div>
<!-- Checklist Issues -->
<div v-if="job.checklistIssues && job.checklistIssues.length > 0" class="job-checklist-issues">
<div class="section-label">
<ion-icon :icon="alertCircleOutline" color="warning"></ion-icon>
Checklist Items
</div>
<ul class="checklist-list">
<li v-for="(issue, index) in job.checklistIssues" :key="index">
{{ issue }}
</li>
</ul>
</div>
<!-- Notes -->
<div v-if="job.notes && job.notes.length > 0 && job.notes.some((n: string) => n)" class="job-notes">
<div class="section-label">
<ion-icon :icon="documentTextOutline" color="primary"></ion-icon>
Notes
</div>
<ul class="notes-list">
<li v-for="(note, index) in job.notes.filter((n: string) => n)" :key="index">
{{ note }}
</li>
</ul>
</div>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
<ion-row v-if="sessionStore.jobs.length === 0">
<ion-col>
<div class="empty-session">
<ion-icon
:icon="briefcaseOutline"
size="large"
color="medium"
></ion-icon>
<h3>No jobs in session</h3>
<p>Start by adding a job to your session</p>
<ion-button @click="startSession" fill="outline">
Add Job
</ion-button>
</div>
</ion-col>
</ion-row>
</ion-grid>
</BaseLayout>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from "vue";
import { useRouter } from "vue-router";
import { getDb } from "@/dataAccess/getDb";
import BaseLayout from "@/components/BaseLayout.vue";
import ShowCurrency from "@/components/ShowCurrency.vue";
import { useSessionStore } from "@/stores/session";
import {
IonGrid,
IonRow,
IonCol,
IonCard,
IonCardHeader,
IonCardTitle,
IonCardSubtitle,
IonCardContent,
IonIcon,
IonButton,
} from "@ionic/vue";
import {
checkmarkCircle,
ellipseOutline,
briefcaseOutline,
alertCircleOutline,
documentTextOutline,
} from "ionicons/icons";
const sessionStore = useSessionStore();
const router = useRouter();
const menuNames = ref<Record<string, string>>({});
function hasSelectedOffer(job: any): boolean {
return job.selectedOffer && Object.keys(job.selectedOffer).length > 0;
}
function hasAdjustments(job: any): boolean {
return job.timeMult !== 1.0 || job.materialMult !== 1.0;
}
function getMenuName(job: any): string {
if (!job.problem || !job.problem.menus || !job.problem.menus[0]) {
return "No menu assigned";
}
const menuId = job.problem.menus[0];
return menuNames.value[menuId] || "Loading...";
}
function getSelectedOfferName(job: any): string {
if (!hasSelectedOffer(job)) return "";
return job.selectedOffer.name || "Selected Offer";
}
function viewJob(job: any) {
if (!job.problem || !job.problem.menus || !job.problem.menus[0]) {
router.push(`/job/${job.id}/adjust`);
} else {
router.push(`/job/${job.id}/show/legacy`);
}
}
function startSession() {
router.push("/session/start");
}
// Extract menu loading logic into a separate function
async function loadMenuNames() {
try {
const db = await getDb();
if (db) {
const uniqueMenuIds = new Set<string>();
// Collect all unique menu IDs from the current jobs
sessionStore.jobs.forEach((job) => {
if (job.problem && job.problem.menus && job.problem.menus[0]) {
uniqueMenuIds.add(job.problem.menus[0]);
}
});
// Load menu names for all unique menu IDs
for (const menuId of uniqueMenuIds) {
// Skip if already loaded (avoid redundant API calls)
if (menuNames.value[menuId] && menuNames.value[menuId] !== "Loading...") {
continue;
}
try {
const menuResponse = await db.menus.byMenuId(menuId);
if (menuResponse) {
menuNames.value[menuId] = menuResponse.name;
}
} catch (error) {
console.error(`Error loading menu ${menuId}:`, error);
menuNames.value[menuId] = "Error loading menu";
}
}
}
} catch (error) {
console.error("Error loading menu names:", error);
}
}
// Watch for changes in the session store jobs
watch(
() => sessionStore.jobs,
async (newJobs) => {
if (newJobs && newJobs.length > 0) {
await loadMenuNames();
}
},
{ deep: true, immediate: false }
);
// Also watch the entire store state for any updates
watch(
() => sessionStore.$state,
async () => {
await loadMenuNames();
},
{ deep: true, immediate: false }
);
onMounted(async () => {
try {
// Load the session store first
await sessionStore.load();
// Then load menu names
await loadMenuNames();
} catch (error) {
console.error("Error loading session review:", error);
}
});
</script>
<style scoped>
.session-info {
text-align: center;
margin-bottom: 20px;
padding: 20px;
background: var(--ion-color-light);
border-radius: 8px;
}
.session-info h2 {
margin: 0 0 10px 0;
color: var(--ion-color-primary);
}
.session-info p {
margin: 0;
color: var(--ion-color-medium);
}
.job-tile {
height: 100%;
transition:
transform 0.2s ease-in-out,
box-shadow 0.2s ease-in-out;
cursor: pointer;
}
.job-tile:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.job-tile.has-selection {
border-left: 4px solid var(--ion-color-success);
}
.menu-name {
font-size: 18px;
font-weight: 600;
margin-bottom: 5px;
}
.job-title {
font-size: 14px;
color: var(--ion-color-medium);
font-style: italic;
}
.job-status {
display: flex;
align-items: flex-start;
gap: 15px;
margin-bottom: 15px;
}
.status-text {
flex: 1;
}
.status-label {
font-weight: 600;
margin-bottom: 5px;
}
.offer-details {
margin-top: 8px;
}
.offer-name {
font-size: 14px;
color: var(--ion-color-dark);
margin-bottom: 3px;
}
.offer-price {
font-size: 16px;
font-weight: 700;
color: var(--ion-color-success);
}
.no-selection-text {
font-size: 14px;
color: var(--ion-color-medium);
font-style: italic;
}
.job-adjustments {
display: flex;
gap: 15px;
padding-top: 10px;
border-top: 1px solid var(--ion-color-light);
}
.adjustment-item {
font-size: 12px;
color: var(--ion-color-medium);
background: var(--ion-color-light);
padding: 4px 8px;
border-radius: 4px;
}
.empty-session {
text-align: center;
padding: 60px 20px;
}
.empty-session h3 {
margin: 20px 0 10px 0;
color: var(--ion-color-medium);
}
.empty-session p {
margin-bottom: 30px;
color: var(--ion-color-medium);
}
.job-checklist-issues,
.job-notes {
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid var(--ion-color-light-shade);
}
.section-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 600;
color: var(--ion-color-medium);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.section-label ion-icon {
font-size: 16px;
}
.checklist-list,
.notes-list {
margin: 0;
padding-left: 20px;
font-size: 14px;
color: var(--ion-color-dark);
}
.checklist-list li,
.notes-list li {
margin-bottom: 4px;
line-height: 1.4;
}
.checklist-list li:last-child,
.notes-list li:last-child {
margin-bottom: 0;
}
</style>