Hello from MCP server
<template>
<BaseLayout title="Problem Details">
<ion-grid :fixed="true">
<ion-row>
<ion-col>
<ion-button fill="clear" @click="goBack">
<ion-icon :icon="arrowBackOutline" slot="start" />
Back to Browse
</ion-button>
</ion-col>
</ion-row>
<ion-row v-if="problem">
<ion-col>
<ion-card>
<ion-card-header>
<ion-card-title>{{ problem.name }}</ion-card-title>
<ion-card-subtitle>Problem ID: {{ problem.refId }}</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<ion-grid>
<ion-row>
<ion-col size="12" size-md="8">
<div class="problem-info">
<div class="description-header">
<h3>Description</h3>
<ion-button
fill="clear"
size="small"
@click="toggleDescriptionEdit"
>
<ion-icon :icon="isEditingDescription ? closeOutline : pencilOutline" slot="start" />
{{ isEditingDescription ? 'Cancel' : 'Edit' }}
</ion-button>
</div>
<div v-if="!isEditingDescription" class="description-display">
<p class="description">
{{ problem.description || 'Description will go here' }}
</p>
</div>
<div v-if="isEditingDescription" class="description-edit">
<ion-textarea
v-model="editingDescription"
placeholder="Enter problem description..."
:rows="4"
fill="outline"
></ion-textarea>
<div class="description-actions">
<ion-button
@click="saveDescription"
color="primary"
size="small"
:disabled="isSavingDescription"
>
<ion-spinner v-if="isSavingDescription" />
<span v-else>Save</span>
</ion-button>
<ion-button
@click="cancelDescriptionEdit"
fill="clear"
size="small"
:disabled="isSavingDescription"
>
Cancel
</ion-button>
</div>
</div>
<p class="problem-id">
<strong>Reference ID:</strong> {{ problem.refId }}
</p>
<p class="org-info">
<strong>Organization:</strong> {{ problem.org }}
</p>
<div class="creation-info">
<p v-if="problem.created">
<strong>Created:</strong> {{ formatDate(problem.created) }}
</p>
<p v-if="problem.updated">
<strong>Last Updated:</strong> {{ formatDate(problem.updated) }}
</p>
</div>
</div>
</ion-col>
<ion-col size="12" size-md="4">
<div class="problem-metadata">
<div class="tags-header">
<h3>Tags</h3>
<ion-button
fill="clear"
size="small"
@click="toggleTagManagement"
>
<ion-icon :icon="isManagingTags ? closeOutline : addOutline" slot="start" />
{{ isManagingTags ? 'Done' : 'Manage' }}
</ion-button>
</div>
<div class="tags-section">
<!-- Current Tags Display -->
<div v-if="problemTags.length > 0" class="tags-container">
<span
v-for="tag in problemTags"
:key="tag.id"
class="tag"
:class="{ removable: isManagingTags }"
@click="isManagingTags && removeTagFromProblem(tag.id)"
>
#{{ tag.name }}
<ion-icon
v-if="isManagingTags"
:icon="closeCircleOutline"
class="remove-icon"
/>
</span>
</div>
<p v-else class="no-tags">No tags assigned</p>
<!-- Tag Management Section -->
<div v-if="isManagingTags" class="tag-management">
<div class="add-existing-tag">
<h4>Add Existing Tags</h4>
<!-- Search input -->
<ion-searchbar
v-model="tagSearchQuery"
placeholder="Search tags..."
@ionInput="onTagSearchInput"
class="tag-search"
></ion-searchbar>
<!-- Tag selection area -->
<div class="tag-selection-container">
<div v-if="filteredAvailableTags.length > 0" class="available-tags-list">
<div
v-for="tag in filteredAvailableTags"
:key="tag.id"
class="tag-selection-item"
>
<ion-checkbox
:checked="selectedTagsToAdd.includes(tag.id)"
@ionChange="toggleTagSelection(tag.id, $event)"
></ion-checkbox>
<span class="tag-name">{{ tag.name }}</span>
</div>
</div>
<div v-else class="no-tags-found">
<p v-if="tagSearchQuery.trim()">No tags found matching "{{ tagSearchQuery }}"</p>
<p v-else>No tags available to add</p>
</div>
</div>
<!-- Selected tags display -->
<div v-if="selectedTagsToAdd.length > 0" class="selected-tags-preview">
<h5>Selected Tags ({{ selectedTagsToAdd.length }}):</h5>
<div class="selected-tags-chips">
<span
v-for="tagId in selectedTagsToAdd"
:key="tagId"
class="selected-tag-chip"
@click="removeFromSelection(tagId)"
>
{{ getTagName(tagId) }}
<ion-icon :icon="closeCircleOutline" class="remove-selected-icon" />
</span>
</div>
</div>
<ion-button
v-if="selectedTagsToAdd.length > 0"
@click="addSelectedTags"
size="small"
:disabled="isAddingTags"
class="add-tags-button"
>
<ion-spinner v-if="isAddingTags" />
<span v-else>Add {{ selectedTagsToAdd.length }} Tag{{ selectedTagsToAdd.length > 1 ? 's' : '' }}</span>
</ion-button>
</div>
<div class="create-new-tag">
<h4>Create New Tag</h4>
<div class="new-tag-form">
<ion-input
v-model="newTagName"
placeholder="Enter tag name..."
@keyup.enter="createNewTag"
></ion-input>
<ion-button
@click="createNewTag"
:disabled="!newTagName.trim() || isCreatingTag"
size="small"
>
<ion-spinner v-if="isCreatingTag" />
<span v-else>Create</span>
</ion-button>
</div>
</div>
</div>
</div>
<h3>Associated Menus</h3>
<div class="menus-section">
<div v-if="associatedMenus.length > 0">
<ion-chip
v-for="menu in associatedMenus"
:key="menu.id"
color="secondary"
outline
>
<ion-label>{{ menu.name || 'Unnamed Menu' }}</ion-label>
</ion-chip>
</div>
<p v-else class="no-menus">No associated menus</p>
</div>
</div>
</ion-col>
</ion-row>
</ion-grid>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
<!-- Loading State -->
<ion-row v-if="!problem && !loading">
<ion-col>
<ion-card>
<ion-card-content>
<p class="error-message">Problem not found.</p>
<ion-button @click="goBack" fill="clear">
<ion-icon :icon="arrowBackOutline" slot="start" />
Back to Browse
</ion-button>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
<!-- Loading Spinner -->
<ion-row v-if="loading">
<ion-col class="ion-text-center">
<ion-spinner></ion-spinner>
<p>Loading problem details...</p>
</ion-col>
</ion-row>
</ion-grid>
</BaseLayout>
</template>
<script setup lang="ts">
import { onMounted, ref, computed } from "vue";
import { useRouter, useRoute } from "vue-router";
import BaseLayout from "@/components/BaseLayout.vue";
import type { ProblemsRecord, ProblemTagsRecord, MenusRecord } from "@/pocketbase-types.ts";
import { getDb } from "@/dataAccess/getDb";
import { getApi } from "@/dataAccess/getApi";
import { ChangesetProcessor, type Change } from "@/dataAccess/changeset-processor";
import {
IonButton,
IonGrid,
IonRow,
IonCol,
IonCard,
IonCardHeader,
IonCardTitle,
IonCardSubtitle,
IonCardContent,
IonIcon,
IonChip,
IonLabel,
IonSpinner,
IonSelect,
IonSelectOption,
IonInput,
IonTextarea,
IonSearchbar,
IonCheckbox,
alertController,
toastController,
} from "@ionic/vue";
import {
arrowBackOutline,
addOutline,
closeOutline,
closeCircleOutline,
pencilOutline,
} from "ionicons/icons";
const router = useRouter();
const route = useRoute();
const problem = ref<ProblemsRecord | null>(null);
const problemTags = ref<ProblemTagsRecord[]>([]);
const allTags = ref<ProblemTagsRecord[]>([]);
const associatedMenus = ref<MenusRecord[]>([]);
const loading = ref(true);
// Tag management states
const isManagingTags = ref(false);
const selectedTagsToAdd = ref<string[]>([]);
const newTagName = ref("");
const isCreatingTag = ref(false);
const isAddingTags = ref(false);
const tagSearchQuery = ref("");
// Description editing states
const isEditingDescription = ref(false);
const editingDescription = ref("");
const isSavingDescription = ref(false);
// Computed properties
const availableTagsToAdd = computed(() => {
const currentTagIds = problemTags.value.map(tag => tag.id);
return allTags.value
.filter(tag => !currentTagIds.includes(tag.id))
.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
});
const filteredAvailableTags = computed(() => {
const searchTerm = tagSearchQuery.value.toLowerCase().trim();
if (!searchTerm) {
return availableTagsToAdd.value;
}
return availableTagsToAdd.value.filter(tag =>
tag.name?.toLowerCase().includes(searchTerm)
);
});
// Helper functions
const formatDate = (dateString: string): string => {
try {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch {
return dateString;
}
};
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 [];
};
const getMenuIds = (problem: ProblemsRecord): string[] => {
if (!problem.menus) return [];
if (typeof problem.menus === 'string') {
try {
return JSON.parse(problem.menus);
} catch (e) {
return [];
}
} else if (Array.isArray(problem.menus)) {
return problem.menus;
}
return [];
};
// Helper function to publish changes both to API and apply locally
const publishAndApplyChanges = async (changes: Change[], orgId: string) => {
const { pb } = await getApi();
const db = await getDb();
if (!db) {
throw new Error('Database not available');
}
console.log('Publishing changes for orgId:', orgId);
console.log('Publishing changes:', changes);
// For book lookup, always use the user's activeOrg since clientApp is a special case
// that maps to the user's actual organization for changeset purposes
const activeOrgId = pb.authStore.record?.expand?.activeOrg?.id || pb.authStore.record?.activeOrg;
if (!activeOrgId) {
throw new Error('No active organization found for user');
}
const books = await pb.collection("books").getFullList({
filter: `org = '${activeOrgId}'`,
sort: "created"
});
if (books.length === 0) {
throw new Error(`No books found for active organization: ${activeOrgId}`);
}
const bookId = books[0].id;
console.log('Using activeOrg book:', bookId, 'for changeset, but processing in orgId:', orgId);
// Apply changes locally first for immediate UI feedback
// We need to access the SQLite connection directly
const { getSQLite } = await import("@/dataAccess/getSQLite");
const sql = await getSQLite();
const processor = new ChangesetProcessor(sql.dbConn);
console.log('Applying changes locally with orgId:', orgId);
// Pass false for useTransaction to avoid nested transaction conflict
await processor.processChangeset(changes, orgId, false);
await sql.saveDb();
// Then publish to API for server sync
console.log('Publishing to API...');
await pb.collection("pricebookChanges").create({
org: activeOrgId, // Use activeOrg for API changeset
book: bookId,
changeset: changes,
});
console.log('Changes applied successfully');
};
// Description editing functions
const toggleDescriptionEdit = () => {
if (isEditingDescription.value) {
cancelDescriptionEdit();
} else {
isEditingDescription.value = true;
editingDescription.value = problem.value?.description || "";
}
};
const saveDescription = async () => {
if (!problem.value) return;
isSavingDescription.value = true;
try {
const changes: Change[] = [
{
collection: "problems",
operation: "update",
data: {
refId: problem.value.refId,
description: editingDescription.value.trim(),
},
},
];
console.log('Saving description:', editingDescription.value.trim());
// Publish changes to API and apply locally using problem's org
await publishAndApplyChanges(changes, problem.value.org || 'clientApp');
// Update local problem data
problem.value.description = editingDescription.value.trim();
// Exit edit mode
isEditingDescription.value = false;
const toast = await toastController.create({
message: 'Description saved successfully',
duration: 2000,
color: 'success'
});
await toast.present();
} catch (error) {
console.error("Error saving description:", error);
const toast = await toastController.create({
message: 'Failed to save description',
duration: 3000,
color: 'danger'
});
await toast.present();
} finally {
isSavingDescription.value = false;
}
};
const cancelDescriptionEdit = () => {
isEditingDescription.value = false;
editingDescription.value = "";
};
// Tag management functions
const toggleTagManagement = () => {
isManagingTags.value = !isManagingTags.value;
selectedTagsToAdd.value = [];
newTagName.value = "";
tagSearchQuery.value = "";
};
const generateTagRefId = (name: string): string => {
return name.toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/_{2,}/g, '_').replace(/^_|_$/g, '');
};
const createNewTag = async () => {
const tagName = newTagName.value.trim().toLowerCase();
if (!tagName || !problem.value) return;
console.log('Creating new tag:', tagName);
// Check if tag name already exists
if (allTags.value.some(tag => tag.name?.toLowerCase() === tagName.toLowerCase())) {
const toast = await toastController.create({
message: 'A tag with this name already exists',
duration: 3000,
color: 'warning'
});
await toast.present();
return;
}
isCreatingTag.value = true;
try {
const refId = generateTagRefId(tagName);
console.log('Generated refId:', refId);
const changes: Change[] = [
{
collection: "problemTags",
operation: "create",
data: {
name: tagName,
refId: refId,
},
},
];
// Publish changes to API and apply locally using problem's org
await publishAndApplyChanges(changes, problem.value.org || 'clientApp');
// Refresh local data to see the new tag
await loadAllTags();
console.log('All tags after creation:', allTags.value);
// Find the newly created tag and add it to the problem
const newTag = allTags.value.find(tag => tag.refId === refId);
console.log('Found new tag:', newTag);
if (newTag) {
await addTagToProblem(newTag.id);
} else {
console.error('Could not find newly created tag with refId:', refId);
}
newTagName.value = "";
const toast = await toastController.create({
message: `Tag "${tagName}" created successfully`,
duration: 2000,
color: 'success'
});
await toast.present();
} catch (error) {
console.error("Error creating tag:", error);
const toast = await toastController.create({
message: 'Failed to create tag',
duration: 3000,
color: 'danger'
});
await toast.present();
} finally {
isCreatingTag.value = false;
}
};
const onTagSearchInput = (event: CustomEvent) => {
tagSearchQuery.value = event.detail.value;
};
const toggleTagSelection = (tagId: string, event: CustomEvent) => {
const isChecked = event.detail.checked;
if (isChecked) {
if (!selectedTagsToAdd.value.includes(tagId)) {
selectedTagsToAdd.value.push(tagId);
}
} else {
selectedTagsToAdd.value = selectedTagsToAdd.value.filter(id => id !== tagId);
}
console.log('Selected tags to add:', selectedTagsToAdd.value);
};
const removeFromSelection = (tagId: string) => {
selectedTagsToAdd.value = selectedTagsToAdd.value.filter(id => id !== tagId);
};
const getTagName = (tagId: string): string => {
const tag = allTags.value.find(t => t.id === tagId);
return tag?.name || 'Unknown Tag';
};
const addSelectedTags = async () => {
if (selectedTagsToAdd.value.length === 0) return;
isAddingTags.value = true;
try {
console.log('Adding multiple tags:', selectedTagsToAdd.value);
// Store count before clearing
const addedCount = selectedTagsToAdd.value.length;
// Use the more efficient batch add function
await addMultipleTagsToProblem(selectedTagsToAdd.value);
// Clear selection after successful addition
selectedTagsToAdd.value = [];
const toast = await toastController.create({
message: `Successfully added ${addedCount} tag${addedCount > 1 ? 's' : ''} to problem`,
duration: 2000,
color: 'success'
});
await toast.present();
} catch (error) {
console.error('Failed to add tags:', error);
const toast = await toastController.create({
message: 'Failed to add some tags to problem',
duration: 3000,
color: 'danger'
});
await toast.present();
} finally {
isAddingTags.value = false;
}
};
const addMultipleTagsToProblem = async (tagIds: string[]) => {
if (!problem.value || tagIds.length === 0) return;
try {
console.log('Adding multiple tags to problem:', tagIds, 'to problem:', problem.value.refId);
const currentTagIds = getProblemTagIds(problem.value);
const newTagIds = [...currentTagIds, ...tagIds];
console.log('Current tag IDs:', currentTagIds);
console.log('New tag IDs:', newTagIds);
// Get the refIds for all tags
const tagRefIds = allTags.value
.filter(tag => newTagIds.includes(tag.id))
.map(tag => tag.refId)
.filter(refId => refId !== undefined);
console.log('Tag refIds for update:', tagRefIds);
const changes: Change[] = [
{
collection: "problems",
operation: "update",
data: {
refId: problem.value.refId,
refs: [
{
collection: "problemTags",
refIds: tagRefIds,
},
],
},
},
];
console.log('About to publish batch changes:', JSON.stringify(changes, null, 2));
// Publish changes to API and apply locally using problem's org
await publishAndApplyChanges(changes, problem.value.org || 'clientApp');
// Force reload the problem from database to get updated tags
const db = await getDb();
if (db) {
const updatedProblems = await db.selectAll("problems") || [];
const updatedProblem = updatedProblems.find((p: ProblemsRecord) => p.id === problem.value!.id);
if (updatedProblem) {
problem.value = updatedProblem;
console.log('Updated problem data:', updatedProblem);
}
}
// Refresh all tags and problem tags display
await loadAllTags();
await loadProblemTags();
} catch (error) {
console.error("Error adding multiple tags to problem:", error);
throw error; // Re-throw to let caller handle it
}
};
const addTagToProblem = async (tagId: string) => {
if (!problem.value) return;
try {
console.log('Adding tag to problem:', tagId, 'to problem:', problem.value.refId);
console.log('Problem object:', problem.value);
const currentTagIds = getProblemTagIds(problem.value);
const newTagIds = [...currentTagIds, tagId];
console.log('Current tag IDs:', currentTagIds);
console.log('New tag IDs:', newTagIds);
// Get the refIds for the tags
const tagRefIds = allTags.value
.filter(tag => newTagIds.includes(tag.id))
.map(tag => tag.refId)
.filter(refId => refId !== undefined);
console.log('Tag refIds for update:', tagRefIds);
const changes: Change[] = [
{
collection: "problems",
operation: "update",
data: {
refId: problem.value.refId,
refs: [
{
collection: "problemTags",
refIds: tagRefIds,
},
],
},
},
];
console.log('About to publish changes:', JSON.stringify(changes, null, 2));
// Let's also check if we can find this problem in the database first
const db = await getDb();
if (db) {
const problems = await db.selectAll("problems") || [];
const foundProblem = problems.find((p: ProblemsRecord) => p.refId === problem.value!.refId);
console.log('Found problem in database:', foundProblem);
if (!foundProblem) {
console.error('Problem not found with refId:', problem.value.refId);
console.log('Available problems:', problems.map((p: any) => ({ id: p.id, refId: p.refId, name: p.name })));
}
}
// Publish changes to API and apply locally using problem's org
await publishAndApplyChanges(changes, problem.value.org || 'clientApp');
// Force reload the problem from database to get updated tags
if (db) {
const updatedProblems = await db.selectAll("problems") || [];
const updatedProblem = updatedProblems.find((p: ProblemsRecord) => p.id === problem.value!.id);
if (updatedProblem) {
problem.value = updatedProblem;
console.log('Updated problem data:', updatedProblem);
}
}
// Refresh all tags and problem tags display
await loadAllTags();
await loadProblemTags();
const addedTag = allTags.value.find(tag => tag.id === tagId);
const toast = await toastController.create({
message: `Tag "${addedTag?.name}" added to problem`,
duration: 2000,
color: 'success'
});
await toast.present();
} catch (error) {
console.error("Error adding tag to problem:", error);
const toast = await toastController.create({
message: 'Failed to add tag to problem',
duration: 3000,
color: 'danger'
});
await toast.present();
}
};
const removeTagFromProblem = async (tagId: string) => {
if (!problem.value) return;
const tagToRemove = allTags.value.find(tag => tag.id === tagId);
const alert = await alertController.create({
header: 'Remove Tag',
message: `Are you sure you want to remove the tag "${tagToRemove?.name}" from this problem?`,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Remove',
role: 'destructive',
handler: async () => {
await performRemoveTag(tagId);
},
},
],
});
await alert.present();
};
const performRemoveTag = async (tagId: string) => {
if (!problem.value) return;
try {
const currentTagIds = getProblemTagIds(problem.value);
const newTagIds = currentTagIds.filter(id => id !== tagId);
const changes: Change[] = [
{
collection: "problems",
operation: "update",
data: {
refId: problem.value.refId,
refs: [
{
collection: "problemTags",
refIds: allTags.value
.filter(tag => newTagIds.includes(tag.id))
.map(tag => tag.refId)
.filter(refId => refId !== undefined),
},
],
},
},
];
// Publish changes to API
await publishAndApplyChanges(changes, problem.value.org || '');
// Update local problem data
problem.value.problemTags = newTagIds;
// Refresh problem tags display
await loadProblemTags();
const removedTag = allTags.value.find(tag => tag.id === tagId);
const toast = await toastController.create({
message: `Tag "${removedTag?.name}" removed from problem`,
duration: 2000,
color: 'success'
});
await toast.present();
} catch (error) {
console.error("Error removing tag from problem:", error);
const toast = await toastController.create({
message: 'Failed to remove tag from problem',
duration: 3000,
color: 'danger'
});
await toast.present();
}
};
// Event handlers
const goBack = () => {
router.push('/browse/problems');
};
// Helper functions for data loading
const loadAllTags = async () => {
const db = await getDb();
if (db) {
allTags.value = (await db.selectAll("problemTags")) || [];
}
};
const loadProblemTags = async () => {
if (!problem.value) return;
const problemTagIds = getProblemTagIds(problem.value);
problemTags.value = allTags.value.filter((tag: ProblemTagsRecord) =>
problemTagIds.includes(tag.id)
);
};
// Lifecycle
onMounted(async () => {
const problemId = route.params.id as string;
if (!problemId) {
loading.value = false;
return;
}
try {
const db = await getDb();
if (!db) {
loading.value = false;
return;
}
// Get the specific problem
const problems = await db.selectAll("problems") || [];
const foundProblem = problems.find((p: ProblemsRecord) => p.id === problemId);
if (foundProblem) {
problem.value = foundProblem;
// Load all tags
await loadAllTags();
// Load problem-specific tags
await loadProblemTags();
// Get associated menus
const allMenus = await db.selectAll("menus") || [];
const menuIds = getMenuIds(foundProblem);
associatedMenus.value = allMenus.filter((menu: MenusRecord) =>
menuIds.includes(menu.id)
);
}
} catch (error) {
console.error("Error loading problem details:", error);
} finally {
loading.value = false;
}
});
</script>
<style scoped>
.problem-info {
margin-bottom: 20px;
}
.description {
font-size: 1.1em;
line-height: 1.5;
margin-bottom: 16px;
color: var(--ion-color-dark);
}
.problem-id,
.org-info {
margin-bottom: 8px;
color: var(--ion-color-medium);
}
.creation-info p {
margin-bottom: 4px;
font-size: 0.9em;
color: var(--ion-color-medium);
}
.problem-metadata h3 {
margin-top: 0;
margin-bottom: 12px;
color: var(--ion-color-primary);
font-size: 1.1em;
}
.tags-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.tags-header h3 {
margin: 0;
}
.tags-section,
.menus-section {
margin-bottom: 24px;
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag {
display: inline-block;
background-color: var(--ion-color-primary);
color: var(--ion-color-primary-contrast);
padding: 4px 12px;
border-radius: 16px;
font-size: 0.85em;
position: relative;
transition: all 0.2s ease;
}
.tag.removable {
cursor: pointer;
padding-right: 28px;
}
.tag.removable:hover {
background-color: var(--ion-color-danger);
transform: scale(1.05);
}
.tag .remove-icon {
position: absolute;
right: 6px;
top: 50%;
transform: translateY(-50%);
font-size: 0.9em;
}
.no-tags,
.no-menus {
color: var(--ion-color-medium);
font-style: italic;
margin: 0;
}
.additional-info {
text-align: left;
}
.additional-info p {
margin-bottom: 20px;
color: var(--ion-color-medium);
line-height: 1.5;
}
.navigation-buttons {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.error-message {
color: var(--ion-color-danger);
font-weight: 500;
text-align: center;
margin-bottom: 20px;
}
.tag-management {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--ion-color-light);
}
.add-existing-tag,
.create-new-tag {
margin-bottom: 16px;
}
.add-existing-tag h4,
.create-new-tag h4 {
margin: 0 0 8px 0;
font-size: 0.95em;
color: var(--ion-color-dark);
font-weight: 500;
}
.new-tag-form {
display: flex;
gap: 8px;
align-items: center;
}
.new-tag-form ion-input {
flex: 1;
}
.add-tags-button {
margin-top: 12px;
width: 100%;
}
.tag-search {
margin-bottom: 16px;
}
.tag-selection-container {
max-height: 200px;
overflow-y: auto;
border: 1px solid var(--ion-color-light);
border-radius: 8px;
margin-bottom: 16px;
}
.available-tags-list {
padding: 8px;
}
.tag-selection-item {
display: flex;
align-items: center;
padding: 8px;
border-bottom: 1px solid var(--ion-color-light-shade);
cursor: pointer;
transition: background-color 0.2s ease;
}
.tag-selection-item:last-child {
border-bottom: none;
}
.tag-selection-item:hover {
background-color: var(--ion-color-light);
}
.tag-name {
margin-left: 12px;
font-size: 0.9em;
color: var(--ion-color-dark);
}
.no-tags-found {
padding: 16px;
text-align: center;
color: var(--ion-color-medium);
font-style: italic;
}
.selected-tags-preview {
margin-bottom: 16px;
}
.selected-tags-preview h5 {
margin: 0 0 8px 0;
font-size: 0.9em;
color: var(--ion-color-dark);
font-weight: 500;
}
.selected-tags-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.selected-tag-chip {
display: inline-flex;
align-items: center;
background-color: var(--ion-color-primary);
color: var(--ion-color-primary-contrast);
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8em;
cursor: pointer;
transition: all 0.2s ease;
}
.selected-tag-chip:hover {
background-color: var(--ion-color-danger);
transform: scale(1.05);
}
.remove-selected-icon {
margin-left: 4px;
font-size: 0.9em;
}
.description-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.description-header h3 {
margin: 0;
}
.description-display {
margin-bottom: 16px;
}
.description-edit {
margin-bottom: 16px;
}
.description-actions {
display: flex;
gap: 8px;
margin-top: 12px;
justify-content: flex-start;
}
@media (max-width: 768px) {
.navigation-buttons {
flex-direction: column;
}
.navigation-buttons ion-button {
width: 100%;
}
}
</style>