Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
<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>