Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
<template>
  <div class="dashboard-job" @click="handleCardClick">
    <div class="hours-display">
      <div class="hours-value">{{ hours }}</div>
    </div>
    <div class="job-content">
      <div class="job-header">
        <h3 class="job-title">{{ title }}</h3>
        <div v-if="hasConfirmedOffer" class="job-buttons">
          <ion-button
            fill="solid"
            color="success"
            size="default"
            class="copy-button"
            @click.stop="handleCopy"
          >
            <ion-icon :icon="copyOutline" slot="start"></ion-icon>
            Copy
          </ion-button>
          <ion-button
            fill="outline"
            color="primary"
            size="default"
            class="details-button"
            @click.stop="handleShowDetails"
          >
            <ion-icon :icon="documentTextOutline" slot="icon-only"></ion-icon>
          </ion-button>
        </div>
        <div v-else class="job-buttons">
          <button class="job-button" @click.stop="handleEdit">{{ button2 }}</button>
          <button class="job-button job-button-danger" @click.stop="handleDelete">{{ button4 }}</button>
        </div>
      </div>
    </div>
  </div>

  <!-- Details Modal -->
  <ion-modal :is-open="isModalOpen" @didDismiss="closeModal">
    <ion-header>
      <ion-toolbar>
        <ion-title>Job Details</ion-title>
        <ion-buttons slot="end">
          <ion-button @click="closeModal">Close</ion-button>
        </ion-buttons>
      </ion-toolbar>
    </ion-header>
    <ion-content class="ion-padding">
      <div class="details-content">
        <div v-if="jobData?.selectedTierTitle" class="detail-section">
          <div class="detail-header">
            <h3 class="detail-label">Offer Title</h3>
            <ion-icon :icon="copyOutline" class="copy-icon" @click="copySectionToClipboard('Offer Title', jobData.selectedTierTitle)"></ion-icon>
          </div>
          <p class="detail-value detail-offer-title">{{ jobData.selectedTierTitle }}</p>
        </div>

        <div class="detail-section">
          <div class="detail-header">
            <h3 class="detail-label">Job</h3>
            <ion-icon :icon="copyOutline" class="copy-icon" @click="copySectionToClipboard('Job', jobData?.problem?.name || jobData?.title || 'Untitled Job')"></ion-icon>
          </div>
          <p class="detail-value">{{ jobData?.problem?.name || jobData?.title || 'Untitled Job' }}</p>
        </div>

        <div v-if="jobData?.problem?.description" class="detail-section">
          <div class="detail-header">
            <h3 class="detail-label">Description</h3>
            <ion-icon :icon="copyOutline" class="copy-icon" @click="copySectionToClipboard('Description', jobData.problem.description)"></ion-icon>
          </div>
          <p class="detail-value">{{ jobData.problem.description }}</p>
        </div>

        <div v-if="jobData?.selectedTierName" class="detail-section">
          <div class="detail-header">
            <h3 class="detail-label">Selected Option</h3>
            <ion-icon :icon="copyOutline" class="copy-icon" @click="copySectionToClipboard('Selected Option', jobData.selectedTierName)"></ion-icon>
          </div>
          <p class="detail-value detail-tier" :class="getTierBadgeClass(jobData.selectedTierName)">
            {{ jobData.selectedTierName }}
          </p>
        </div>

        <div v-if="jobData?.selectedPrice" class="detail-section">
          <div class="detail-header">
            <h3 class="detail-label">Price</h3>
            <ion-icon :icon="copyOutline" class="copy-icon" @click="copySectionToClipboard('Price', '$' + formatPrice(jobData.selectedPrice))"></ion-icon>
          </div>
          <p class="detail-value detail-price">${{ formatPrice(jobData.selectedPrice) }}</p>
        </div>

        <div v-if="jobData?.baseHours" class="detail-section">
          <div class="detail-header">
            <h3 class="detail-label">Estimated Time</h3>
            <ion-icon :icon="copyOutline" class="copy-icon" @click="copySectionToClipboard('Estimated Time', ((jobData.baseHours || 0) + (jobData.extraTime || 0)).toFixed(1) + ' hours')"></ion-icon>
          </div>
          <p class="detail-value">{{ ((jobData.baseHours || 0) + (jobData.extraTime || 0)).toFixed(1) }} hours</p>
        </div>

        <div v-if="jobData?.notes && jobData.notes.length > 0" class="detail-section">
          <div class="detail-header">
            <h3 class="detail-label">Notes</h3>
            <ion-icon :icon="copyOutline" class="copy-icon" @click="copySectionToClipboard('Notes', jobData.notes.join('\\n'))"></ion-icon>
          </div>
          <div class="detail-notes">
            <p v-for="(note, index) in jobData.notes" :key="index" class="detail-note">{{ note }}</p>
          </div>
        </div>
      </div>
    </ion-content>
  </ion-modal>
</template>

<script setup lang="ts">
import { ref } from "vue";
import BlinkingButton from "@/components/BlinkingButton.vue";
import { IonButton, IonIcon, IonModal, IonHeader, IonToolbar, IonTitle, IonButtons, IonContent } from "@ionic/vue";
import { copyOutline, documentTextOutline } from "ionicons/icons";

const props = withDefaults(defineProps<{
  title?: string;
  hours?: string;
  button1?: string;
  button2?: string;
  button3?: string;
  button4?: string;
  blinkFixHours?: boolean;
  hasConfirmedOffer?: boolean;
  jobData?: any;
}>(), {
  title: "Job Title",
  hours: "3.5 h",
  button1: "Fix Hours",
  button2: "Edit",
  button3: "Details",
  button4: "Delete",
  blinkFixHours: false,
  hasConfirmedOffer: false,
  jobData: null
});

const isModalOpen = ref(false);

const emit = defineEmits<{
  delete: []
  details: []
  fixHours: []
  edit: []
  copied: []
  cardClick: []
}>();

const handleFixHours = () => {
  emit('fixHours');
};

const handleEdit = () => {
  emit('edit');
};

const handleDelete = () => {
  emit('delete');
};

const handleDetails = () => {
  emit('details');
};

const handleCardClick = () => {
  emit('cardClick');
};

const handleCopy = async () => {
  if (!props.jobData) return;

  const job = props.jobData;

  // Build the clipboard text
  let clipboardText = '';

  if (job.selectedTierTitle) {
    clipboardText += `Offer Title: ${job.selectedTierTitle}\n\n`;
  }

  clipboardText += `Job: ${job.problem?.name || job.title || 'Untitled Job'}\n`;

  if (job.problem?.description) {
    clipboardText += `Description: ${job.problem.description}\n`;
  }

  clipboardText += `\n`;

  if (job.selectedTierName) {
    clipboardText += `Selected Option: ${job.selectedTierName}\n`;
  }

  if (job.selectedPrice) {
    const price = parseFloat(job.selectedPrice);
    if (!isNaN(price)) {
      clipboardText += `Price: $${price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}\n`;
    }
  }

  if (job.baseHours) {
    const hours = (job.baseHours || 0) + (job.extraTime || 0);
    clipboardText += `Estimated Time: ${hours.toFixed(1)} hours\n`;
  }

  if (job.notes && job.notes.length > 0) {
    clipboardText += `\nNotes:\n${job.notes.join('\n')}`;
  }

  try {
    await navigator.clipboard.writeText(clipboardText);
    // Could add a toast notification here if desired
    console.log('Copied to clipboard:', clipboardText);

    // Emit copied event
    emit('copied');
  } catch (err) {
    console.error('Failed to copy to clipboard:', err);
  }
};

const handleShowDetails = () => {
  isModalOpen.value = true;
};

const closeModal = () => {
  isModalOpen.value = false;
};

const formatPrice = (priceStr: string): string => {
  const price = parseFloat(priceStr);
  if (isNaN(price)) return '0.00';
  return price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
};

const getTierBadgeClass = (tierName: string): string => {
  const name = tierName.toLowerCase();
  if (name.includes('platinum')) return 'badge-platinum';
  if (name.includes('gold')) return 'badge-gold';
  if (name.includes('silver')) return 'badge-silver';
  if (name.includes('bronze')) return 'badge-bronze';
  if (name.includes('band')) return 'badge-bandaid';
  return '';
};

const copySectionToClipboard = async (label: string, value: string) => {
  const text = `${label}: ${value}`;
  try {
    await navigator.clipboard.writeText(text);
    console.log(`Copied ${label} to clipboard:`, text);

    // Emit copied event for individual section copies too
    emit('copied');
  } catch (err) {
    console.error('Failed to copy to clipboard:', err);
  }
};
</script>

<style scoped>
.dashboard-job {
  background: var(--ion-color-medium);
  border-radius: 12px;
  display: flex;
  gap: 0;
  overflow: hidden;
  transition: all 0.2s ease;
  cursor: pointer;
}

.dashboard-job:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.hours-display {
  background: var(--ion-color-primary);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  min-width: 120px;
  flex-shrink: 0;
}

.hours-value {
  font-size: 43px;
  font-weight: 700;
  color: var(--ion-color-light);
  font-family: "Courier New", Courier, monospace;
  letter-spacing: 2px;
  white-space: nowrap;
}

.job-content {
  flex: 1;
  padding: 20px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.job-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
}

.job-title {
  margin: 0;
  font-size: 20px;
  font-weight: 600;
  color: #ffffff;
  font-variant: small-caps;
}

.job-buttons {
  display: flex;
  gap: 12px;
  align-items: center;
}

.job-button {
  background: transparent;
  border: 2px solid var(--ion-color-secondary);
  border-radius: 8px;
  padding: 8px 16px;
  font-size: 14px;
  font-weight: 600;
  color: var(--ion-color-secondary);
  cursor: pointer;
  transition: all 0.2s ease;
  font-variant: small-caps;
  white-space: nowrap;
}

.job-button:hover {
  background: var(--ion-color-secondary);
  color: #ffffff;
}

.job-button:active {
  transform: scale(0.95);
}

.job-button-danger {
  border-color: var(--ion-color-danger);
  color: var(--ion-color-danger);
}

.job-button-danger:hover {
  background: var(--ion-color-danger);
  color: #ffffff;
}

.job-button-ionic {
  font-size: 14px;
  font-weight: 600;
  font-variant: small-caps;
  --padding-start: 16px;
  --padding-end: 16px;
  --padding-top: 8px;
  --padding-bottom: 8px;
  height: auto;
  margin: 0;
}

.copy-button {
  font-size: 16px;
  font-weight: 700;
  font-variant: small-caps;
  --padding-start: 20px;
  --padding-end: 20px;
  --padding-top: 10px;
  --padding-bottom: 10px;
  height: auto;
  margin: 0;
}

.details-button {
  font-size: 16px;
  font-weight: 700;
  font-variant: small-caps;
  --padding-start: 20px;
  --padding-end: 20px;
  --padding-top: 10px;
  --padding-bottom: 10px;
  height: auto;
  margin: 0;
}

/* Modal Styles */
.details-content {
  max-width: 600px;
  margin: 0 auto;
}

.detail-section {
  margin-bottom: 24px;
  padding-bottom: 16px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}

.detail-section:last-child {
  border-bottom: none;
}

.detail-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 8px;
}

.detail-label {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--ion-color-medium);
}

.copy-icon {
  font-size: 20px;
  color: var(--ion-color-primary);
  cursor: pointer;
  transition: transform 0.2s ease, color 0.2s ease;
  padding: 4px;
}

.copy-icon:hover {
  transform: scale(1.2);
  color: var(--ion-color-primary-tint);
}

.copy-icon:active {
  transform: scale(1.0);
}

.detail-value {
  margin: 0;
  font-size: 18px;
  line-height: 1.5;
  color: var(--ion-text-color);
}

.detail-offer-title {
  font-size: 22px;
  font-weight: 600;
  color: var(--ion-color-primary);
  text-transform: uppercase;
}

.detail-tier {
  display: inline-block;
  padding: 8px 16px;
  border-radius: 8px;
  font-weight: 700;
  text-transform: uppercase;
  color: #000;
}

.badge-platinum {
  background: linear-gradient(135deg, var(--menu-color-platinum, #3db4d6) 0%, var(--menu-color-platinum-tint, #5ec3dd) 100%);
}

.badge-gold {
  background: linear-gradient(135deg, var(--menu-color-gold, #ffd83b) 0%, var(--menu-color-gold-tint, #ffe066) 100%);
}

.badge-silver {
  background: linear-gradient(135deg, var(--menu-color-silver, #bfbfbf) 0%, var(--menu-color-silver-tint, #d4d4d4) 100%);
}

.badge-bronze {
  background: linear-gradient(135deg, var(--menu-color-bronze, #ffad2b) 0%, var(--menu-color-bronze-tint, #ffc04d) 100%);
}

.badge-bandaid {
  background: linear-gradient(135deg, var(--menu-color-bandaid, #ff8073) 0%, var(--menu-color-bandaid-tint, #ff9a8f) 100%);
}

.detail-price {
  font-size: 32px;
  font-weight: 700;
  color: var(--ion-color-success);
}

.detail-notes {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.detail-note {
  margin: 0;
  padding: 12px;
  background-color: rgba(255, 255, 255, 0.1);
  border: 1px solid var(--ion-color-medium);
  border-radius: 8px;
  font-size: 16px;
  line-height: 1.5;
  color: var(--ion-text-color);
}

/* Mobile adjustments */
@media (max-width: 767px) {
  .hours-display {
    padding: 16px;
    min-width: 100px;
  }

  .hours-value {
    font-size: 34px;
  }

  .job-content {
    padding: 16px;
  }

  .job-title {
    font-size: 18px;
  }

  .copy-button,
  .details-button {
    font-size: 14px;
    --padding-start: 16px;
    --padding-end: 16px;
    --padding-top: 8px;
    --padding-bottom: 8px;
  }

  .detail-value {
    font-size: 16px;
  }

  .detail-price {
    font-size: 28px;
  }
}
</style>