Hello from MCP server
<template>
<BaseLayout title="Technician">
<div class="dashboard-container">
<div class="header-section">
<div class="title-container">
<div class="title-icons-left">
<div class="star-icon-wrapper">
<ion-icon :icon="star" class="star-icon-shadow" color="primary"></ion-icon>
<ion-icon :icon="star" @click="openBookmarksModal" class="clickable-icon star-icon-front" color="medium"></ion-icon>
</div>
</div>
<h1 class="page-title">Dashboard</h1>
<div class="title-icons-right">
<ion-icon :icon="colorPaletteOutline" @click="goToThemeSettings" class="clickable-icon" color="medium"></ion-icon>
<ion-icon :icon="helpCircleOutline" @click="openInfoModal" class="clickable-icon" color="medium"></ion-icon>
</div>
</div>
</div>
<div class="content-section">
<!-- Progress bars -->
<div class="progress-grid">
<div class="progress-column">
<div class="progress-number">{{ menusShownCount }}</div>
<div class="progress-wrapper">
<div class="progress-gradient"></div>
<div class="progress-cover" :style="{ width: ((1 - menusShownProgress) * 100) + '%' }"></div>
</div>
<div class="progress-label">Menus Shown</div>
</div>
<div class="progress-column">
<div class="progress-number">{{ higherTierChosenCount }}</div>
<div class="progress-wrapper">
<div class="progress-gradient"></div>
<div class="progress-cover" :style="{ width: ((1 - riseProgress) * 100) + '%' }"></div>
</div>
<div class="progress-label">Rise</div>
</div>
<div class="progress-column">
<div class="progress-number">{{ closeRatePercentage }}</div>
<div class="progress-wrapper">
<div class="progress-gradient"></div>
<div class="progress-cover" :style="{ width: ((1 - closeRateProgress) * 100) + '%' }"></div>
</div>
<div class="progress-label">Close Rate</div>
</div>
</div>
<div class="clocks-grid">
<Clock label="Current Time" />
<Clock label="Estimated Time" :time="estimatedTime" />
<Clock label="Elapsed Time" :time="elapsedTime" />
</div>
<div class="divider"></div>
<!-- Jobs Section -->
<div class="jobs-section">
<DashboardJob
v-for="job in displayJobs"
:key="job.id"
:title="job.title"
:hours="job.hours"
:blink-fix-hours="job.blinkFixHours"
:has-confirmed-offer="job.hasConfirmedOffer"
:job-data="job.jobData"
@delete="() => handleDeleteJob(job.id)"
@details="() => handleJobDetails(job.id)"
@fix-hours="() => handleFixHours(job.id)"
@edit="() => handleEdit(job.id)"
/>
</div>
<div class="divider-compact"></div>
<!-- Search Component -->
<div class="dashboard-search-section">
<h2 class="dashboard-search-title">What are we doing today?</h2>
<div class="dashboard-search-tags-container">
<!-- Search term chips (without #) -->
<ion-chip
v-for="(term, index) in appliedSearchTerms"
:key="'search-term-' + index"
color="secondary"
class="dashboard-search-tag-chip dashboard-search-term-chip"
@click="removeSearchTerm(index)"
>
{{ term }}
</ion-chip>
<!-- Tag chips (with #) -->
<ion-chip
v-for="tagItem in topTagsForDashboard"
:key="`dashboard-search-${tagItem.tag?.id}`"
@click="selectTagAndNavigate(tagItem)"
:color="getChipColor(tagItem)"
class="dashboard-search-tag-chip"
>
{{ '#' + tagItem.tag?.name }}
</ion-chip>
</div>
<div class="dashboard-search-bar">
<div class="dashboard-search-input-container">
<ion-input
v-model="searchQuery"
placeholder="Search jobs..."
fill="outline"
class="dashboard-search-input"
@keydown="handleSearchKeydown"
@input="handleSearchInput"
@ionFocus="handleSearchFocus"
@ionBlur="handleSearchBlur"
></ion-input>
<!-- Suggestions dropdown -->
<div v-if="showSuggestions && searchSuggestions.length > 0" class="suggestions-dropdown">
<ion-list>
<ion-item
v-for="(suggestion, index) in searchSuggestions"
:key="`${suggestion.type}-${suggestion.id}`"
button
@click="selectSuggestion(suggestion)"
:class="{ 'suggestion-selected': index === selectedSuggestionIndex }"
class="suggestion-item"
>
<ion-icon
:icon="suggestion.type === 'tag' ? pricetag : briefcase"
slot="start"
:class="suggestion.type === 'tag' ? 'tag-icon' : 'job-icon'"
></ion-icon>
<ion-label>{{ suggestion.name }}</ion-label>
</ion-item>
</ion-list>
</div>
</div>
<ion-button fill="solid" color="secondary" @click="applySearchAndNavigate">
Search
</ion-button>
<ion-button fill="outline" color="tertiary" @click="clearSearch">
Clear
</ion-button>
<div v-if="canUndo" class="dashboard-search-undo-wrapper">
<ion-button
@click="undoLastAction"
fill="solid"
color="warning"
class="dashboard-search-undo-button dashboard-search-undo-shadow"
size="default"
>
<ion-icon :icon="arrowUndo" slot="icon-only"></ion-icon>
</ion-button>
<ion-button
@click="undoLastAction"
fill="solid"
color="medium"
class="dashboard-search-undo-button dashboard-search-undo-front"
size="default"
>
<ion-icon :icon="arrowUndo" slot="icon-only" color="tertiary"></ion-icon>
</ion-button>
</div>
</div>
</div>
</div>
<!-- Action Bar -->
<div
class="action-bar"
:class="{ 'action-bar-disabled': sessionStore.jobs.length === 0 }"
@click="handleShowMenus"
>
<div class="action-bar-content">
<h2 class="action-bar-text">Show Menus</h2>
</div>
</div>
</div>
<!-- Info Modal -->
<ion-modal :is-open="isInfoModalOpen" @didDismiss="closeInfoModal">
<ion-header>
<ion-toolbar>
<ion-title>Dashboard Metrics</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">
<h2>Menus Shown</h2>
<p>Shows the total number of menus presented to customers today.</p>
<p><strong>Progress calculation:</strong> Your company goal is to show an average of 3 menus per service call (configurable in company settings).</p>
<p><strong>Formula:</strong> (Total Menus Shown) ÷ (Number of Service Calls × 3)</p>
<p><strong>Example:</strong> If you've shown 3 menus on 1 service call: 3 ÷ (1 × 3) = 100%</p>
<h2>Rise</h2>
<p>Shows the percentage of menu options where customers chose a higher tier option (above the lowest tier).</p>
<p><strong>Formula:</strong> (Higher Tier Options Chosen) ÷ (Total Menus Shown)</p>
<p><strong>Example:</strong> If 3 menus were shown and 2 higher tier options were chosen and 1 lowest tier option: 2 ÷ 3 = 66%</p>
<h2>Close Rate</h2>
<p>Shows the percentage of service calls where at least one menu option was presented to the customer.</p>
<p><strong>Formula:</strong> (Service Calls with Menus Shown) ÷ (Total Service Calls)</p>
<p><strong>Example:</strong> If you've completed 5 service calls and showed menus on 4 of them: 4 ÷ 5 = 80%</p>
</div>
</ion-content>
</ion-modal>
<!-- Bookmarks Modal -->
<ion-modal :is-open="isBookmarksModalOpen" @didDismiss="closeBookmarksModal">
<ion-header>
<ion-toolbar>
<ion-title>Bookmarks</ion-title>
<ion-buttons slot="end">
<ion-button @click="closeBookmarksModal">Close</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="bookmarks-content">
<div class="bookmarks-list">
<div
v-for="job in bookmarkedJobs"
:key="job.id"
@click="selectBookmarkedJob(job)"
class="bookmark-item"
>
<ion-icon :icon="star" color="warning" class="bookmark-star"></ion-icon>
<div class="bookmark-details">
<h3 class="bookmark-title">{{ job.name }}</h3>
<p class="bookmark-description">{{ job.description }}</p>
</div>
</div>
</div>
</div>
</ion-content>
</ion-modal>
<!-- Delete Confirmation Alert -->
<ion-alert
:is-open="isDeleteAlertOpen"
header="Delete Job"
message="Are you sure you want to delete this job? This action cannot be undone."
:buttons="[
{
text: 'Cancel',
role: 'cancel',
handler: cancelDelete
},
{
text: 'Delete',
role: 'destructive',
handler: confirmDelete
}
]"
@didDismiss="cancelDelete"
></ion-alert>
</BaseLayout>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
import BaseLayout from "@/components/BaseLayout.vue";
import Clock from "@/components/Clock.vue";
import DashboardJob from "@/components/DashboardJob.vue";
import type { ProblemsRecord, ProblemTagsRecord } from "@/pocketbase-types";
import { IonProgressBar, IonModal, IonHeader, IonToolbar, IonTitle, IonButtons, IonButton, IonContent, IonIcon, IonInput, IonChip, IonList, IonItem, IonLabel, IonAlert } from "@ionic/vue";
import { helpCircleOutline, arrowUndo, pricetag, briefcase, star, colorPaletteOutline } from "ionicons/icons";
import { useRouter, useRoute } from "vue-router";
import { useSessionStore } from "@/stores/session";
const router = useRouter();
const route = useRoute();
const sessionStore = useSessionStore();
// Clear search state whenever we navigate to dashboard (including back button)
watch(() => route.path, async (newPath) => {
if (newPath === '/dashboard') {
await sessionStore.clearSearch();
}
}, { immediate: true });
// Search-related state
const searchQuery = ref("");
const showSuggestions = ref(false);
const selectedSuggestionIndex = ref(-1);
const isInfoModalOpen = ref(false);
const isBookmarksModalOpen = ref(false);
const isDeleteAlertOpen = ref(false);
const jobToDelete = ref<string | null>(null);
const openInfoModal = () => {
isInfoModalOpen.value = true;
};
const closeInfoModal = () => {
isInfoModalOpen.value = false;
};
const openBookmarksModal = () => {
isBookmarksModalOpen.value = true;
};
const closeBookmarksModal = () => {
isBookmarksModalOpen.value = false;
};
const handleDeleteJob = (jobId: string) => {
jobToDelete.value = jobId;
isDeleteAlertOpen.value = true;
};
const confirmDelete = async () => {
if (jobToDelete.value) {
await sessionStore.deleteJob(jobToDelete.value);
jobToDelete.value = null;
}
isDeleteAlertOpen.value = false;
};
const cancelDelete = () => {
jobToDelete.value = null;
isDeleteAlertOpen.value = false;
};
const handleJobDetails = (jobId: string) => {
// Find the job in the session to get its problem ID
const job = sessionStore.jobs.find(j => j.id === jobId);
if (job && job.problem?.id) {
router.push(`/view-job/${job.problem.id}`);
}
};
const handleFixHours = (jobId: string) => {
router.push(`/confirm-job/${jobId}`);
};
const handleEdit = (jobId: string) => {
router.push(`/editor/job/${jobId}`);
};
const handleShowMenus = () => {
if (sessionStore.jobs.length > 0) {
router.push("/cart/");
}
};
const goToSearch = () => {
router.push("/search");
};
const goToThemeSettings = () => {
router.push("/theme-settings");
};
// Search-related computed properties
const problems = computed(() => sessionStore.problems);
const problemTags = computed(() => sessionStore.problemTags);
const selectedTags = computed(() => sessionStore.selectedTags);
const appliedSearchQuery = computed(() => sessionStore.appliedSearchQuery);
const selectionHistory = computed(() => sessionStore.selectionHistory);
// Get problem tag IDs
const getProblemTagIds = (problem: ProblemsRecord): string[] => {
return sessionStore.getProblemTagIds(problem);
};
// Applied search terms
const appliedSearchTerms = computed(() => {
if (appliedSearchQuery.value.trim() === "") return [];
return appliedSearchQuery.value
.split(/\s+/)
.filter(term => term.trim() !== "");
});
// Calculate top tags with information gain (simplified version for dashboard)
const sortedTagsByFrequency = computed(() => {
if (problemTags.value.length === 0 || problems.value.length === 0) return [];
const itemFrequency = new Map<string, {
tag: ProblemTagsRecord;
count: number;
}>();
// Count tag frequencies
problemTags.value.forEach((tag) => {
if (selectedTags.value.includes(tag.id)) return;
const problemsWithTag = problems.value.filter((problem) => {
const tagIds = getProblemTagIds(problem);
return tagIds.includes(tag.id);
});
if (problemsWithTag.length > 0) {
itemFrequency.set(tag.id, {
tag,
count: problemsWithTag.length,
});
}
});
return Array.from(itemFrequency.values());
});
// Top 15 tags for dashboard
const topTagsForDashboard = computed(() => {
return sortedTagsByFrequency.value
.slice(0, 15)
.map(item => ({
tag: item.tag,
type: "tag" as const,
count: item.count,
problems: []
}));
});
// Check if undo is available
const canUndo = computed(() => selectionHistory.value.length > 0);
// Search suggestions
const searchSuggestions = computed(() => {
if (searchQuery.value.trim().length < 2) return [];
const query = searchQuery.value.toLowerCase();
const suggestions: Array<{ id: string; name: string; type: 'tag' | 'job' }> = [];
// Add matching tags
problemTags.value.forEach((tag) => {
if (tag.name?.toLowerCase().includes(query)) {
if (!selectedTags.value.includes(tag.id)) {
suggestions.push({
id: tag.id,
name: tag.name || '',
type: 'tag'
});
}
}
});
// Add matching job titles
problems.value.forEach((problem) => {
if (problem.name?.toLowerCase().includes(query)) {
suggestions.push({
id: problem.id,
name: problem.name || '',
type: 'job'
});
}
});
return suggestions.slice(0, 10);
});
// Get chip color
const getChipColor = (itemData: { tag?: ProblemTagsRecord; type: string }) => {
if (itemData.type === "tag" && itemData.tag) {
return selectedTags.value.includes(itemData.tag.id) ? "tertiary" : "primary";
}
return "primary";
};
// Search functions
const selectTagAndNavigate = async (itemData: { tag?: ProblemTagsRecord }) => {
if (itemData.tag) {
await sessionStore.selectTag(itemData.tag.id);
router.push("/search");
}
};
const applySearchAndNavigate = async () => {
if (searchQuery.value.trim() === "") return;
await sessionStore.setSearchQuery(searchQuery.value);
searchQuery.value = "";
router.push("/search");
};
const removeSearchTerm = async (index: number) => {
await sessionStore.removeSearchTerm(index);
};
const clearSearch = async () => {
searchQuery.value = "";
await sessionStore.clearSearch();
};
const undoLastAction = async () => {
await sessionStore.undoSearch();
};
const handleSearchKeydown = (event: KeyboardEvent) => {
if (!showSuggestions.value || searchSuggestions.value.length === 0) {
if (event.key === "Enter") {
applySearchAndNavigate();
}
return;
}
if (event.key === "ArrowDown") {
event.preventDefault();
selectedSuggestionIndex.value = Math.min(
selectedSuggestionIndex.value + 1,
searchSuggestions.value.length - 1
);
} else if (event.key === "ArrowUp") {
event.preventDefault();
selectedSuggestionIndex.value = Math.max(selectedSuggestionIndex.value - 1, -1);
} else if (event.key === "Enter") {
event.preventDefault();
if (selectedSuggestionIndex.value >= 0) {
selectSuggestion(searchSuggestions.value[selectedSuggestionIndex.value]);
} else {
applySearchAndNavigate();
}
} else if (event.key === "Escape") {
showSuggestions.value = false;
selectedSuggestionIndex.value = -1;
}
};
const handleSearchInput = () => {
showSuggestions.value = searchQuery.value.trim().length >= 2;
selectedSuggestionIndex.value = -1;
};
const handleSearchFocus = () => {
if (searchQuery.value.trim().length >= 2) {
showSuggestions.value = true;
}
};
const handleSearchBlur = () => {
setTimeout(() => {
showSuggestions.value = false;
selectedSuggestionIndex.value = -1;
}, 200);
};
const selectSuggestion = async (suggestion: { id: string; name: string; type: 'tag' | 'job' }) => {
if (suggestion.type === 'tag') {
if (!selectedTags.value.includes(suggestion.id)) {
await sessionStore.selectTag(suggestion.id);
}
searchQuery.value = "";
showSuggestions.value = false;
selectedSuggestionIndex.value = -1;
router.push("/search");
} else if (suggestion.type === 'job') {
// Navigate to view job page
router.push(`/view-job/${suggestion.id}`);
searchQuery.value = "";
showSuggestions.value = false;
selectedSuggestionIndex.value = -1;
}
};
// Get 5 random jobs as bookmarks (mock data)
const bookmarkedJobs = computed(() => {
if (problems.value.length === 0) return [];
// Create a copy and shuffle
const shuffled = [...problems.value].sort(() => 0.5 - Math.random());
// Return first 5
return shuffled.slice(0, 5);
});
// Select a bookmarked job
const selectBookmarkedJob = (problem: ProblemsRecord) => {
closeBookmarksModal();
router.push(`/view-job/${problem.id}`);
};
// Menus shown metrics
const menusShownCount = computed(() => sessionStore.menusShownCount);
const serviceCallsCount = computed(() => sessionStore.serviceCallsCount);
const menusShownGoal = computed(() => sessionStore.menusShownGoal);
// Calculate menus shown progress: (Total Menus Shown) ÷ (Number of Service Calls × Goal)
const menusShownProgress = computed(() => {
if (serviceCallsCount.value === 0) return 0;
const targetMenus = serviceCallsCount.value * menusShownGoal.value;
const progress = menusShownCount.value / targetMenus;
return Math.min(progress, 1); // Cap at 100%
});
// Rise metrics
const higherTierChosenCount = computed(() => sessionStore.higherTierChosenCount);
// Calculate rise progress: (Higher Tier Options Chosen) ÷ (Total Menus Shown)
const riseProgress = computed(() => {
if (menusShownCount.value === 0) return 0;
const progress = higherTierChosenCount.value / menusShownCount.value;
return Math.min(progress, 1); // Cap at 100%
});
// Close rate metrics
const serviceCallsWithMenusShown = computed(() => sessionStore.serviceCallsWithMenusShown);
// Calculate close rate progress: (Service Calls with Menus Shown) ÷ (Total Service Calls)
const closeRateProgress = computed(() => {
if (serviceCallsCount.value === 0) return 0;
const progress = serviceCallsWithMenusShown.value / serviceCallsCount.value;
return Math.min(progress, 1); // Cap at 100%
});
// Format close rate as percentage for display
const closeRatePercentage = computed(() => {
if (serviceCallsCount.value === 0) return '0%';
const percentage = Math.round((serviceCallsWithMenusShown.value / serviceCallsCount.value) * 100);
return `${percentage}%`;
});
// Jobs to display: mock jobs if no real jobs have ever been added, otherwise real jobs (or empty)
const displayJobs = computed(() => {
// If real jobs have been added, never show mock data again (even if jobs array is empty)
if (sessionStore.hasHadRealJobs) {
// Show real jobs (or empty array if all deleted)
return sessionStore.jobs.map(job => ({
id: job.id,
title: job.problem?.name || job.title || 'Untitled Job',
hours: job.baseHours && job.extraTime
? `${(job.baseHours * job.extraTime).toFixed(1)} h`
: '--- h',
blinkFixHours: job.extraTime === undefined || job.extraTime === 0,
hasConfirmedOffer: !!(job.selectedTierName && job.selectedPrice),
jobData: job,
}));
}
// Return empty array if no jobs
return [];
});
// Elapsed time (counts up from session start)
const elapsedSeconds = ref(0);
let timerInterval: number | null = null;
const formatTime = (totalSeconds: number): string => {
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const pad = (num: number) => String(num).padStart(2, "0");
return `${pad(hours)}:${pad(minutes)}`;
};
// Estimated time: 45 min (diagnosis) if no jobs have menus presented,
// otherwise sum of all job estimates
const estimatedTime = computed(() => {
// Check if any jobs have been presented with menus (have extraTime set)
const jobsWithMenus = sessionStore.jobs.filter(job => job.extraTime && job.extraTime > 0);
if (jobsWithMenus.length === 0) {
// No menus presented yet, show 45 min diagnosis estimate
return formatTime(45 * 60);
}
// Sum up all job estimates (baseHours * extraTime)
const totalHours = sessionStore.jobs.reduce((sum, job) => {
if (job.baseHours && job.extraTime) {
return sum + (job.baseHours * job.extraTime);
}
return sum;
}, 0);
return formatTime(Math.round(totalHours * 3600));
});
const elapsedTime = ref(formatTime(elapsedSeconds.value));
const updateTimers = () => {
// Count up elapsed time from session start
if (sessionStore.startTime) {
const startTime = new Date(sessionStore.startTime).getTime();
const now = Date.now();
elapsedSeconds.value = Math.floor((now - startTime) / 1000);
// Only update display when minute changes
if (elapsedSeconds.value % 60 === 0) {
elapsedTime.value = formatTime(elapsedSeconds.value);
}
} else {
// No session started yet
elapsedSeconds.value = 0;
elapsedTime.value = formatTime(0);
}
};
onMounted(async () => {
await sessionStore.load();
// Load problems and tags for search functionality
await sessionStore.loadProblemsAndTags();
// Set test data for demo purposes
sessionStore.serviceCallsCount = 2;
sessionStore.menusShownCount = 4;
sessionStore.higherTierChosenCount = 1;
sessionStore.serviceCallsWithMenusShown = 2;
timerInterval = window.setInterval(updateTimers, 1000);
});
onUnmounted(() => {
if (timerInterval !== null) {
window.clearInterval(timerInterval);
}
});
</script>
<style scoped>
.dashboard-container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.header-section {
margin-bottom: 32px;
}
.title-container {
position: relative;
margin-bottom: 24px;
}
.title-icons-left {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
display: flex;
gap: 8px;
align-items: center;
}
.title-icons-left ion-icon {
font-size: 48px;
}
.title-icons-right {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
display: flex;
gap: 8px;
align-items: center;
}
.title-icons-right ion-icon {
font-size: 48px;
}
.star-icon-wrapper {
position: relative;
display: inline-block;
}
.star-icon-shadow {
position: absolute;
top: 2px;
left: -2px;
font-size: 48px;
z-index: 0;
}
.star-icon-front {
position: relative;
z-index: 1;
}
.clickable-icon {
cursor: pointer;
transition: transform 0.2s ease, color 0.2s ease;
}
.clickable-icon:hover {
transform: scale(1.1);
color: var(--ion-color-primary-shade);
}
.clickable-icon:active {
transform: scale(0.95);
}
.page-title {
margin: 0;
color: #ffffff;
font-size: 48px;
font-weight: 700;
text-align: center;
font-variant: small-caps;
}
.content-section {
padding: 0 20px 20px 20px;
}
.progress-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
max-width: 1000px;
margin: 0 auto 24px auto;
}
.progress-column {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.progress-number {
font-size: 43px;
font-weight: 700;
color: var(--ion-color-primary);
font-variant: normal;
font-family: "Courier New", Courier, monospace;
letter-spacing: 4px;
text-align: center;
}
.progress-label {
font-size: 22px;
font-weight: 600;
color: var(--ion-color-medium);
font-variant: small-caps;
text-align: center;
}
.progress-wrapper {
position: relative;
width: 100%;
height: 43px;
border-radius: 12px;
background: var(--ion-color-medium);
overflow: hidden;
}
.progress-gradient {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
to right,
var(--ion-color-secondary) 0%,
var(--ion-color-primary) 100%
);
border-radius: 12px;
}
.progress-cover {
position: absolute;
top: 0;
right: 0;
height: 100%;
background: var(--ion-color-medium);
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
transition: width 0.3s ease;
}
/* Mobile adjustments for progress bars */
@media (max-width: 767px) {
.progress-number {
font-size: 34px;
}
.progress-label {
font-size: 14px;
}
.progress-wrapper {
height: 34px;
}
}
.clocks-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
max-width: 1000px;
margin: 0 auto;
}
.divider {
width: 100%;
height: 1px;
background-color: #d3d3d3;
margin: 40px 0;
max-width: 1000px;
margin-left: auto;
margin-right: auto;
}
.divider-compact {
width: 100%;
height: 1px;
background-color: #d3d3d3;
margin: 16px 0;
max-width: 1000px;
margin-left: auto;
margin-right: auto;
}
/* Dashboard Search Section */
.dashboard-search-section {
margin-top: 0;
margin-bottom: 32px;
width: 100%;
max-width: 1000px;
margin-left: auto;
margin-right: auto;
padding-bottom: 100px;
}
.dashboard-search-title {
margin: 0 0 24px 0;
color: #ffffff;
font-size: 32px;
font-weight: 600;
text-align: center;
font-variant: small-caps;
}
.dashboard-search-tags-container {
display: flex;
flex-wrap: wrap;
gap: 16px;
justify-content: center;
align-items: center;
padding: 24px;
background-color: #000000;
border-radius: 12px;
}
.dashboard-search-tag-chip {
cursor: pointer;
margin: 0;
text-transform: lowercase;
font-size: 20px;
height: 48px;
padding: 0 24px;
white-space: nowrap;
}
.dashboard-search-tag-chip:hover {
filter: brightness(1.1);
transform: scale(1.02);
transition: all 0.2s ease;
}
.dashboard-search-term-chip {
/* Blue/secondary color chips for search terms */
}
.dashboard-search-bar {
display: flex;
gap: 8px;
align-items: center;
background-color: var(--ion-color-dark);
padding: 12px;
border-radius: 8px;
margin-top: 24px;
}
.dashboard-search-input-container {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
}
.dashboard-search-input {
width: 100%;
font-size: 20px;
min-height: 48px;
}
.dashboard-search-undo-wrapper {
position: relative;
display: inline-block;
}
.dashboard-search-undo-button {
margin: 0;
min-width: 44px;
}
.dashboard-search-undo-shadow {
position: absolute;
top: 2px;
left: -2px;
z-index: 0;
}
.dashboard-search-undo-front {
position: relative;
z-index: 1;
--color: var(--ion-color-tertiary);
}
/* Suggestions dropdown */
.suggestions-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
margin-top: 4px;
background: var(--ion-color-dark);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 1px solid var(--ion-color-medium);
max-height: 400px;
overflow-y: auto;
}
.suggestions-dropdown ion-list {
background: transparent;
padding: 0;
}
.suggestion-item {
--background: var(--ion-color-dark);
--border-color: transparent;
cursor: pointer;
transition: background 0.2s ease;
}
.suggestion-item:hover,
.suggestion-item.suggestion-selected {
--background: var(--ion-color-medium);
}
.suggestion-item ion-label {
font-size: 15px;
color: var(--ion-color-light);
}
.suggestion-item .tag-icon {
color: var(--ion-color-primary);
font-size: 20px;
}
.suggestion-item .job-icon {
color: var(--ion-color-success);
font-size: 20px;
}
.jobs-section {
max-width: 1000px;
margin: 0 auto 16px auto;
display: flex;
flex-direction: column;
gap: 16px;
}
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: var(--ion-color-primary);
padding: 20px;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
cursor: pointer;
transition: transform 0.1s ease;
}
.action-bar:hover {
background-color: var(--ion-color-primary-shade);
}
.action-bar:active {
transform: scale(0.98);
}
.action-bar-disabled {
background-color: var(--ion-color-medium);
cursor: not-allowed;
}
.action-bar-disabled:hover {
background-color: var(--ion-color-medium);
}
.action-bar-disabled:active {
transform: none;
}
.action-bar-content {
max-width: 1000px;
margin: 0 auto;
text-align: center;
}
.action-bar-text {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #ffffff;
font-variant: small-caps;
}
.info-content {
font-size: 16px;
line-height: 1.6;
color: var(--ion-color-light);
}
.info-content h2 {
margin-top: 24px;
margin-bottom: 12px;
font-size: 20px;
font-weight: 700;
color: var(--ion-color-primary);
}
.info-content h2:first-child {
margin-top: 0;
}
.info-content p {
margin-bottom: 16px;
color: var(--ion-color-light);
}
.info-content p:last-child {
margin-bottom: 0;
}
/* Mobile adjustments */
@media (max-width: 767px) {
.dashboard-container {
padding: 16px;
}
.page-title {
font-size: 36px;
}
.progress-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.clocks-grid {
grid-template-columns: 1fr;
gap: 16px;
}
}
/* Bookmarks Modal Styles */
.bookmarks-content {
font-size: 16px;
line-height: 1.6;
color: var(--ion-color-light);
}
.bookmarks-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.bookmark-item {
display: flex;
gap: 16px;
padding: 16px;
background-color: var(--ion-color-dark);
border-radius: 8px;
cursor: pointer;
transition: background-color 0.2s ease;
border-left: 3px solid var(--ion-color-warning);
}
.bookmark-item:hover {
background-color: rgba(118, 221, 84, 0.1);
}
.bookmark-star {
font-size: 24px;
flex-shrink: 0;
margin-top: 4px;
}
.bookmark-details {
flex: 1;
}
.bookmark-title {
margin: 0 0 8px 0;
font-size: 20px;
font-weight: 600;
color: var(--ion-color-light);
}
.bookmark-description {
margin: 0;
font-size: 14px;
line-height: 1.5;
color: var(--ion-color-medium);
max-width: 600px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>