Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
<template>
  <BaseLayout title="Service Call Details">
    <!-- Toolbar at top of screen -->
    <Toolbar @help-clicked="openInfoModal" />

    <div class="details-container">
      <h2 class="details-title">Service Call Details</h2>

      <!-- Loading State -->
      <div v-if="isLoading" class="loading-section">
        <ion-spinner name="crescent"></ion-spinner>
        <p class="loading-text">Loading service call...</p>
      </div>

      <!-- Service Call Details -->
      <div v-else-if="serviceCall" class="content-sections">
        <!-- Header Info -->
        <div class="section">
          <h3 class="section-heading">Call Information</h3>
          <div class="info-grid">
            <div class="info-item">
              <span class="info-label">Date</span>
              <span class="info-value">{{ formatDate(serviceCall.created) }}</span>
            </div>
            <div class="info-item">
              <span class="info-label">Technician</span>
              <span class="info-value">{{ serviceCall.expand?.user?.name || 'Unknown' }}</span>
            </div>
            <div class="info-item">
              <span class="info-label">Payment Method</span>
              <span class="info-value">{{ callData.paymentMethod || 'Not specified' }}</span>
            </div>
            <div v-if="callData.applyDiscount" class="info-item">
              <span class="info-label">Discount Applied</span>
              <span class="info-value">Yes</span>
            </div>
          </div>
        </div>

        <!-- Jobs Section -->
        <div v-if="callData.jobs && callData.jobs.length > 0" class="section">
          <h3 class="section-heading">Jobs ({{ callData.jobs.length }})</h3>
          <div class="jobs-list">
            <div
              v-for="(job, index) in callData.jobs"
              :key="job.id"
              class="job-card"
            >
              <div class="job-header">
                <span class="job-number">{{ index + 1 }}</span>
                <div class="job-title-section">
                  <h4 class="job-title">{{ job.selectedTierTitle || job.problem?.name || job.title || 'Untitled Job' }}</h4>
                  <div class="job-badges">
                    <span v-if="job.selectedTierName" class="job-badge tier-badge">{{ job.selectedTierName }}</span>
                    <span v-if="job.baseHours && job.extraTime" class="job-badge hours-badge">
                      {{ (job.baseHours * job.extraTime).toFixed(1) }}h
                    </span>
                  </div>
                </div>
                <div class="job-price">
                  <show-currency :currencyIn="job.selectedPrice || '0'" />
                </div>
              </div>
              <div v-if="job.notes && job.notes.length > 0" class="job-notes">
                <div class="notes-header">Notes:</div>
                <ul class="notes-list">
                  <li v-for="(note, noteIndex) in job.notes" :key="noteIndex">{{ note }}</li>
                </ul>
              </div>
            </div>
          </div>
        </div>

        <!-- Total Section -->
        <div class="section total-section">
          <h3 class="section-heading">Total</h3>
          <div class="total-display">
            <span class="total-label">Total Amount:</span>
            <span class="total-value">
              <show-currency :currencyIn="callData.totalAmount || 0" />
            </span>
          </div>
        </div>

        <!-- Hours Adjustments Section -->
        <div v-if="jobsWithAdjustments.length > 0" class="section">
          <h3 class="section-heading">Hours Adjustments</h3>
          <div class="adjustments-list">
            <div
              v-for="(job, index) in jobsWithAdjustments"
              :key="job.id"
              class="adjustment-item"
            >
              <div class="adjustment-header">
                <span class="adjustment-number">{{ index + 1 }}</span>
                <span class="adjustment-title">{{ job.selectedTierTitle || job.problem?.name || job.title || 'Untitled Job' }}</span>
              </div>
              <div class="adjustment-details">
                <div class="adjustment-row">
                  <span class="adjustment-label">Base Hours:</span>
                  <span class="adjustment-value">{{ job.baseHours?.toFixed(2) || '0.00' }}h</span>
                </div>
                <div class="adjustment-row">
                  <span class="adjustment-label">Multiplier:</span>
                  <span class="adjustment-value">{{ job.extraTime?.toFixed(2) || '1.00' }}x</span>
                </div>
                <div class="adjustment-row total">
                  <span class="adjustment-label">Adjusted Hours:</span>
                  <span class="adjustment-value highlight">{{ ((job.baseHours || 0) * (job.extraTime || 1)).toFixed(2) }}h</span>
                </div>
              </div>
            </div>
          </div>
        </div>

        <!-- All Notes Section -->
        <div v-if="allNotes.length > 0" class="section">
          <h3 class="section-heading">Notes</h3>
          <div class="all-notes-list">
            <div
              v-for="(noteGroup, index) in allNotes"
              :key="index"
              class="note-group"
            >
              <div class="note-group-header">
                <span class="note-number">{{ index + 1 }}</span>
                <span class="note-job-title">{{ noteGroup.jobTitle }}</span>
              </div>
              <ul class="note-items">
                <li v-for="(note, noteIndex) in noteGroup.notes" :key="noteIndex">{{ note }}</li>
              </ul>
            </div>
          </div>
        </div>

        <!-- Metrics Section -->
        <div v-if="hasMetrics" class="section">
          <h3 class="section-heading">Metrics</h3>
          <div class="metrics-grid">
            <div v-if="callData.menusShownCount !== undefined" class="metric-item">
              <span class="metric-label">Menus Shown</span>
              <span class="metric-value">{{ callData.menusShownCount }}</span>
            </div>
            <div v-if="callData.higherTierChosenCount !== undefined" class="metric-item">
              <span class="metric-label">Higher Tier Chosen</span>
              <span class="metric-value">{{ callData.higherTierChosenCount }}</span>
            </div>
          </div>
        </div>

        <!-- Back Button -->
        <div class="actions">
          <ion-button expand="block" color="medium" @click="goBack">
            Back to Review
          </ion-button>
        </div>
      </div>

      <!-- Error State -->
      <div v-else class="empty-section">
        <ion-icon :icon="alertCircleOutline" class="empty-icon"></ion-icon>
        <p class="empty-message">Service call not found</p>
        <ion-button color="primary" @click="goBack">
          Back to Review
        </ion-button>
      </div>
    </div>

    <!-- Info Modal -->
    <ion-modal :is-open="isInfoModalOpen" @didDismiss="closeInfoModal">
      <ion-header>
        <ion-toolbar>
          <ion-title>Service Call Data</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(serviceCall, null, 2) }}</pre>
        </div>
      </ion-content>
    </ion-modal>
  </BaseLayout>
</template>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import {
  IonIcon,
  IonButton,
  IonSpinner,
  IonModal,
  IonHeader,
  IonToolbar,
  IonTitle,
  IonButtons,
  IonContent,
} from '@ionic/vue';
import { alertCircleOutline } from 'ionicons/icons';
import BaseLayout from '@/components/BaseLayout.vue';
import Toolbar from '@/components/Toolbar.vue';
import ShowCurrency from '@/components/ShowCurrency.vue';
import { getApi } from '@/dataAccess/getApi';

const router = useRouter();
const route = useRoute();
const serviceCall = ref<any>(null);
const isLoading = ref(true);
const isInfoModalOpen = ref(false);

const callData = computed(() => {
  if (!serviceCall.value?.details) return {};
  try {
    return typeof serviceCall.value.details === 'string'
      ? JSON.parse(serviceCall.value.details)
      : serviceCall.value.details;
  } catch (error) {
    console.error('[ServiceCallDetails] Error parsing details:', error);
    return {};
  }
});

const hasMetrics = computed(() => {
  return callData.value.menusShownCount !== undefined ||
         callData.value.higherTierChosenCount !== undefined;
});

const jobsWithAdjustments = computed(() => {
  if (!callData.value.jobs) return [];
  return callData.value.jobs.filter((job: any) =>
    job.baseHours !== undefined && job.extraTime !== undefined
  );
});

const allNotes = computed(() => {
  if (!callData.value.jobs) return [];
  return callData.value.jobs
    .filter((job: any) => job.notes && job.notes.length > 0)
    .map((job: any) => ({
      jobTitle: job.selectedTierTitle || job.problem?.name || job.title || 'Untitled Job',
      notes: job.notes
    }));
});

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

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

const formatDate = (dateString: string) => {
  const date = new Date(dateString);
  return date.toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    hour: '2-digit',
    minute: '2-digit'
  });
};

const goBack = () => {
  router.push('/service-call-review');
};

const loadServiceCall = async () => {
  isLoading.value = true;
  try {
    const { pb } = await getApi();
    const id = route.params.id as string;

    const record = await pb.collection('serviceCalls').getOne(id, {
      expand: 'user',
    });

    serviceCall.value = record;
    console.log('[ServiceCallDetails] Loaded service call:', record);
  } catch (error) {
    console.error('[ServiceCallDetails] Failed to load service call:', error);
    serviceCall.value = null;
  } finally {
    isLoading.value = false;
  }
};

onMounted(async () => {
  await loadServiceCall();
});
</script>

<style scoped>
:deep(.ion-page) {
  animation: none !important;
  transform: none !important;
  transition: none !important;
}

.details-container {
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
  animation: none !important;
  transform: none !important;
  transition: none !important;
}

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

.loading-section {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 60px 20px;
  gap: 16px;
}

.loading-text {
  color: var(--ion-color-medium);
  font-size: 16px;
  font-style: italic;
}

.content-sections {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.section {
  background-color: var(--ion-background-color-step-50);
  border-radius: 12px;
  padding: 24px;
}

.section-heading {
  margin: 0 0 20px 0;
  color: var(--ion-text-color);
  font-size: 24px;
  font-weight: 700;
  text-align: center;
}

.info-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 16px;
}

.info-item {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.info-label {
  color: var(--ion-color-medium);
  font-size: 14px;
  font-weight: 500;
}

.info-value {
  color: var(--ion-text-color);
  font-size: 18px;
  font-weight: 600;
}

.jobs-list {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.job-card {
  background: var(--ion-background-color);
  border-radius: 8px;
  padding: 16px;
  border-left: 4px solid var(--ion-color-primary);
}

.job-header {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  margin-bottom: 12px;
}

.job-number {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  background: var(--ion-color-primary);
  color: var(--ion-color-primary-contrast);
  border-radius: 50%;
  font-size: 16px;
  font-weight: 700;
  flex-shrink: 0;
}

.job-title-section {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

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

.job-badges {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.job-badge {
  display: inline-block;
  padding: 4px 12px;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 600;
}

.tier-badge {
  background-color: var(--ion-color-primary);
  color: var(--ion-color-primary-contrast);
  text-transform: uppercase;
}

.hours-badge {
  background-color: var(--ion-color-medium);
  color: var(--ion-color-medium-contrast);
}

.job-price {
  font-size: 24px;
  font-weight: 700;
  color: var(--ion-color-success);
  white-space: nowrap;
  flex-shrink: 0;
}

.job-notes {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
}

.notes-header {
  color: var(--ion-color-medium);
  font-size: 14px;
  font-weight: 600;
  margin-bottom: 8px;
}

.notes-list {
  margin: 0;
  padding-left: 20px;
  color: var(--ion-text-color);
  font-size: 14px;
  line-height: 1.6;
}

.total-section {
  background-color: var(--ion-background-color);
  border: 3px solid var(--ion-color-success);
}

.total-display {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.total-label {
  font-size: 24px;
  font-weight: 700;
  color: var(--ion-text-color);
  text-transform: uppercase;
  letter-spacing: 1px;
}

.total-value {
  font-size: 32px;
  font-weight: 700;
  color: var(--ion-color-success);
}

/* Adjustments Section */
.adjustments-list {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.adjustment-item {
  background: var(--ion-background-color);
  border-radius: 8px;
  padding: 16px;
  border-left: 4px solid var(--ion-color-warning);
}

.adjustment-header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 12px;
}

.adjustment-number {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  background: var(--ion-color-warning);
  color: var(--ion-color-warning-contrast);
  border-radius: 50%;
  font-size: 14px;
  font-weight: 700;
  flex-shrink: 0;
}

.adjustment-title {
  color: var(--ion-text-color);
  font-size: 16px;
  font-weight: 600;
}

.adjustment-details {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-left: 40px;
}

.adjustment-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 4px 0;
}

.adjustment-row.total {
  border-top: 2px solid rgba(255, 255, 255, 0.1);
  padding-top: 12px;
  margin-top: 4px;
}

.adjustment-label {
  color: var(--ion-color-medium);
  font-size: 14px;
  font-weight: 500;
}

.adjustment-value {
  color: var(--ion-text-color);
  font-size: 16px;
  font-weight: 600;
}

.adjustment-value.highlight {
  color: var(--ion-color-warning);
  font-size: 18px;
  font-weight: 700;
}

/* All Notes Section */
.all-notes-list {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.note-group {
  background: var(--ion-background-color);
  border-radius: 8px;
  padding: 16px;
  border-left: 4px solid var(--ion-color-secondary);
}

.note-group-header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 12px;
}

.note-number {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  background: var(--ion-color-secondary);
  color: var(--ion-color-secondary-contrast);
  border-radius: 50%;
  font-size: 14px;
  font-weight: 700;
  flex-shrink: 0;
}

.note-job-title {
  color: var(--ion-text-color);
  font-size: 16px;
  font-weight: 600;
}

.note-items {
  margin: 0;
  padding-left: 40px;
  color: var(--ion-text-color);
  font-size: 14px;
  line-height: 1.8;
}

.note-items li {
  margin-bottom: 8px;
}

.metrics-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 16px;
}

.metric-item {
  display: flex;
  flex-direction: column;
  gap: 4px;
  text-align: center;
}

.metric-label {
  color: var(--ion-color-medium);
  font-size: 14px;
  font-weight: 500;
}

.metric-value {
  color: var(--ion-color-primary);
  font-size: 28px;
  font-weight: 700;
}

.actions {
  margin-top: 16px;
  max-width: 400px;
  margin-left: auto;
  margin-right: auto;
}

.empty-section {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 60px 20px;
  text-align: center;
  gap: 24px;
}

.empty-icon {
  font-size: 80px;
  color: var(--ion-color-danger);
  opacity: 0.5;
}

.empty-message {
  color: var(--ion-text-color);
  font-size: 20px;
  font-weight: 600;
  margin: 0;
}

/* 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;
}

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

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

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

  .job-header {
    flex-wrap: wrap;
  }

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

  .total-label {
    font-size: 20px;
  }

  .total-value {
    font-size: 24px;
  }
}
</style>