Hello from MCP server
<template>
<BaseLayout title="Service Call Review">
<!-- Toolbar at top of screen -->
<Toolbar @help-clicked="openInfoModal" />
<div class="review-container">
<h2 class="review-title">Service Call Review</h2>
<!-- Loading State -->
<div v-if="isLoading" class="loading-section">
<ion-spinner name="crescent"></ion-spinner>
<p class="loading-text">Loading service calls...</p>
</div>
<!-- Service Calls List -->
<div v-else-if="serviceCalls.length > 0" class="section">
<p class="section-message">{{ serviceCalls.length }} service call{{ serviceCalls.length !== 1 ? 's' : '' }} found</p>
<div class="calls-list">
<div
v-for="call in serviceCalls"
:key="call.id"
class="call-item"
@click="navigateToDetails(call.id)"
>
<div class="call-header">
<ion-icon :icon="documentTextOutline" class="call-icon"></ion-icon>
<div class="call-info">
<div class="call-date">{{ formatDate(call.created) }}</div>
<div class="call-meta">
<span class="call-tech">{{ call.expand?.user?.name || 'Unknown Tech' }}</span>
<span v-if="callDetails(call).jobCount > 0" class="call-jobs">
{{ callDetails(call).jobCount }} job{{ callDetails(call).jobCount !== 1 ? 's' : '' }}
</span>
</div>
</div>
<div class="call-amount">
<show-currency v-if="callDetails(call).totalAmount > 0" :currencyIn="callDetails(call).totalAmount" />
<span v-else class="no-amount">—</span>
</div>
</div>
<ion-icon :icon="chevronForwardOutline" class="arrow-icon"></ion-icon>
</div>
</div>
</div>
<!-- Empty State -->
<div v-else class="empty-section">
<ion-icon :icon="documentTextOutline" class="empty-icon"></ion-icon>
<p class="empty-message">No service calls found</p>
<p class="empty-submessage">Service calls will appear here after they are saved</p>
</div>
</div>
<!-- Info Modal -->
<ion-modal :is-open="isInfoModalOpen" @didDismiss="closeInfoModal">
<ion-header>
<ion-toolbar>
<ion-title>Service Calls 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(serviceCalls, null, 2) }}</pre>
</div>
</ion-content>
</ion-modal>
</BaseLayout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import {
IonIcon,
IonSpinner,
IonModal,
IonHeader,
IonToolbar,
IonTitle,
IonButtons,
IonButton,
IonContent,
} from '@ionic/vue';
import { documentTextOutline, chevronForwardOutline } 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 serviceCalls = ref<any[]>([]);
const isLoading = ref(true);
const isInfoModalOpen = ref(false);
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: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const callDetails = (call: any) => {
let details = { jobCount: 0, totalAmount: 0 };
try {
if (call.details) {
const parsed = typeof call.details === 'string' ? JSON.parse(call.details) : call.details;
details.jobCount = parsed.jobs?.length || 0;
details.totalAmount = parsed.totalAmount || 0;
}
} catch (error) {
console.error('[ServiceCallReview] Error parsing call details:', error);
}
return details;
};
const navigateToDetails = (id: string) => {
router.push(`/service-call-review/${id}`);
};
const loadServiceCalls = async () => {
isLoading.value = true;
try {
const { pb } = await getApi();
// Get service calls for the current user's organization
const records = await pb.collection('serviceCalls').getFullList({
sort: '-created',
expand: 'user',
filter: `org = "${pb.authStore.record?.activeOrg}"`,
});
serviceCalls.value = records;
console.log('[ServiceCallReview] Loaded service calls:', records.length);
} catch (error) {
console.error('[ServiceCallReview] Failed to load service calls:', error);
} finally {
isLoading.value = false;
}
};
onMounted(async () => {
await loadServiceCalls();
});
</script>
<style scoped>
:deep(.ion-page) {
animation: none !important;
transform: none !important;
transition: none !important;
}
.review-container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
animation: none !important;
transform: none !important;
transition: none !important;
}
.review-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;
}
.section {
margin-bottom: 40px;
}
.section-message {
text-align: center;
color: var(--ion-color-medium);
font-size: 16px;
font-style: italic;
margin: 0 0 20px 0;
}
.calls-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.call-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
background-color: var(--ion-background-color-step-50);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
transition: background 0.2s;
}
.call-item:hover {
background: rgba(118, 221, 84, 0.1);
}
.call-header {
display: flex;
align-items: center;
gap: 16px;
flex: 1;
}
.call-icon {
font-size: 32px;
color: var(--ion-color-primary);
flex-shrink: 0;
}
.call-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.call-date {
color: var(--ion-text-color);
font-size: 18px;
font-weight: 600;
}
.call-meta {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
}
.call-tech {
color: var(--ion-color-medium);
font-size: 14px;
}
.call-jobs {
color: var(--ion-color-primary);
font-size: 14px;
font-weight: 600;
}
.call-amount {
font-size: 24px;
font-weight: 700;
color: var(--ion-color-success);
margin-right: 8px;
flex-shrink: 0;
}
.no-amount {
color: var(--ion-color-medium);
}
.arrow-icon {
font-size: 24px;
color: var(--ion-color-medium);
flex-shrink: 0;
}
.empty-section {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.empty-icon {
font-size: 80px;
color: var(--ion-color-medium);
opacity: 0.5;
margin-bottom: 24px;
}
.empty-message {
color: var(--ion-text-color);
font-size: 20px;
font-weight: 600;
margin: 0 0 8px 0;
}
.empty-submessage {
color: var(--ion-color-medium);
font-size: 16px;
font-style: italic;
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) {
.review-container {
padding: 16px;
}
.review-title {
font-size: 28px;
}
.call-date {
font-size: 16px;
}
.call-amount {
font-size: 20px;
}
}
</style>