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