Hello from MCP server
<template>
<BaseLayout title="Problem Selection">
<ion-grid :fixed="true">
<ion-row>
<ion-col>
<p>
<ion-button color="tertiary" fill="outline" @click="goToSession"
>Session Home</ion-button
>
</p>
</ion-col>
</ion-row>
<!-- Search and Filter Section -->
<ion-row>
<ion-col>
<ion-card>
<ion-card-header>
<ion-card-title>Search & Filter Problems</ion-card-title>
</ion-card-header>
<ion-card-content>
<ion-searchbar
v-model="searchQuery"
placeholder="Search problems by title, description, or tags..."
@ionInput="onSearchInput"
></ion-searchbar>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
<!-- Tags Section - Only shows tags from filtered problems -->
<ion-row>
<ion-col>
<ion-card>
<ion-card-header>
<ion-card-title>Filter by Tags</ion-card-title>
<ion-card-subtitle>
{{ availableTags.length }} tag{{ availableTags.length === 1 ? '' : 's' }} available
</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<div class="tags-container">
<ion-chip
v-for="tag in availableTags"
:key="tag.id"
:color="isTagSelected(tag.id) ? 'primary' : 'medium'"
@click="toggleTag(tag.id)"
class="tag-chip"
>
<ion-label>
{{ tag.name }} ({{ getTagUsageCount(tag.id) }})
</ion-label>
</ion-chip>
<span v-if="availableTags.length === 0" class="no-tags">
No tags match your search
</span>
</div>
<div v-if="selectedTags.length > 0" class="selected-tags-info">
<ion-text color="primary">
<small>Selected: {{ selectedTags.length }} tag{{ selectedTags.length === 1 ? '' : 's' }}</small>
</ion-text>
<ion-button
size="small"
fill="clear"
@click="clearTags"
>
Clear Tags
</ion-button>
</div>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
<!-- Results Summary -->
<ion-row>
<ion-col>
<div class="results-summary">
<h3>
{{ filteredProblems.length }} Problem{{ filteredProblems.length === 1 ? '' : 's' }} Found
</h3>
<ion-button
v-if="hasActiveFilters"
fill="clear"
@click="clearAllFilters"
>
Clear All Filters
</ion-button>
</div>
</ion-col>
</ion-row>
<div
style="border-top: 1px solid var(--ion-color-medium); margin: 16px 0"
></div>
<!-- Problems List -->
<ion-row>
<ion-col>
<ion-card v-for="problem in filteredProblems" :key="problem.id">
<ion-card-header>
<ion-card-title>{{ problem.name }}</ion-card-title>
</ion-card-header>
<ion-card-content>
<ion-grid>
<ion-row>
<ion-col size="10">
<p class="problem-description">
{{ problem.description || "No description available" }}
</p>
<div class="problem-tags">
<span v-if="getTagsForProblem(problem).length > 0">
<ion-chip
v-for="tagName in getTagsForProblem(problem)"
:key="tagName"
size="small"
:color="isTagNameSelected(tagName) ? 'primary' : 'light'"
@click="toggleTagByName(tagName)"
class="problem-tag"
>
<ion-label>#{{ tagName }}</ion-label>
</ion-chip>
</span>
<span v-else class="no-tags">No tags</span>
</div>
</ion-col>
<ion-col size="2">
<div class="button-group">
<ion-button :data-id="problem.id" @click="viewProblem"
>View</ion-button
>
</div>
</ion-col>
</ion-row>
</ion-grid>
</ion-card-content>
</ion-card>
<!-- No Results Message -->
<ion-card v-if="filteredProblems.length === 0">
<ion-card-content>
<div class="no-results">
<ion-icon :icon="searchOutline" size="large" color="medium"></ion-icon>
<h3>No problems found</h3>
<p>Try adjusting your search or filters</p>
<ion-button
v-if="hasActiveFilters"
fill="outline"
@click="clearAllFilters"
>
Clear All Filters
</ion-button>
</div>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
</ion-grid>
</BaseLayout>
</template>
<script setup lang="ts">
import { onMounted, ref, computed } from "vue";
import { useRouter } from "vue-router";
import BaseLayout from "@/components/BaseLayout.vue";
import type { ProblemsRecord, ProblemTagsRecord } from "@/pocketbase-types.ts";
import { getDb } from "@/dataAccess/getDb";
import { getApi } from "@/dataAccess/getApi";
import { useSessionStore } from "@/stores/session";
import {
IonButton,
IonGrid,
IonRow,
IonCol,
IonCard,
IonCardHeader,
IonCardTitle,
IonCardSubtitle,
IonCardContent,
IonSearchbar,
IonChip,
IonLabel,
IonText,
IonIcon,
} from "@ionic/vue";
import { searchOutline } from "ionicons/icons";
const router = useRouter();
const sessionStore = useSessionStore();
const problems = ref<ProblemsRecord[]>([]);
const problemTags = ref<ProblemTagsRecord[]>([]);
const searchQuery = ref("");
const selectedTags = ref<string[]>([]);
// Helper function to get tag IDs from a problem
const getProblemTagIds = (problem: ProblemsRecord): string[] => {
if (!problem.problemTags) return [];
if (typeof problem.problemTags === 'string') {
try {
return JSON.parse(problem.problemTags);
} catch (e) {
return [];
}
} else if (Array.isArray(problem.problemTags)) {
return problem.problemTags;
}
return [];
};
// Helper function to get tag names for a problem
const getTagsForProblem = (problem: ProblemsRecord): string[] => {
const tagIds = getProblemTagIds(problem);
return tagIds
.map((tagId) => problemTags.value.find((tag) => tag.id === tagId))
.filter((tag) => tag !== undefined)
.map((tag) => tag!.name!)
.filter((name) => name !== undefined);
};
// Computed: Problems filtered by search and selected tags
const filteredProblems = computed(() => {
let filtered = [...problems.value];
// Filter by search query
if (searchQuery.value.trim() !== "") {
const query = searchQuery.value.toLowerCase();
filtered = filtered.filter(problem => {
// Search in title
if (problem.name?.toLowerCase().includes(query)) return true;
// Search in description
if (problem.description?.toLowerCase().includes(query)) return true;
// Search in tags
const tagNames = getTagsForProblem(problem);
if (tagNames.some(tagName => tagName.toLowerCase().includes(query))) return true;
return false;
});
}
// Filter by selected tags (AND logic - problem must have ALL selected tags)
if (selectedTags.value.length > 0) {
filtered = filtered.filter(problem => {
const problemTagIds = getProblemTagIds(problem);
return selectedTags.value.every(tagId => problemTagIds.includes(tagId));
});
}
return filtered;
});
// Computed: Available tags based on filtered problems (CORRECT CROSS-FILTERING)
const availableTags = computed(() => {
// Collect all unique tag IDs from the currently filtered problems
const tagIdsInFilteredProblems = new Set<string>();
filteredProblems.value.forEach(problem => {
const tagIds = getProblemTagIds(problem);
tagIds.forEach(tagId => tagIdsInFilteredProblems.add(tagId));
});
// Return only tags that exist in the filtered problems
return problemTags.value.filter(tag => tagIdsInFilteredProblems.has(tag.id));
});
// Computed: Check if filters are active
const hasActiveFilters = computed(() => {
return searchQuery.value.trim() !== "" || selectedTags.value.length > 0;
});
// Get count of problems using a specific tag (within filtered problems)
const getTagUsageCount = (tagId: string): number => {
return filteredProblems.value.filter(problem => {
const tagIds = getProblemTagIds(problem);
return tagIds.includes(tagId);
}).length;
};
// Check if a tag is selected
const isTagSelected = (tagId: string): boolean => {
return selectedTags.value.includes(tagId);
};
// Check if a tag name is selected
const isTagNameSelected = (tagName: string): boolean => {
const tag = problemTags.value.find(t => t.name === tagName);
return tag ? isTagSelected(tag.id) : false;
};
// Toggle tag selection
const toggleTag = (tagId: string) => {
const index = selectedTags.value.indexOf(tagId);
if (index > -1) {
selectedTags.value.splice(index, 1);
} else {
selectedTags.value.push(tagId);
}
};
// Toggle tag by name
const toggleTagByName = (tagName: string) => {
const tag = problemTags.value.find(t => t.name === tagName);
if (tag) {
toggleTag(tag.id);
}
};
// Clear all selected tags
const clearTags = () => {
selectedTags.value = [];
};
// Clear all filters
const clearAllFilters = () => {
searchQuery.value = "";
selectedTags.value = [];
};
// Handle search input
const onSearchInput = (event: CustomEvent) => {
searchQuery.value = event.detail.value || "";
};
// Navigation functions
const viewProblem = async (e: MouseEvent) => {
const target = e?.target as HTMLElement;
if (target) {
const problemId = target.dataset.id;
// Get the problem data
const db = await getDb();
if (db) {
const problem = await db.problems.byId(problemId);
if (problem) {
console.log('[ProblemSelect] Navigating to confirm-job for problem:', problem.name);
// Navigate to confirm-job with problem ID (don't create job yet)
router.push(`/confirm-job?problemId=${problem.id}`);
}
}
}
};
const goToSession = () => {
router.push("/service-call");
};
// Load data on mount
onMounted(async () => {
const db = await getDb();
if (db) {
problems.value = (await db.selectAll("problems")) || [];
problemTags.value = (await db.selectAll("problemTags")) || [];
}
});
</script>
<style scoped>
.button-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.tag-chip {
cursor: pointer;
transition: all 0.2s ease;
}
.tag-chip:hover {
transform: scale(1.05);
}
.problem-tag {
cursor: pointer;
margin-right: 4px;
margin-bottom: 4px;
}
.problem-description {
color: var(--ion-color-dark);
margin-bottom: 12px;
line-height: 1.4;
}
.problem-tags {
margin-top: 8px;
}
.no-tags {
color: var(--ion-color-medium);
font-style: italic;
font-size: 0.9em;
}
.selected-tags-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--ion-color-light);
}
.results-summary {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px;
}
.results-summary h3 {
margin: 0;
color: var(--ion-color-dark);
}
.no-results {
text-align: center;
padding: 40px 20px;
}
.no-results h3 {
color: var(--ion-color-medium);
margin-top: 16px;
margin-bottom: 8px;
}
.no-results p {
color: var(--ion-color-medium);
margin-bottom: 24px;
}
</style>