Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
<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>