Hello from MCP server
<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>